TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

12 KiB

C++ 命名空间、前缀与符号 ABI 规则

本文是 TypePHP、PHPX 以及 TypePHP 生成代码的内部 C++ 命名规范。它解决以下问题:

  • 区分 TypePHP 运行时逻辑、PHPX ZendAPI 封装、项目私有实现和用户 PHP 符号;
  • 防止框架 helper 与用户定义的 PHP 函数或类方法生成相同的 C++ 符号;
  • 明确哪些名称属于稳定 ABI,哪些名称仅限单个生成项目内部使用;
  • 为新增 helper、缓存、入口函数和生成符号提供统一的命名决策。

1. 总体规则

命名域 含义 典型形式 可见范围 ABI 属性
typephp_ TypePHP 独有的运行时或编译产物支持逻辑 typephp_call_parent_constructor() TypePHP/PHPX 运行时 内部或显式导出 ABI
php:: 对 ZendAPI、zval、HashTable、call frame 等 PHP 运行时能力的 C++ 封装 php::deindirect() PHPX C++ API PHPX API
typephp_<project> 单个编译项目的私有 C++ 命名空间 namespace typephp_tpc 当前生成项目 非公共 ABI
php_ 用户 PHP 函数和类方法映射后的 C++ callable 符号 php_app__user__save() 链接器可见 TypePHP/stub callable ABI

核心约束:

  1. 不得新增全局的框架 php_* helper。
  2. 与 TypePHP 无关、只是包装 ZendAPI 的能力必须放入 namespace php
  3. TypePHP 独有且需要跨生成文件调用的逻辑使用 typephp_ 前缀。
  4. 只服务于一个编译项目的数据和函数放入 typephp_<project> 命名空间。
  5. 全局 php_* callable 名称保留给用户 PHP 声明的编译 ABI。

2. typephp_:TypePHP 独有逻辑

typephp_ 表示该 API 的语义由 TypePHP 定义,不是 ZendAPI 的一般性 C++ 包装。常见场景包括:

  • TypePHP 属性读写规则;
  • TypePHP 构造、克隆和父方法调用链;
  • TypePHP 编译期 Attribute 的运行时支持;
  • TypePHP Native Class、Property Hook 等专属运行时逻辑;
  • TypePHP embed runtime 的初始化和关闭入口。

示例:

typephp_call_parent_constructor(object, constructor, args);
typephp_call_parent_clone(object, clone_method);
typephp_install_property_handlers(class_entry, handlers);
typephp_write_property_scoped(object, member, value, scope);
typephp_runtime_init(argc, argv);

2.1 使用边界

  • 该前缀是 TypePHP 内部 C/C++ 名称空间,不代表 PHP 用户函数。
  • 新增 API 时应使用完整、可识别的 snake_case 名称,不能使用含义过宽的名称,例如 typephp_call()
  • 仅在一个 .cc 文件中使用的函数还应增加 static 或放入匿名命名空间。
  • 需要跨动态库边界时,使用对应的导出宏;不需要导出的 helper 不应扩大符号可见性。
  • 不要仅因为代码位于 typephp_helper.h 就使用 typephp_;判断依据是语义是否为 TypePHP 独有。

2.2 正反例

// 正确:TypePHP 独有的构造链语义。
typephp_call_parent_constructor(object, constructor, args);

// 错误:只是将 INDIRECT zval 物化为普通值,并非 TypePHP 独有。
typephp_deindirect(value);

// 正确:通用 Zend 值包装属于 PHPX。
php::deindirect(value);

3. php:::ZendAPI 的 C++ 封装

namespace php 由 PHPX 提供,用于把 Zend 的 C API、宏、裸指针和手工资源管理封装为类型安全、RAII 友好的 C++ API。

这一命名域包含两类能力:

  1. PHP 值和运行时对象,如 php::Varphp::Strphp::Arrayphp::Object
  2. ZendAPI 的安全包装,如符号查询、作用域管理、值转换、对象创建和调用。

示例:

php::Var value;
php::Array arguments;

auto plain = php::deindirect(value);
auto called_ce = php::getCalledCe(this_);
auto scope = php::getCallableScope(function, this_);
auto create_object = php::getCreateObjectFn(class_entry);
auto globals = php::globalsArray();

