PHP 内部开发指南:深入解析 .stub.php 存根文件与 gen_stub.php 代码生成机制
本文以 php-src 官方文档 docs/source/miscellaneous/stubs.rst 为主体,系统讲解 PHP 源码树中存根(stub)文件的工作机制:如何用纯 PHP 声明替代手写 ZEND_BEGIN_ARG_* 宏、如何生成 arginfo 结构、函数表、类注册代码、全局常量与属性注册函数、优化器函数信息,以及如何维护手册文档签名与跨版本向后兼容。读完本篇,你将能够阅读 php-src 中任意 *.stub.php 文件及其生成的 *_arginfo.h 产物,理解二者之间的对应关系,并为第三方扩展正确编写 stub 与 PHPDoc 元信息标签。
什么是 Stub 文件:只有声明、没有可执行代码的 PHP 片段
Stub 文件是只包含声明、不包含可运行代码的 PHP 代码片段,函数和方法体全部为空。其基本形态如下(引自官方文档):
<?php
/** @var string */
const ANIMAL = "Elephant";
/** @var float */
const WEIGHT = 6.8;
class Atmosphere {
public function calculateBar(): float {}
}
function fahrenheitToCelsius(float $fahrenheitToCelsius): float {}
关于 stub 的约束能力,官方文档给出了明确的边界:
- 任何种类的符号都可以用 stub 声明,所有类型都可以使用,唯一例外是不支持析取范式(DNF)类型(即类似
A&B|C这种复合类型); - 额外元信息通过 PHPDoc 块或 PHP 属性(attributes) 附加;
- 命名空间可以通过顶层
namespace声明或**命名空间块(namespace blocks)**使用:
<?php
namespace {
/** @var string */
const ANIMAL = "Elephant";
/** @var float */
const WEIGHT_TON = 6.8;
class Atmosphere {
public function calculateBar(): float {}
}
}
namespace Algorithms {
function fahrenheitToCelsius(float $fahrenheit): float {}
}
上述例子在顶层(全局)命名空间声明了常量 ANIMAL、WEIGHT_TON 和类 Atmosphere,而 fahrenheitToCelsius() 声明在 Algorithms 命名空间中。
在 php-src 仓库中可以随处可见这类文件:Zend/zend_enum.stub.php 声明了 UnitEnum、BackedEnum 等枚举基础接口,ext/bz2/bz2.stub.php 则声明了整个 bz2 扩展的全部函数签名。从源码结构看,Zend/ 目录下有十余个 *.stub.php(zend_closures、zend_exceptions、zend_fibers、zend_generators、zend_weakrefs 等),ext/ 下几乎每个内置扩展(bcmath、curl、date、json、standard……)都有自己的 stub 文件——这正是官方文档所述"stubs 是 PHP 源码发行版的一部分,只需支持其所属分支"的落地形态。
使用 gen_stub.php:stub 的处理引擎
按约定,stub 文件使用 .stub.php 扩展名。它们由 build/gen_stub.php(一个超过 6000 行的 PHP 脚本)处理,脚本内部使用 PHP-Parser 库进行解析。值得注意的是,gen_stub.php 会在首次运行时自动下载并解压 PHP-Parser 5.6.1 到 build/PHP-Parser-5.6.1/ 目录并注册自动加载器(见 build/gen_stub.php 中的 installPhpParser() 与 initPhpParser() 函数),且要求本机 PHP 已加载 tokenizer 扩展。
根据配置和传入参数,gen_stub.php 可以生成多种产物。其完整命令行用法(取自脚本自身的 --help 输出)为:
Usage: gen_stub.php [ -f | --force-regeneration ] [ --replace-predefined-constants ] [ --generate-classsynopses ] [ --replace-classsynopses ] [ --generate-methodsynopses ] [ --replace-methodsynopses ] [ --parameter-stats ] [ --verify ] [ --verify-manual ] [ --generate-optimizer-info ] [ -h | --help ] [ name.stub.php | directory ] [ directory ]
其中位置参数可以是单个 name.stub.php 文件,也可以是目录(如 ./ 表示整个源码树)。手册相关的选项(--generate-methodsynopses、--replace-methodsynopses、--replace-classsynopses、--verify-manual)需要至少两个位置参数:stub 源目录和目标手册目录,缺少时脚本会直接报错退出。
生成 arginfo 结构:用 PHP 声明替代易错的 C 宏
Stub 文件的核心目的是简化 arginfo 结构的声明、校验参数解析声明并维护文档。在此之前,开发者必须手工编写各种 ZEND_BEGIN_ARG_* ... ZEND_END_ARG_INFO() 宏——官方文档直言这是一个枯燥且易错的过程,而"能够使用纯 PHP 代码(并从中生成 C 代码)是一项巨大收益"。
上文第一个例子生成的 arginfo 文件如下:
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: e4ed788d54a20272a92a3f6618b73d48ec848f97 */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_fahrenheitToCelsius, 0, 1, IS_DOUBLE, 0)
ZEND_ARG_TYPE_INFO(0, fahrenheitToCelsius, IS_DOUBLE, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Atmosphere_calculateBar, 0, 0, IS_DOUBLE, 0)
ZEND_END_ARG_INFO()
这个机制在仓库中有大量真实例证。以 ext/bz2/bz2.stub.php 为例:
/** @param resource $bz */
function bzread($bz, int $length = 1024): string|false {}
对应的生成产物 ext/bz2/bz2_arginfo.h 开头即带有文档中提到的标准文件头:
/* This is a generated file, edit bz2.stub.php instead.
* Stub hash: c2c8e0fe1e3244c8cadafe60b65b7235c105b3c9 */
其默认值 1024 被精确翻译成了 ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, length, IS_LONG, 0, "1024"),而联合返回类型 string|false 则变成了 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_bzread, 0, 1, MAY_BE_STRING|MAY_BE_FALSE)——这展示了 stub 类型声明到 C 类型位掩码的映射方式。
Stub hash:增量生成的钥匙
文件头中的 Stub hash 用于保证 stub 文件不会被重复处理:只有 stub 文件内容被修改、或有条件要求(例如使用 -f / --force-regeneration 标志强制重新生成)时才会重新生成。仓库中所有生成的 arginfo 文件头都遵循这一格式,例如 ext/bz2/bz2_arginfo.h、Zend/zend_enum_arginfo.h。
arginfo 必须与 ZPP 宏保持同步
一个必须牢记的约束:stub 中的类型声明必须与 PHP 函数中通过 ZEND_PARSE_PARAMETERS_* 宏(简称 ZPP)实现的参数解析代码保持同步。两者在运行时的表现差异取决于构建模式:
- Release 构建:arginfo 结构只用于 Reflection;
- Debug 构建:PHP 会将 arginfo 结构与 ZPP 宏做对比,确保参数和返回类型在 stub 与实际数据之间一致;不一致时会产生错误。
因此在 stub 文件中声明正确的类型至关重要。仓库中保留了针对此场景的测试用例:Zend/tests/arginfo_zpp_mismatch.phpt、Zend/tests/arginfo_zpp_mismatch_strict.phpt 及其共享片段 Zend/tests/arginfo_zpp_mismatch.inc,它们验证了 debug 构建下 arginfo 与 ZPP 不匹配时确实会触发内部一致性检查。
对于文档用途,可以在 stub 中使用 PHPDoc。
自 PHP 8.0 起:arginfo 可携带默认值
自 PHP 8.0 起,arginfo 结构还可以包含默认值,例如可被 ReflectionParameter::getDefaultValue() 使用。除了常量字面量,默认值还可以包含可在编译期求值的表达式,并能引用常量。
下面的例子定义了一个带可选参数、且默认值引用常量的函数:
<?php
/** @var string */
const ANIMAL = "Elephant";
function formatName(string $defaultName = ANIMAL . " Mc" . ANIMAL . "Face"): string {}
生成的 arginfo 为:
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: a9685164284e73f47b15838122b631ebdfef23d6 */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_formatName, 0, 0, IS_STRING, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, defaultName, IS_STRING, 0, "ANIMAL . \" Mc\" . ANIMAL . \"Face\"")
ZEND_END_ARG_INFO()
常量只能在其定义于同一个 stub 文件时使用。如果做不到,声明该常量的 stub 应通过 require 引入:
// constants.stub.php
<?php
/** @var string */
const ANIMAL = "Elephant";
// example.stub.php
<?php
require "constants.stub.php";
function foo(string $param = ANIMAL): string {}
按引用传参与 ZEND_SEND_PREFER_REF
有时参数必须按引用传递,或使用 ZEND_SEND_PREFER_REF 标志:
- 按引用解析:使用惯用的
&语法; - 包含
ZEND_SEND_PREFER_REF标志:使用@prefer-refPHPDoc 标签。
<?php
/**
* @param array $herd
* @prefer-ref $elephantName
*/
function addElephantsToHerd(&$herd, string $elephantName): string {}
生成结果:
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_addElephantsToHerd, 0, 2, IS_STRING, 0)
ZEND_ARG_INFO(1, herd)
ZEND_ARG_TYPE_INFO(ZEND_SEND_PREFER_REF, elephantName, IS_STRING, 0)
ZEND_END_ARG_INFO()
注意 $herd 因为是引用参数,第一个字段变为 1;$elephantName 的第一个字段则是 ZEND_SEND_PREFER_REF 而非 0。
生成功能函数表(Function Entries)
除了 arginfo 结构,函数表(function entries)本身也可以由 stub 生成。为此需在文件级添加 @generate-function-entries PHPDoc 标签:
<?php
/** @generate-function-entries */
class Atmosphere {
public function calculateBar(): float {}
}
function fahrenheitToCelsius(float $fahrenheit): float {}
生成的 C 代码如下:
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_fahrenheitToCelsius, 0, 1, IS_DOUBLE, 0)
ZEND_ARG_TYPE_INFO(0, fahrenheit, IS_DOUBLE, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Atmosphere_calculateBar, 0, 0, IS_DOUBLE, 0)
ZEND_END_ARG_INFO()
ZEND_FUNCTION(fahrenheitToCelsius);
ZEND_METHOD(Atmosphere, calculateBar);
static const zend_function_entry ext_functions[] = {
ZEND_FE(fahrenheitToCelsius, arginfo_fahrenheitToCelsius)
ZEND_FE_END
};
static const zend_function_entry class_Atmosphere_methods[] = {
ZEND_ME(Atmosphere, calculateBar, arginfo_class_Atmosphere_calculateBar, ZEND_ACC_PUBLIC)
ZEND_FE_END
};
生成物有两条明确的"接线"规则:
- 生成的
ext_functions变量必须作为zend_module_entry结构体的functions成员传入; - 生成的
class_Atmosphere_methods必须在注册Atmosphere类时使用:
INIT_CLASS_ENTRY(ce, "Atmosphere", class_Atmosphere_methods);
对照仓库真实产物 ext/bz2/bz2_arginfo.h,可以看到 ZEND_FUNCTION(bzopen); ... 声明与 static const zend_function_entry ext_functions[] 表尾的 ZEND_FE_END 与上述文档示例结构完全一致。
函数/方法的元信息 PHPDoc 标签
函数可以附加以下元信息标签:
| 标签 | 效果 |
|---|---|
@deprecated |
调用该函数/方法时触发常规弃用通知(deprecation notice)。自 PHP 8.4 起应改用 #[Deprecated] 属性 |
@alias |
若该函数/方法是另一个函数/方法的别名,需提供被别名者名称作为值。例如 sizeof() 带有 @alias count 注解 |
@implementation-alias |
与 @alias 非常相似但有语义差别:此类别名纯粹为了避免重复代码,别名与被别名者之间没有其他联系。典型例子是 Error::getCode(),带有 @implementation-alias Exception::getCode 注解。两者区别十分微妙,只在 PHP 手册中可观察 |
@tentative-return-type |
使用该注解后,返回类型声明被重新归类为暂定返回类型(tentative return type) |
@genstubs-expose-comment-block |
放在 PHPDoc 块开头,可使该 PHPDoc 块的内容通过 ReflectionFunctionAbstract::getDocComment() 暴露。此功能自 PHP 8.4 起可用 |
生成类注册代码(Class Entries)
要生成注册常量、类、属性、枚举、trait 所需的代码,使用文件级 @generate-class-entries PHPDoc 块。@generate-class-entries 蕴含 @generate-function-entries,因此后者在此情况下是多余的。
给定以下 stub:
<?php
/** @generate-class-entries */
enum Number: string {
/** @var string */
public const ONE = "one";
case One = Number::ONE;
case Two = Number::TWO;
}
class Elephant extends stdClass {
/** @cvalue M_PI */
public const float PI = UNKNOWN;
public readonly string $name;
}
生成的 arginfo 文件(含注册函数)为:
static const zend_function_entry class_Number_methods[] = {
ZEND_FE_END
};
static const zend_function_entry class_Elephant_methods[] = {
ZEND_FE_END
};
static zend_class_entry *register_class_Number(void)
{
zend_class_entry *class_entry = zend_register_internal_enum("Number", IS_STRING, class_Number_methods);
...
return class_entry;
}
static zend_class_entry *register_class_Elephant(zend_class_entry *class_entry_stdClass)
{
zend_class_entry ce, *class_entry;
INIT_CLASS_ENTRY(ce, "Elephant", class_Elephant_methods);
class_entry = zend_register_internal_class_ex(&ce, class_entry_stdClass);
...
return class_entry;
}
生成的 register_class_*() 函数必须在 PHP_MINIT_FUNCTION 中直接调用以注册这些类:
zend_class_entry *number_ce = register_class_Number();
zend_class_entry *elephpant_ce = register_class_Elephant(zend_standard_class_def);
类依赖(父类、实现的接口)必须作为参数传入注册函数。上面的例子中传入的是 stdClass 的类条目(zend_standard_class_def)。
类的元信息标签
与函数/方法一样,类也支持通过 PHPDoc 标签传递元信息:
@deprecated:使用该类时触发弃用通知;@strict-properties:为类添加ZEND_ACC_NO_DYNAMIC_PROPERTIES标志(自 PHP 8.0),禁止动态属性;@not-serializable:为类添加ZEND_ACC_NOT_SERIALIZABLE标志(自 PHP 8.1),阻止类被序列化;@genstubs-expose-comment-block:放在 PHPDoc 块开头后,块内容可通过ReflectionClass::getDocComment()暴露。自 PHP 8.4 起可用。
包含全部标志的完整示例:
<?php
/**
* @generate-class-entries
*/
/**
* @deprecated
* @not-serializable
* @strict-properties */
/** @genstubs-expose-comment-block
* This is a comment
* @see https://www.php.net */
class Elephant extends stdClass {
public readonly string $name;
}
对应生成物中的变化:
...
static zend_class_entry *register_class_Elephant(zend_class_entry *class_entry_stdClass)
{
zend_class_entry ce, *class_entry;
INIT_CLASS_ENTRY(ce, "Elephant", class_Elephant_methods);
class_entry = zend_register_internal_class_ex(&ce, class_entry_stdClass);
class_entry->ce_flags |= ZEND_ACC_DEPRECATED|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE;
class_entry->doc_comment = zend_string_init_interned("/**\n * This is a comment\n * @see https://www.php.net */", 55, 1);
...
return class_entry;
}
可以看到三个类标志被"或"进 ce_flags,而 @genstubs-expose-comment-block 标注的第二个 PHPDoc 块被完整固化为 doc_comment 字符串常量。
生成全局常量与属性注册函数
虽然全局常量和函数属性与类无关,但它们同样要求 /** @generate-class-entries */ 文件级 PHPDoc 块。如果 stub 文件存在全局常量或函数属性,生成的 C 代码将包含一个 register_{$stub_file_name}_symbols() 函数。
给定如下文件:
// example.stub.php
<?php
/** @generate-class-entries */
/** @var string */
const ANIMAL = "Elephant";
/**
* @var float
* @cvalue M_PI
*/
const BAR = UNKNOWN;
function connect(#[\SensitiveParameter] string $connectionString): string {}
为注册这两个全局常量和该属性,将生成如下 C 函数(文件名对应 example.stub.php):
...
static void register_example_symbols(int module_number)
{
REGISTER_STRING_CONSTANT("ANIMAL", "Elephant", CONST_PERSISTENT);
REGISTER_DOUBLE_CONSTANT("BAR", M_PI, CONST_PERSISTENT);
zend_add_parameter_attribute(zend_hash_str_find_ptr(CG(function_table), "connect", sizeof("connect") - 1), 0, ZSTR_KNOWN(ZEND_STR_SENSITIVEPARAMETER), 0);
}
与类注册函数类似,生成的 register_{$stub_file_name}_symbols() 函数必须在 PHP_MINIT_FUNCTION 中调用,以让全局常量和属性生效:
PHP_MINIT_FUNCTION(example)
{
register_example_symbols(module_number);
return SUCCESS;
}
常量的类型标注规则
- 全局常量必须用
@varPHPDoc 标签指定类型; - 类常量的类型如果已有类型声明则自动推断,否则需要
@var标签; - 如果启用了
generate-legacy-arginfo(见后文),@var标签也是必需的。
UNKNOWN 与 @cvalue:当精确值未知时的写法
当常量的值由第三方库、PHP 内部实现或特定类型(如位掩码)定义时,使用 stub 的时刻还无法知道确切值。此时不要在 stub 中重复该值,而应使用 UNKNOWN 常量值并配合 @cvalue PHPDoc 标签。
以 BAR 为例:stub 中将其定义为 UNKNOWN,并通过 @cvalue M_PI 将其关联到 C 层常量 M_PI(由 PHP 内部实现定义)——最终生成 REGISTER_DOUBLE_CONSTANT("BAR", M_PI, CONST_PERSISTENT),即注册时使用 C 层的 M_PI 而非 stub 里的占位值。
常量支持的额外元信息标签:
@deprecated:使用该常量时触发弃用通知。自 PHP 8.5 起应改用#[Deprecated]属性;@genstubs-expose-comment-block:同前述,暴露 PHPDoc 块内容供ReflectionClass::getDocComment()使用,自 PHP 8.4 起可用。
维护向后兼容性:legacy arginfo 与预处理条件
PHP 源码发行版中的 stub 只需支持其所属分支。但第三方扩展往往需要支持更宽的 PHP 版本范围,不同版本支持的特性不同。stub 机制本身可能引入早期版本没有的新特性,且次版本之间可能发生 ABI 兼容性断裂;PHP 7.x 与 PHP 8 更是存在实质性差异。
可以通过向 gen_stub.php 指定最低支持版本来让它同时生成 legacy arginfo:
- 若扩展仍需支持 PHP 7,添加不带值的
@generate-legacy-arginfo文件级 PHPDoc 标签。此时会额外生成一个_legacy_arginfo.h文件,可以条件包含:
#if (PHP_VERSION_ID >= 80000)
# include "example_arginfo.h"
#else
# include "example_legacy_arginfo.h"
#endif
- 若
@generate-arginfo传入了最低支持的 PHP 版本 ID,则只生成一个 arginfo 文件,由文件内的#if预处理指令保证所有所需 PHP 8 版本的兼容性。
PHP 版本 ID 对照:80000 对应 PHP 8.0,80100 对应 PHP 8.1,80200 对应 PHP 8.2,80300 对应 PHP 8.3,80400 对应 PHP 8.4。
对前述例子稍作修改,添加 PHP 8.0 兼容要求:
<?php
/**
* @generate-class-entries
* @generate-legacy-arginfo 80000
*/
enum Number: string {
case One;
}
/**
* @strict-properties
* @not-serializable */
class Elephant {
/**
* @cvalue M_PI
* @var float
*/
public const float PI = UNKNOWN;
public readonly string $name;
}
生成的 arginfo 文件中出现了 #if (PHP_VERSION_ID >= ...) 条件:
...
#if (PHP_VERSION_ID >= 80100)
static zend_class_entry *register_class_Number(void)
{
zend_class_entry *class_entry = zend_register_internal_enum("Number", IS_STRING, class_Number_methods);
zend_enum_add_case_cstr(class_entry, "One", NULL);
return class_entry;
}
#endif
static zend_class_entry *register_class_Elephant(void)
{
zend_class_entry ce, *class_entry;
INIT_CLASS_ENTRY(ce, "Elephant", class_Elephant_methods);
class_entry = zend_register_internal_class_ex(&ce, NULL);
#if (PHP_VERSION_ID >= 80100)
class_entry->ce_flags |= ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE;
#elif (PHP_VERSION_ID >= 80000)
class_entry->ce_flags |= ZEND_ACC_NO_DYNAMIC_PROPERTIES;
#endif
zval const_PI_value;
ZVAL_DOUBLE(&const_PI_value, M_PI);
zend_string *const_PI_name = zend_string_init_interned("PI", sizeof("PI") - 1, 1);
#if (PHP_VERSION_ID >= 80300)
zend_declare_typed_class_constant(class_entry, const_PI_name, &const_PI_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_DOUBLE));
#else
zend_declare_class_constant_ex(class_entry, const_PI_name, &const_PI_value, ZEND_ACC_PUBLIC, NULL);
#endif
zend_string_release(const_PI_name);
zval property_name_default_value;
ZVAL_UNDEF(&property_name_default_value);
zend_string *property_name_name = zend_string_init("name", sizeof("name") - 1, 1);
#if (PHP_VERSION_ID >= 80100)
zend_declare_typed_property(class_entry, property_name_name, &property_name_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING));
#elif (PHP_VERSION_ID >= 80000)
zend_declare_typed_property(class_entry, property_name_name, &property_name_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING));
#endif
zend_string_release(property_name_name);
return class_entry;
}
这些预处理条件之所以必要,是因为枚举(enum)、readonly 属性和 not-serializable 标志都是 PHP 8.1 特性,在 PHP 8.0 中不存在。因此 Number 的注册被完全省略,而 Elephant::$name 在 8.1 之前的版本上不会添加 readonly 标志;此外带类型的类常量是 PHP 8.3 的新特性,8.3 之前的版本使用不同的注册函数(zend_declare_class_constant_ex 而非 zend_declare_typed_class_constant)。
为优化器生成函数信息:zend_func_infos.h
优化器在 Zend/Optimizer/zend_func_infos.h 中维护一份函数列表,其中包含关于**返回类型和返回值基数(cardinality)**的额外信息,从而启用更精确的优化(即更好的类型推断)。该文件此前靠人工维护,自 PHP 8.1 起 gen_stub.php 可通过 --generate-optimizer-info 选项接管——脚本源码中对应的处理逻辑正是把生成结果写回 Zend/Optimizer/zend_func_infos.h。
注意:该功能目前仅对 php-src 内置 stub 可用,因为向优化器提供函数列表目前只能通过直接覆盖 zend_func_infos.h 实现。
仓库中 Zend/Optimizer/zend_func_infos.h 的真实内容印证了这一点,文件头即为 /* This is a generated file, edit the .stub.php files instead. */,随后是 static const func_info_t func_infos[] 表,条目形如 F1("get_declared_classes", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_STRING)。
入选规则与 refcount 语义
当 @return 或 @refcount PHPDoc 标签提供的信息超出返回类型声明本身可提供的信息时,函数会被加入 zend_func_infos.h。默认规则:标量返回类型的 refcount 为 0,非标量值为 N;如果函数只可能返回新创建的非标量值,其 refcount 可以设为 1。
内置函数中的例子:
/**
* @return array<int, string>
* @refcount 1
*/
function get_declared_classes(): array {}
编译期求值:@compile-time-eval
当函数的参数在编译期已知、其行为无副作用且不受全局状态影响时,该函数可以在编译期被求值。优化器的这类函数列表在 PHP 8.2 之前靠人工维护;自 PHP 8.2 起,可以对符合上述限制的任意函数施加 @compile-time-eval PHPDoc 标签使其获得编译期求值资格。该特性内部通过添加 ZEND_ACC_COMPILE_TIME_EVAL 函数标志实现。
仓库中 ext/standard/basic_functions.stub.php 大量使用了这一标签(第 1572 行起可见多处 @compile-time-eval)。
无帧函数(Frameless Functions):@frameless-function
PHP 8.4 引入了基于参数个数(arity)的无帧函数。这是一种优化技术:当参数个数在编译期已知时,消除对实参数量的不必要检查,从而加快内部函数调用。
要利用无帧函数,需添加带配置的 @frameless-function PHPDoc 标签。由于目前仅支持基于 arity 的优化,标签形式为 @frameless-function {"arity": NUM},其中 NUM 是可用无帧函数的参数个数。
in_array() 的 stub 就是一个好例子(与 ext/standard/basic_functions.stub.php 中的实际写法一致,文档中还额外标注了 @compile-time-eval):
/**
* @compile-time-eval
* @frameless-function {"arity": 2}
* @frameless-function {"arity": 3}
*/
function in_array(mixed $needle, array $haystack, bool $strict = false): bool {}
它除了可编译期求值外,同时拥有 2 参数与 3 参数两种签名的无帧版本。对应的 C 实现形态:
/* The regular in_array() function */
PHP_FUNCTION(in_array)
{
php_search_array(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0);
}
/* The frameless version of the in_array() function when 2 arguments are passed */
ZEND_FRAMELESS_FUNCTION(in_array, 2)
{
zval *value, *array;
Z_FLF_PARAM_ZVAL(1, value);
Z_FLF_PARAM_ARRAY(2, array);
_php_search_array(return_value, value, array, false, 0);
flf_clean:;
}
/* The frameless version of the in_array() function when 3 arguments are passed */
ZEND_FRAMELESS_FUNCTION(in_array, 3)
{
zval *value, *array;
bool strict;
Z_FLF_PARAM_ZVAL(1, value);
Z_FLF_PARAM_ARRAY(2, array);
Z_FLF_PARAM_BOOL(3, strict);
_php_search_array(return_value, value, array, strict, 0);
flf_clean:;
}
可以注意到无帧函数体使用 Z_FLF_PARAM_ZVAL / Z_FLF_PARAM_ARRAY / Z_FLF_PARAM_BOOL 宏直接按位置取参,省去了 ZPP 宏的参数解析与数量检查开销;flf_clean: 标签则是其统一的清理跳转点。这类实现可进一步参阅 Zend/zend_frameless_function.h。
为手册生成签名(Method/Class Synopses)
手册应当反映与 stub 所表示完全一致的签名。对于内置符号目前尚未完全做到这一点,但 gen_stub.php 提供了多个自动化同步选项:
为新增函数/方法生成文档骨架:提供 --generate-methodsynopses 选项。运行
./build/gen_stub.php --generate-methodsynopses ./ext/mbstring ../doc-en/reference/mbstring
会为每个尚未被文档化的 ext/mbstring 函数创建专属页面,保存到 ../doc-en/reference/mbstring/functions 目录。由于这些是 stub 文档页面,许多章节是空的:需要补充相关描述,并删除无关章节。
更新已有条目的函数/方法签名:提供 --replace-methodsynopses 选项。运行
./build/gen_stub.php --replace-methodsynopses ./ ../doc-en/
将更新英文文档中所有能找到 stub 对应物的函数或方法签名。
更新类签名:提供 --replace-classsynopses 选项。运行
./build/gen_stub.php --replace-classsynopses ./ ../doc-en/
将更新英文文档中所有能找到 stub 对应物的类签名。
不打算文档化的符号:应添加 @undocumentable PHPDoc 标签,从而阻止为该符号创建任何文档。若想避免整个 stub 文件被加入手册,该标签应施加到文件本身。这对仅用于测试目的而存在的符号(例如为 ext/zend_test 声明的符号)或其他原因无法文档化的符号非常有用。
校验(Validation)
--verify 标志用于校验别名函数/方法的签名是否正确。别名应与被别名者除名称外具有完全相同的签名;有些情况下这做不到。例如 bzwrite() 是 fwrite() 的别名,但由于 resource 类型不同,第一个参数名不同。
当检查出现误报时,对别名施加 @no-verify PHPDoc 标签即可抑制错误:
/**
* @param resource $bz
* @implementation-alias fwrite
* @no-verify Uses different parameter name
*/
function bzwrite($bz, string $data, ?int $length = null): int|false {}
这一示例与 ext/bz2/bz2.stub.php 中 bzwrite() 的真实声明几乎一致(仓库中的实际写法还额外携带 @implementation-alias fwrite)。--verify 的比对逻辑在脚本内部实现:它逐参数比较别名与被别名者的参数名、类型、默认值以及返回类型与 PHPDoc 返回类型,任何差异都会产生错误并使脚本以非零码退出。
除别名外,向 gen_stub.php 提供 --verify-manual 选项还可以校验手册文档内容。该标志需要 stub 源目录与目标手册目录两个参数,如:
./build/gen_stub.php --verify-manual ./ ../doc-en/
此校验要求指定路径下同时具备全部 php-src stub 和完整的英文文档。它执行以下校验:
- 检测缺失的全局常量;
- 检测缺失的类;
- 检测缺失的方法;
- 检测错误文档化的别名函数或方法。
用本文档中的 stub 示例运行时会输出类似如下警告(脚本对未文档化的常量、类、方法分别打印 Missing predefined constant / class synopsis / method synopsis 警告):
Warning: Missing class synopsis for Number
Warning: Missing class synopsis for Elephant
Warning: Missing class synopsis for Atmosphere
Warning: Missing method synopsis for fahrenheitToCelsius()
Warning: Missing method synopsis for Atmosphere::calculateBar()
参数统计:--parameter-stats
gen_stub.php 的 --parameter-stats 标志会统计参数名在整个代码库中出现的次数。输出是一个 JSON 对象,包含参数名及其出现次数,按次数降序排列(对应脚本中 arsort() 后 json_encode(..., JSON_PRETTY_PRINT) 的输出)。这一统计常被用于评估命名约定的统一性——哪些参数名(如 $data、$value、$flags)在整个 PHP 内部函数集中最常用,可作为新函数命名参数的参考依据。
小结:stub 生态在 php-src 中的完整闭环
综合官方文档与仓库实证,stubs 机制形成了清晰的分工闭环:
- 声明层:
*.stub.php(如 Zend/zend_constants.stub.php、ext/date/php_date.stub.php)用纯 PHP 声明符号、类型与 PHPDoc 元信息; - 生成层:build/gen_stub.php 基于 PHP-Parser 解析 stub,依据
@generate-function-entries、@generate-class-entries、@generate-legacy-arginfo等文件级标签以及命令行选项,产出 arginfo 头文件(带 stub hash 增量控制)、ext_functions/class_*_methods表、register_class_*()/register_*_symbols()注册函数、Zend/Optimizer/zend_func_infos.h 优化器信息,以及手册签名内容; - 校验层:debug 构建在运行时将 arginfo 与
ZEND_PARSE_PARAMETERS_*宏交叉比对;--verify校验别名签名一致性;--verify-manual核对手册覆盖度; - 消费层:release 构建中 arginfo 服务于 Reflection,优化器消费
zend_func_infos.h,手册消费 synopses 产物。
掌握这套机制后,阅读 php-src 时看到任何 *_arginfo.h 文件头部的 Stub hash 注释,即可反查到其权威来源——同名的 .stub.php 文件,并据此理解该扩展全部符号的注册路径与版本兼容策略。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00