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.
 
 

6.1 KiB

Interface Property Hooks 实现方案

本文记录 TP-AOT-010 的设计与实施计划。目标是支持 PHP 8.4 的 Interface Property Hook 契约,同时保持 TypePHP Native 调用的零成本抽象,并让 PHP 8.4 ZendVM 的 Reflection、动态类链接和继承检查获得完整元数据。

当前状态(2026-08-14)

第一阶段已经落地:Interface 契约模型、AOT 实现检查、get/set 方向方差、PHPX 抽象 Hook 元数据、Reflection、动态 PHP 实现类及回归测试均已接通。显式 setter 参数类型仍按下文约定在编译期拒绝;完成独立写入类型模型后再开放。

1. 设计结论

Interface 中的 Hooked Property 只表示属性契约:

interface Named
{
    public string $name { get; set; }
}
  • Interface 不持有属性槽,不生成 getter/setter 实现,也不产生访问时的契约检查。
  • TypePHP 在编译期验证已知 AOT 类是否满足属性的可见性、类型和 get/set 能力。
  • PHP 8.4 目标在 MINIT 注册原生 Zend Hook 元数据,使 Reflection 和动态 PHP 类获得相同契约。
  • 编译器前端解析和验证该语法不依赖 PHP 8.4;但使用 Property Hooks 的最终目标运行时必须链接 PHP 8.4 或更高版本。
  • PHP 8.3 不提供静默降级。缺少 Zend Hook 元数据会使动态属性访问、Reflection、JSON、序列化和 eval() 类链接的行为取决于执行路径,不能视为可靠支持。

2. 语法与诊断

支持三类契约:

public string $readable { get; }
public string $writable { set; }
public string $readWrite { get; set; }

Interface Property Hook 必须是 public、非 static、无默认值且 Hook 不得包含函数体。普通 Interface Property、private/protectedreadonly、重复或未知 Hook,以及带实现体的 Hook 均在 TypePHP 编译期抛出 FatalError。错误信息应尽可能与 PHP 8.4 一致。

第一阶段只接收隐式 setter 参数:

public string $name { set; }

PHP 8.4 还允许 set(string|Stringable $value) 这类显式、可逆变的 setter 参数。该语法需要让编译期契约模型与 Zend Hook arg_info 同时保存独立于属性读取类型的写入类型;在这部分完成前,TypePHP 会给出明确的编译期错误,不生成可能错误的运行时元数据。

3. 编译器模型

Interface Property Hook 不应伪装成普通属性或 lowering 后的普通方法。为其建立独立契约模型,至少保存:

  • 属性名和声明节点;
  • 解析后的 TypePHP 类型与类类型;
  • 是否要求 get
  • 是否要求 set
  • 可见性及其他用于诊断的标志。

契约存放在 InterfaceDef 中。AST/预处理阶段只收集和验证声明,不为 Interface 分配属性槽,不运行具体类使用的 PropertyHookLowering,也不生成隐藏方法。

所有类型完成预处理后再执行契约链接:展开父 Interface 契约,然后检查实现类自身或父类提供的属性。普通 public backed property 同时满足读写契约;Hooked Property 根据实际 Hook 能力判断。get-only 类型按读取方向协变,set-only 类型按写入方向逆变,同时包含 get/set 时保持不变。

4. PHPX 与 Zend 元数据

现有 php::registerPropertyHooks() 用于具有真实 AOT getter/setter 的具体类,不能复用于抽象 Interface Hook。

PHPX 增加独立 helper:

php::registerAbstractPropertyHooks(
    zend_class_entry *interface_ce,
    zend_property_info *property_info,
    bool readable,
    bool writable
);

它只在 PHP_VERSION_ID >= 80400 下访问 PHP 8.4 ABI,并负责:

  • 持久化分配 zend_property_info::hooks
  • 创建没有 handler 的 abstract get/set zend_internal_function 元数据;
  • 设置 ZEND_ACC_PUBLIC | ZEND_ACC_ABSTRACT、正确的参数/返回类型及 common.prop_info
  • 更新 num_hooked_props,使 Zend inheritance 和 Reflection 识别该契约;
  • 保证所有字符串、Hook 表和函数描述具有 MINIT 级持久生命周期。

生成代码先注册 Interface,再以 IS_UNDEFZEND_ACC_PUBLIC | ZEND_ACC_ABSTRACT | ZEND_ACC_VIRTUAL 声明属性并挂载抽象 Hook,最后才注册和链接实现类。

5. PHP 版本边界

TypePHP 应区分编译器宿主与目标 PHP:

  • PHP Parser 和 TypePHP 前端可以在 PHP 8.3 环境解析该语法;
  • 构建后端以项目选择的 PHP language/target version 以及最终链接的 PHP headers/libphp 作为能力依据;编译器进程自身可以运行在更旧的 PHP 上;
  • 发现 Property Hooks 且目标低于 PHP 8.4 时,在 C++ 编译前报告:
Property Hooks require PHP 8.4 or later as the target runtime

PHPX 仍使用条件编译作为 ABI 防线,但不应把清晰的功能诊断推迟为 C++ 编译错误。

6. TDD 覆盖

实现前先加入失败测试,覆盖:

  1. get-only、set-only、get/set Interface 契约;
  2. 普通 backed property、Hooked Property 和继承属性满足契约;
  3. 缺失属性、缺少 get/set、非 public 和类型不兼容的编译错误;
  4. Interface 继承、多个契约的合并与冲突;
  5. Reflection 的 abstract、virtual、hasHook/getHook 元数据;
  6. PHP 8.4 动态 PHP 类的成功与失败链接;
  7. O0/O3 结果一致,Interface 不生成属性槽或 Native Hook 实现;
  8. PHP 8.3 目标得到明确的构建期错误;
  9. PHPX helper 在 NTS/ZTS 和 PHP 8.4/8.5 下的生命周期与 ABI 回归。

7. 实施顺序

  1. 添加 TP-AOT-010 正常场景及语法错误 PHPT,确认当前失败。
  2. 增加 Interface Property Contract 模型和预处理收集逻辑。
  3. 实现 Interface 继承与实现类的编译期契约检查。
  4. 在 PHPX 增加抽象 Hook 元数据 helper。
  5. 修改 stub 生成和类注册顺序,接入 PHP 8.4 Zend 元数据。
  6. 添加 Reflection、动态类链接、目标版本和生成代码测试。
  7. 执行 Interface、Property Hook、Reflection 及全量编译器回归。

完成后的运行时属性访问仍直接进入实现类的普通属性或 Native Hook;Interface 契约本身只存在于编译期模型和 MINIT 元数据中,不进入请求热路径。