3.1 何时使用 php::

满足以下条件时应放入 namespace php

  • API 对任何 PHPX C++ 调用者都有意义;
  • API 的行为可以完全用 Zend/PHP 运行时语义解释;
  • API 不依赖 TypePHP AST、编译期 Attribute 或 TypePHP 特有语言规则;
  • API 的主要作用是隐藏 Zend 宏、裸 zval *、引用计数或异常检查。

3.2 禁止全局 php_* helper

以下旧式写法是禁止的:

php::Var php_deindirect(const php::Var &value);
php::Str php_get_called_class(php::Object &this_);
zend_class_entry *php_get_called_ce(php::Object &this_);
auto php_get_create_object_fn(zend_class_entry *ce);

它们必须写成:

namespace php {

Var deindirect(const Var &value);
Str getCalledClass(Object &this_);
zend_class_entry *getCalledCe(Object &this_);
auto getCreateObjectFn(zend_class_entry *ce);

}  // namespace php

原因是用户可以合法声明:

function deindirect(mixed $value): mixed {}
function get_called_ce(): string {}
function get_create_object_fn(): string {}

这些 PHP 函数会生成 php_deindirectphp_get_called_cephp_get_create_object_fn。如果 PHPX 也在全局定义同名 helper,可能在声明、重载解析或链接阶段发生冲突。

3.3 命名风格

PHPX C++ API 使用现有的 camelCase 风格:

php::getCalledClass();
php::getClassEntrySafe();
php::getPersistentCache();
php::stdCreateObject();

不要把 Zend 的 snake_case 名称机械地保留为全局 C++ 名称。底层调用可以继续使用 Zend 原始 API,例如 zend_objects_new(),但对生成代码暴露的包装层应使用 php::

4. typephp_<project>:项目私有命名空间

每个 TypePHP 编译项目拥有独立的 C++ 命名空间:

typephp_<target-name>

例如项目名为 tpc

namespace typephp_tpc {
    // Project-private generated state and helpers.
}

项目名中的 -* 会转换为 _,其余字符必须满足编译器的 target identifier 校验。由于固定带有 typephp_ 前缀,即使项目名以数字开头,最终 C++ namespace 仍是合法标识符。

4.1 应放入该命名空间的内容

  • literal string 表和 get_str()
  • class/function/property cache 表及其访问函数;
  • 当前项目的全局变量存储;
  • class entry、object handler 和默认属性模板;
  • module entry、MINIT/RINIT/RSHUTDOWN 辅助状态;
  • module_init()module_clean() 等仅在生成 extension 文件内部调用的函数;
  • Python module cache 等项目级生成状态。

示意:

namespace typephp_demo {

static php::Str literal_strings[] = {
    php::Str{"hello"},
};

php::Str &get_str(uint32_t index) {
    return literal_strings[index];
}

static THREAD_LOCAL zend_class_entry *class_map[8];

zend_class_entry *get_class(int id, const php::Str &name) {
    // Resolve and cache a symbol owned by this project.
}

static void module_init() {
    // Initialize this project's generated state.
}

}  // namespace typephp_demo

4.2 可见性与 ABI

  • typephp_<project> 内的名称是实现细节,不是 library stub ABI。
  • 可限制为 static 的对象和函数应继续标记为 static
  • 生成头文件可以声明必须跨 translation unit 使用的项目内部 accessor,但不应暴露底层数组或缓存表。
  • 外部手写 C++ 代码不得依赖 literal index、cache index 或项目内部 storage 名称。
  • 不同 TypePHP 项目可以链接到同一进程,因为相同的内部短名称位于不同的项目 namespace 中。

4.3 作用域优先于名称拼写

项目 namespace 中仍可能出现历史生成名称,例如:

typephp_demo::php_class_entry_App_User

虽然成员名以 php_ 开头,但完整符号位于 typephp_demo 中,因此它属于项目私有实现,而不是第 5 节所述的全局用户 callable ABI。新增项目内部 helper 应优先使用不带 php_ 的短名称,例如 get_class()get_func()get_str()

5. php_:用户 PHP callable 的 C++ ABI

全局 php_ 前缀用于 TypePHP 将用户声明的 PHP 函数和类方法映射为 C++ callable 符号。这套命名同时被生成代码、library stub 和外部 C++ 实现使用,因此不能随意改变。

示例:

namespace App;

function greet(string $name): string {}

class User
{
    public function save(): bool {}
}

概念上的 C++ 符号为:

php::Str php_app__greet(php::Str name);
php::Bool php_app__user__save(php::Object &this_);

规则包括:

  • 使用 php_ 标识“由 PHP 声明映射而来”;
  • PHP namespace、class 和 method/function 名经过规范化后组合;
  • __ 是现有 ABI 的组合分隔符;
  • 实例方法的第一个参数是对象 this_
  • stub、library 和消费方必须使用完全相同的映射规则。

5.1 为什么内部 helper 不能使用 php_

php_ 映射不是独立的保留关键字空间,而是用户 PHP 名称的机械 ABI。以下用户声明:

function deindirect(mixed $value): mixed {}

会自然生成:

php::Var php_deindirect(php::Var value);

因此框架若定义全局 php_deindirect(),就侵占了用户符号空间。正确做法是 php::deindirect()

5.2 组合冲突

由于当前 ABI 使用 __ 组合 PHP namespace、class 和 callable 名,下列两个 PHP 声明可能映射到同一个 C++ 符号:

function App\user__test(): void {}

namespace App;
class User
{
    public function test(): void {}
}

编译器必须在预处理阶段检测这种情况并抛出 FatalError,不能通过覆盖、链接顺序或增加运行时分派来处理。修改映射分隔规则会破坏既有 stub/ABI,因此冲突必须由用户重命名解决。

5.3 入口符号例外

少量 C ABI/嵌入入口由生成器固定定义,不属于普通用户 callable。例如:

php_<project>_embed_get_module();

它是 binary/library embed runtime 与当前项目 module entry 的连接点。该名称包含项目名并由构建器和 typephp_main.cc 成对生成,不得作为通用 helper 命名模板。

6. 名称选择流程

新增 C++ API 时按以下顺序判断:

  1. 它是否是用户 PHP 函数或类方法的编译本体?
    • 是:使用既有 php_ callable ABI 生成器,禁止手写另一套映射。
  2. 它是否只服务于当前一个 TypePHP 项目?
    • 是:放入 typephp_<project>,并尽可能使用 static 或私有 accessor。
  3. 它是否实现 TypePHP 独有语义?
    • 是:使用 typephp_ 前缀。
  4. 它是否只是对 Zend/PHP 运行时能力的 C++ 封装?
    • 是:放入 namespace php,使用 PHPX camelCase 风格。
  5. 以上都不是?
    • 不应随意加入 typephp_helper.h;应重新确认所属模块和公共 API 边界。

7. 代码审查清单

新增或修改生成 helper 时必须检查:

  • typephp_helper.h 中没有新增全局 php_* helper;
  • ZendAPI 包装位于 namespace php
  • TypePHP 独有逻辑使用 typephp_
  • 项目缓存和 storage 位于 typephp_<project>
  • 项目私有表没有通过生成头文件直接 extern 暴露;
  • 用户 callable 仍使用统一的 php_ ABI 生成器;
  • 新名称不会与用户可声明的 PHP 函数或方法发生冲突;
  • bin、lib、ext 和 WASM 构建使用相同的项目名推导规则;
  • 修改公开 callable 映射时同步评估 stub 和既有 ABI;
  • 至少增加一个用户同名函数的编译回归测试。

当前相关回归测试为:

tests/compiler/basic/helper-symbol-collision.phpt

8. 主要实现位置

责任 文件
php_ callable 前缀与组合分隔符 src/CompilerBase.php
callable 组合冲突检测 src/Preprocessor.php
typephp_<project> 生成及项目私有表 src/Translator.php
TypePHP extension 前缀常量 src/Metadata/Constants.php
PHPX/TypePHP helper 分类 vendor/swoole/phpx/src/misc/typephp_helper.h
embed module accessor 拼接 vendor/swoole/phpx/src/misc/typephp_main.cc