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.
 
 

1387 lines
71 KiB

# Native Class Object 设计与实现
> 状态:第一阶段实现中。固定布局、Native Call、精确 tracing GC、
> 构造/克隆/析构、Trait、Getter/Setter、Property Hook、抽象类、单继承、有限虚分派和
> Interface 编译期契约已经落地;本文同时记录尚未开放的边界。
## 1. 背景
TypePHP 的普通 class 会注册到 ZendVM,并生成 `zend_class_entry`、对象处理器、属性元数据和 Zend 方法包装函数。这使普通 class 能够兼容动态 PHP、Reflection、动态调用、序列化等能力,但也带来了固定的运行时成本。
Native Class Object 面向少量要求极致性能的场景。它只允许在静态编译的 TypePHP 代码中使用,不注册到 ZendVM,并直接生成为接近 C++ `struct` 的数据结构。
该能力不是普通 class 的自动优化模式,也不替代现有对象模型。开发者必须显式选择 Native Class,并接受相应的功能限制。
## 2. 设计目标
1. 属性具有固定内存布局,可以直接访问 C++ 字段。
2. 方法继续编译为现有 `php_*` C++ 自由函数,第一个参数是具体 Native struct 的 `this_`
3. 不生成 `zend_class_entry`、Zend object handlers 和 Zend 方法包装函数。
4. 不执行 `zend_call_function()`,不经过 ZendVM 动态分派。
5. 对象句柄只占一个机器字,不使用 `std::shared_ptr` 和原子引用计数。
6. 普通参数传递不增加引用计数,不复制对象实体。
7. Getter、Setter、Property Hook 等静态特性可以被 C++ 编译器内联。
8. Native Class 编译器实现放在独立目录中,避免将特殊规则散布到现有编译器代码。
9. 不为了兼容少量依赖 ZendVM 的特性牺牲主要路径的性能和可维护性。
10. 所有属性必须显式声明类型,禁止依靠首次赋值推断属性布局。
11.`bool`、`int`、`float` 外,允许属性保存 string、array、object、Stream、mixed 等合法且受支持的 PHP/TypePHP 类型。
## 3. 非目标
Native Class Object 初版不追求以下能力:
- 与动态 PHP 代码互操作。
-`eval()`、动态 `include` 中访问。
- Zend Reflection 元数据。
- 动态属性、变量方法名和动态类名实例化。
- 与普通 ZendVM class 互相继承。
- 自动装箱为 `php::Object``php::Variant`
- 完整兼容 PHP 对象的析构时机和垃圾回收行为。
- 在无法静态证明安全时自动降级为 ZendVM Object。
## 4. 显式声明
建议使用专用注解,暂定为:
```php
#[Native]
class Point
{
public float $x;
public float $y;
public function length(): float
{
return sqrt($this->x ** 2 + $this->y ** 2);
}
}
```
Native Class 支持单继承,但只能继承另一个 Native Class。普通 ZendVM class 与 Native Class 之间禁止互相继承。`final class` 和 `final` 方法继续生效,并为编译器提供更强的去虚化条件。
继承链中的每一个 concrete class 都必须显式声明 `#[Native]`,不能仅因父类是 Native Class 就隐式切换对象模型:
```php
#[Native]
class Base {}
#[Native]
class Child extends Base {}
```
`#[Native]` 是 Native Class Object 的正式显式声明方式。未使用该注解的普通 class 继续进入现有 ZendVM Object 编译流程。
该注解只能用于具名 `class`;Interface、Trait 与 Enum 均不能声明为 Native。Trait 仍可由
Native Class 在 convert 阶段注入,但 Trait 自身不形成 Native runtime 类型。
## 5. 生成的 C++ 结构
上述代码近似生成:
```cpp
struct php_app__point {
php::Float x;
php::Float y;
};
php::Float php_app__point__length(php_app__point &this_);
```
没有父类、子类和 override method 的 Native Class 不包含:
- 虚函数表。
- Native Object 基类。
- 运行时 class id。
- 对象内引用计数。
- Zend object header。
- 属性名称表和方法名称表。
不参与 override 分派时,Native struct 只包含字段,不生成 C++ 成员函数。PHP 源码中的实例方法继续沿用 TypePHP 当前的函数式 ABI:
```cpp
php::Float php_app__point__length(php_app__point &this_) {
return php::fn::sqrt(this_.x * this_.x + this_.y * this_.y);
}
```
调用生成:
```cpp
php_app__point__length(*point);
```
调用方在解引用前完成一次 null 检查,`php_*` 方法函数内部可以假设 `this_` 有效。普通实例方法统一使用可写的 `native_struct &this_`,不因方法体是否修改属性而产生不同 ABI。
这个规则带来以下收益:
- 完全复用当前 `php_*` 方法符号命名与 Native Call 机制。
- 参数求值顺序、默认参数、类型检查和异常边界继续走现有函数生成逻辑。
- 不参与继承分派的 Native struct 不包含 vtable 或成员函数声明。
- 所有 Native struct 可以先完成前置声明和字段定义,再统一生成方法函数。
- Getter、Setter、Property Hook 和魔术方法可以统一 lowering 为同类自由函数。
Native 方法不生成 Zend method wrapper,也不注册到 ZendVM。普通 PHP class 与 Native Class 的 `php_*` 符号仍必须进入现有编译符号冲突检测。
### 5.1 继承与虚分派
Native Class 使用 C++ public single inheritance 保持基类子对象布局。PHP 方法的实现主体仍然是 `php_*` 自由函数,不改为复杂的 C++ 成员函数模型。
Native abstract class 及 abstract method 也完全在编译期实现。抽象方法在 C++ struct 中
生成 pure virtual thunk,具体 Native 子类的实现继续调用对应 `php_*` 自由函数;通过抽象
基类 typed parameter 调用时只发生一次 C++ 虚分派,不注册 Zend class 或 method。
全程序分析发现继承链中存在同名的 public/protected instance method 时,为每一个声明层级
生成独立的内部 virtual slot。子类实现同时覆盖祖先 slot,并以各 slot 自己的参数和返回
签名生成 adapter,再转调子类的 `php_*` 实现。这样可以保留 PHP 允许的参数逆变、返回值
协变,而不会把不兼容的 C++ 函数指针或引用强制转换到同一个 vtable slot:
```cpp
struct php_app__base;
php::Str php_app__base__name(php_app__base &this_);
struct php_app__base {
virtual php::Str __native_dispatch_base_name() {
return php_app__base__name(*this);
}
~php_app__base() noexcept = default;
};
struct php_app__child;
php::Str php_app__child__name(php_app__child &this_);
struct php_app__child : public php_app__base {
php::Str __native_dispatch_base_name() override {
return php_app__child__name(*this);
}
};
```
调用规则:
- receiver 的静态类型确定为最终实现类时,直接调用对应 `php_*` 函数。
- receiver 是可能指向子类的基类指针,且方法族存在 override 时,通过内部 virtual thunk 分派。
- 未被 override 的方法继续直接调用 `php_*` 函数,不引入虚调用。
- private、static、constructor 和 destructor 不加入 virtual method family。
- PHP 不支持按参数签名重载;Native Class 同样不增加 C++ overload 语义。
- override 必须通过现有 PHP 方法兼容性规则和 Interface 检查。
- 普通值参数通过 per-declaration adapter 支持 PHP 的参数逆变与返回协变。Native Object
参数禁止 `&`;typed pointer 按值传递已经共享对象身份,而 `&` 还会暴露调用方指针槽的
重绑定能力,这不属于 Native Object ABI。
- C++ 默认参数由 receiver 的静态类型绑定,不能直接放在 virtual 声明上。编译器为每个
可用的位置参数数量生成一个重载 virtual slot;动态选中的 adapter 再调用自身 `php_*`
实现,因此使用动态实现类的默认值,不需要运行时 presence mask。
- Native virtual method 的命名参数调用可以省略尾部连续的 optional 参数;若在后续实参
之前留下命名参数空洞则编译期拒绝。该少见形状无法由位置型 C++ 重载表达,支持它会使
每次虚调用携带 presence mask。非虚 Native Call 仍保留普通命名参数行为。
这会提供继承所必需的有限单分派多态,但不支持变量方法名、运行时 overload resolution、`__call()` 或 ZendVM 动态调用。C++ 编译器仍可对 `final` class、`final` method 和已知精确类型完成去虚化。
GC header 的 `NativeTypeDescriptor` 始终记录最派生的动态类型。即使对象通过基类指针存活,trace、finalize 和 destroy 也必须使用动态 descriptor,不能仅依据变量的静态类型。
### 5.2 继承对象布局
继承层次必须满足以下布局规则:
- 父类字段位于 C++ base subobject 中,子类只追加自身新增字段。
- public/protected 同名属性必须通过现有 PHP 属性兼容性检查;表示同一个继承属性时复用父类 slot,不得在子类重复存储。
- 父类 private 属性与子类同名属性是两个不同 slot。生成字段名必须包含 declaring class 或稳定 slot id,避免 C++ 名称隐藏造成误访问。
- 访问继承属性时,代码生成器依据 property definition 的 declaring class 计算固定字段路径,不进行名称查找。
- 最派生类型的 `trace()` 必须覆盖自身和所有 base subobject 中的 Native pointer field。
- constructor 不参与虚分派。与 PHP 一样,子类是否调用 `parent::__construct()` 由源码显式决定,并直接生成确定的 `php_*` 调用。
- `final` class 禁止被继承,`final` method 禁止被 override。
继承图必须在生成 struct 前完成拓扑排序。Native Class 只支持单一 class parent;多个 Interface 不参与对象布局。
## 6. 属性类型与存储
Native Class 的每一个实体属性都必须具有显式类型:
```php
#[Native]
final class RequestContext
{
public bool $ready = false;
public int $status = 0;
public float $elapsed = 0.0;
public string $method = '';
public array $headers = [];
public object $request;
public Stream $body;
public mixed $metadata = null;
public function __construct(object $request, Stream $body)
{
$this->request = $request;
$this->body = $body;
}
}
```
禁止未声明类型的属性:
```php
#[Native]
final class InvalidContext
{
public $value; // FatalError
}
```
类型声明用于在编译期确定 C++ 字段布局。不同类型建议映射如下:
| TypePHP 属性类型 | C++ 字段表示 | 说明 |
|---|---|---|
| `bool` | `php::Bool` | 原生值字段 |
| `int` | `php::Int` | 原生值字段 |
| `float` | `php::Float` | 原生值字段 |
| `string` | `php::Str` | TypePHP 当前使用的 PHPX RAII 字符串类型 |
| `array` | `php::Array` | PHPX RAII 数组,保留 PHP COW 语义 |
| 确定的 Zend class | `php::Object` | 保存 Zend Object,并在赋值入口验证 class |
| `object` | `php::Object` | 保存任意 Zend Object |
| Native Class | `native_struct *` | 保存同一 Native Heap 内的裸指针 |
| `Stream` | `php::Var` | 保存 stream resource zval,并在赋值入口执行精确类型检查 |
| `mixed` / `any` | `php::Var` | 保存任意 PHP zval;两种声明具有相同的无约束槽语义 |
| 不含 Native Class 的 union/intersection/nullable | `php::Var` | 与普通类属性使用同一类型描述和运行时写入检查 |
| `?NativeClass` | `native_struct *` | `nullptr` 表示空值;包含 Native Class 的 union/intersection 不支持 |
| BigInt/BigFloat/Decimal | `php::Var` | 保存 PHPX boxed 高精度值;字段寻址仍是固定偏移,运算复用现有 Variant ABI |
`string`、`array`、Zend Object、Stream 和 mixed 字段仍然直接位于 C++ `struct` 的固定偏移处。它们持有的底层 zval 或 zend 对象由 PHPX RAII 类型管理,但属性读取不需要属性哈希表、object handler 或 ZendVM 分派。
PHP 本身不允许将 `resource` 写成属性类型;TypePHP 中的 stream resource 应使用已有的 `Stream` 伪类型声明。`void`、`never`、`callable` 等 PHP 本身禁止用于属性声明的类型,在 Native Class 中同样禁止。
以下类型明确禁止作为 Native Class 属性类型:
- `Box`
- `std\array`
- `std\vector`
- `std\map`
- `std\ordered_map`
- 后续增加的其他 Std Container 类型
这些类型具有独立的泛型布局、引用或所有权语义,将它们嵌入 Native Class 会显著扩大首版类型组合和生命周期分析范围。开发者可以使用普通 PHP `array` 字段;PHP array 中仍然不能保存 Native Object,因为 Native Object 没有 `zval` 表示。
例如:
```cpp
struct php_app__requestcontext final {
php::Bool ready;
php::Int status;
php::Float elapsed;
php::Str method;
php::Array headers;
php::Object request;
php::Var body;
php::Var metadata;
};
```
允许字段持有 ZendVM 值不代表 Native Class Object 本身进入 ZendVM。ZendVM 可以管理字段指向的 String、Array、Object 或 resource,但它不知道外层 Native Class 的存在。
### 6.1 属性引用
Native 属性是否允许取引用必须完全由声明元数据在编译期决定,不生成运行时类型分支:
- `mixed` / `any` 是无约束的 `php::Var` 槽,允许 `$ref =& $object->property`
- `bool`、`int`、`float` 等固定布局字段不能表示 PHP 引用,编译期拒绝。
- `string`、`array`、`object`、Stream 和高精度类型虽然具有 PHPX 包装层,但仍是固定声明类型,引用写入会绕过类型约束,因此编译期拒绝。
- nullable、union、intersection 等受约束的 `php::Var` 字段同样拒绝引用;不能仅因底层存储也是 `php::Var` 就允许。
- 带 Property Hook 的属性没有可暴露的实体槽,始终拒绝引用。
这项规则只允许引用无约束字段值,不允许引用 Native Object 指针变量本身。Native Object 变量之间的普通赋值已经共享对象身份。
### 6.2 初始化状态
Native Class 不保存 PHP typed property 的 `UNDEF` 状态,也不为字段增加额外状态位。对象创建时,每个没有显式默认值的字段直接使用类型零值:
- `bool``false`;`int`、`float` 为 `0`
- `string` 为空字符串,`array` 为空数组。
- `mixed`/`php::Var` 为 `null`
- Zend Object 和 Native Class 指针均为 `null`
- `Stream` 为空 resource 状态。
- 属性具有显式默认值时,在零值构造之后应用声明默认值。
- 初版禁止对 Native Class 属性使用 `unset()`,避免重新引入运行时 UNDEF 状态。
因此没有默认值的属性也可以立即读取,但读取到的是上述确定零值,而不是 PHP 的“未初始化 typed property”异常。所有属性仍必须声明类型。
Property Hook 的虚拟属性没有实体字段,但 Hook 声明仍必须包含类型。
### 6.3 赋值检查
已确定的赋值在编译期检查。来自 `mixed`、动态 PHP 返回值或其他无法静态确定的值,在写入字段前执行一次运行时类型检查。检查完成后直接写入对应字段,不经过 Zend property handler。
这意味着支持任意 PHP 字段类型不会改变 Native Class 的属性寻址性能;额外成本只出现在无法静态证明类型安全的赋值边界。
## 7. 对象变量与身份语义
为了保留 PHP 对象的身份和别名语义,TypePHP 变量保存原始对象指针:
```cpp
php_app__point *point;
```
赋值只复制指针:
```php
$a = new Point();
$b = $a;
$b->x = 10;
```
生成语义近似:
```cpp
auto *a = native_heap.make<php_app__point>();
auto *b = a;
b->x = 10;
```
因此 `$a``$b` 仍指向同一个对象,不会因为采用 C++ `struct` 而变成值复制。
Native Class Object 不使用 `std::shared_ptr`。`std::shared_ptr` 的控制块、原子引用计数和循环引用问题与本特性的极致性能目标不符。
Native Object 的严格比较使用指针身份:`===`/`!==` 判断两个槽是否指向同一个 Native
对象,也支持与 `null` 比较。与任何 Zend 标量或 Zend Object 的严格比较恒为
`false`,不会把裸指针隐式转换为 `bool`。`match` 条件同样使用这一指针身份规则。
PHP 的 `==`/`!=` 会递归比较 Zend Object 属性;Native
Object 没有 Zend object handler,而且对象图可能包含环,因此不提供隐式字段值比较。
松散比较、大小比较和算术/位运算在编译期直接报错。需要值相等语义时应声明一个具有
明确字段和循环处理规则的普通 Native 方法。
一元算术/位运算、`++`/`--`、复合算术赋值和 `switch` 也依赖 PHP 的数值或松散比较
语义,因此禁止用于 Native Object。该检查必须发生在 C++ 生成前,避免裸指针意外进入
合法但危险的 C++ 指针算术。
`isset($native)``empty($native)` 直接检查裸指针是否为 `nullptr`。命名 Native
属性链使用短路 lambda 逐级检查中间指针,不会把指针传入 `php::Variant`,因此
`isset($node->next->next)` 在中间槽为空时返回 `false`,而不是触发空对象调用。
### 7.1 Native Class 循环引用
两个或多个 Native Class 可以在属性类型上相互引用:
```php
#[Native]
final class A
{
public ?B $b = null;
}
#[Native]
final class B
{
public A $a;
public function __construct(A $a)
{
$this->a = $a;
}
}
```
生成 C++ 时必须先前置声明所有 Native struct:
```cpp
struct php_a;
struct php_b;
struct php_a final {
php_b *b;
};
struct php_b final {
php_a *a;
};
```
Native Class 属性始终保存指针,不按值嵌入另一个 Native struct,因此不会产生无限递归的对象尺寸,也不要求按依赖顺序完整定义 struct。
编译器应对 Native Class 类型依赖图计算强连通分量(SCC):
- SCC 只用于安排前置声明、完整定义和方法实现的生成顺序。
- 循环类型依赖本身不是错误。
- 禁止把 Native Class 属性生成为 by-value struct 字段。
- 所有 Native struct 完整定义完成后,再生成依赖完整类型的方法体。
Native Heap tracing GC 能够遍历所有 Native 指针字段,因此 A 与 B 相互指向不会形成引用计数循环,也不会产生永久泄漏。裸指针字段本身没有析构动作。
Native Object 属性的零值是 `nullptr`,包括源码中声明为 non-nullable 的 Native Class
属性。non-nullable 约束只作用于后续显式赋值,不引入 PHP typed property 的 UNDEF
状态。因此循环对象图可以先分别构造,再建立双向关系:
```php
$a = new A();
$b = new B($a);
$a->b = $b;
```
不需要构造依赖 SCC 或“两阶段发布”机制;类型 SCC 仅用于 C++ 前置声明与生成顺序。
## 8. 内存与生命周期
Request Arena 只能作为分配器和 Request Shutdown 兜底,不能作为唯一生命周期机制。对于常驻 CLI、HTTP Server 或单个超长 request,如果对象只能在 Request Shutdown 释放,内存会持续增长。
Native Class Object 应使用独立的、非移动、精确 tracing GC。本文将该运行时称为 Native Heap。
### 8.1 Native Heap
首版 Wren 派生实现为每个对象分配一块连续内存,并在 struct 前放置隐藏 GC header:
```cpp
struct NativeGcHeader {
const NativeTypeDescriptor *type;
NativeGcHeader *next;
uint32_t flags;
uint32_t size;
};
// 内存布局:[NativeGcHeader][php_app__point]
auto *point = native_heap.make<php_app__point>();
```
GC header 不属于生成的 C++ struct,也不会改变属性偏移。用户可见对象变量仍然只是一个 `native_struct *`
当前分配器使用独立 non-moving allocation;Arena/chunk/free-list 可以作为后续分配器
优化,但不得改变对象地址稳定性、header 布局、精确 tracing 或 finalization 语义。
Native Heap 具有以下特征:
- non-moving:对象地址从创建到回收始终不变。
- precise:只扫描编译器明确登记的 Native 指针,不保守扫描任意内存。
- stop-the-world:初版只在当前 TypePHP request/thread 的安全点执行完整回收。
- non-atomic:Native Object 不跨线程,GC 元数据不使用原子操作。
- 无 per-assignment retain/release:普通指针赋值不修改引用计数。
Request Shutdown 会销毁 Native Heap 中的全部剩余对象,但正常运行期间也会周期性回收不可达对象。
### 8.2 类型描述与对象图遍历
每个 Native struct 生成静态类型描述:
```cpp
struct NativeTypeDescriptor {
void (*trace)(void *object, NativeMarkVisitor &visitor);
void (*destroy)(void *object);
size_t size;
size_t alignment;
};
```
`trace()` 只访问 Native Class 指针字段:
```cpp
static void trace_a(void *ptr, NativeMarkVisitor &visitor) {
auto *object = static_cast<php_a *>(ptr);
visitor.mark(object->b);
}
```
`php::Str`、`php::Array`、`php::Object`、`php::Var` 和 Stream 字段由 Zend 引用计数管理,但它们不能反向保存 Native Object,因此无需由 Native GC 深入扫描。
禁止 Native Object 进入 PHP Array、Box 和 Zend Object,是保证 Native 对象图封闭且可精确遍历的重要条件。局部 Std Container 是例外:当元素类型明确写成 Native Class 时,容器直接保存 typed Native pointer,并由独立的容器 Root Frame 在 GC 标记阶段遍历其当前元素。该 Root Frame 跟踪容器而不是元素地址,因此 vector/map 扩容搬迁不会产生悬空 root。
### 8.3 Root 管理
GC 必须知道当前仍被 TypePHP 代码引用的 Native Object。编译器为可能跨越 GC safe point 存活的局部变量生成轻量 shadow root frame:
```cpp
struct FunctionNativeRoots {
NativeRootFrame frame;
php_a *a;
php_b *b;
};
```
函数入口将 frame 链接到当前 Native Heap,退出时通过 C++ RAII 自动解除链接。C++ 异常展开时也必须正确移除 frame。
为降低开销:
- 不包含 Native Object 的函数不创建 root frame。
- 只借用调用者对象且不会触发 Native 分配的叶子方法不创建 root frame。
- 只登记可能跨越 Native allocation 或显式 GC safe point 存活的变量。
- 普通方法 receiver 由调用者的 root 或对象图保持存活,不重复登记。
- 临时对象如果跨越一次可能触发 GC 的调用,必须先写入 root slot。
- Native Class 允许保存在 TypePHP global 和 static local 中。这些槽不进入 Zend `$GLOBALS`,而是生成独立 Native 指针槽。
- ZTS 构建中的 global/static 指针槽和 static 初始化状态均使用 `THREAD_LOCAL`,不同线程之间不共享 Native 对象。
- RINIT 将这些槽登记为 request root;RSHUTDOWN 清空槽和初始化状态,随后由 Native Heap 统一 finalization 和回收。
这比每次对象赋值执行引用计数更适合大量属性写入和循环计算。
### 8.4 GC 触发点
初版只在确定的 safe point 执行 GC:
- Native Heap 分配量超过自适应阈值。
- Request Shutdown 强制清理全部对象。
Native GC 不暴露语言级显式收集函数。PHPX 内部的收集入口只供运行时阈值策略和底层测试使用,不注册为 TypePHP/PHP API。
普通字段读取、字段写入和方法调用本身不触发 GC。GC 不应异步运行,也不在任意 C++ 指令之间发生。
初版采用完整 mark-sweep:
1. 从 shadow root frame、global/static root 开始标记。
2. 通过每个类型的 `trace()` 遍历 Native 指针字段。
3. 使用显式 worklist,避免递归遍历造成 C++ 栈溢出。
4. 扫描 Native Heap,回收未标记对象。
5. 保留存活对象地址,清除 mark 状态。
6. 根据本次存活比例调整下一次 GC 阈值。
A/B 相互引用但无法从任何 root 到达时,两者都会在同一次 sweep 中回收。
### 8.5 析构与 GC 重入
不可达对象可能包含 `php::Array`、`php::Object` 或 `php::Var`。销毁这些字段时,Zend Object destructor 可能执行用户代码,甚至再次分配 Native Object。因此 sweep 不能一边修改 GC 链表一边直接执行全部 C++ 析构。
初版必须采用 finalize/destroy 分离的回收流程:
1. 标记并从活动对象集合中摘除全部不可达对象,将其状态设为 `finalizing`
2. 完成 GC 内部数据结构更新后,在 GC 临界区外调用用户 `__destruct()` finalizer。
3. finalizing 阶段产生的新 Native Object 加入新的活动列表。
4. finalizing 期间禁止递归进入 GC;新的收集请求记录为 pending,在本轮完成后执行。
5. Native Object 本身不能进入 ZendVM,但用户 `__destruct()` 可以把 `$this` 保存到另一个 Native root,使对象在 finalization 期间复活;对象的 finalized 状态保证用户析构最多执行一次。
6. finalizer 完成后重新扫描 roots;用户 `__destruct()` 与实际 C++ 字段析构分离,只有未复活对象才执行 C++ destroy 和存储释放。
### 8.6 后续栈分配优化
当逃逸分析能够证明对象不会离开当前函数时,可以直接栈分配:
```cpp
php_app__point point_storage;
auto *point = &point_storage;
```
栈分配属于后续优化,不应成为首版正确性依赖。返回值、写入另一个 Native Object 属性或传给未知函数的对象均视为逃逸。
栈上 Native Object 本身不加入 Native Heap,但如果包含 Native 指针字段,GC root descriptor 必须能够遍历该栈对象的对外引用。
### 8.7 Request Shutdown
Request Shutdown 是最终兜底,而不是常规对象回收时机。它必须在 PHP 内存池销毁前停止 GC、摘除 root frame,并销毁 Native Heap 中的全部剩余对象。
不能直接等待 PHP 内存池统一释放,否则 `php::Str`、`php::Array`、`php::Object`、`php::Var` 等字段持有的资源无法正确析构。
### 8.8 开源 GC 实现参考
Native Heap 不应从零发明未经验证的 GC 模型,但也不适合直接嵌入完整语言 VM。应复用成熟算法和测试方法,并针对 TypePHP 的封闭 Native 对象图实现小型专用 GC。
| 项目/算法 | 特点 | 对 TypePHP 的适用性 |
|---|---|---|
| Wren GC | 小型、non-moving、精确 mark-sweep、显式 gray worklist、自适应 heap threshold | 最适合作为首版代码上游;无对象赋值 barrier,容易验证和移植 |
| BDWGC | 历史悠久的 C/C++ conservative collector,默认 STW,也支持部分平台的 incremental/parallel 能力 | 成熟度和接入便利性最高,但不能保证回收所有不可达对象 |
| Oilpan/cppgc | Chrome/Blink 使用的 C++ tracing GC,精确扫描 heap、保守扫描 native stack,支持并发/增量处理 | 大型 C++ 项目成熟,但要求 `GarbageCollected<T>`、`Member<T>`、Trace 和 write barrier,集成过重 |
| MMTk | Rust GC framework,具有 MarkSweep、Immix、generational 等多种 plan 和多语言 VM binding | 性能上限高,但需要实现完整 VM binding、root scanning、object model、barrier 和 safepoint |
| mruby GC | 三色增量 mark-sweep,可选 generational,具有 root arena 和 write barrier | 适合作为第二阶段增量 GC 参考;实现和状态机更复杂 |
| Lua 5.4 GC | 成熟的 incremental/generational collector,可调 pause、step multiplier 和 step size | 长时运行经验丰富,但与 Lua VM 深度耦合,不适合直接集成 |
| PHP/CPython 风格 RC + cycle collector | 对象不可达时通常立即释放,循环由附加 collector 处理 | 高频传参、赋值和字段写入都会产生 INCREF/DECREF,不符合主要性能目标 |
官方参考:
- Wren VM GC:<https://github.com/wren-lang/wren/blob/main/src/vm/wren_vm.c>
- BDWGC:<https://github.com/bdwgc/bdwgc>
- Oilpan standalone library:<https://v8.dev/blog/oilpan-library>
- Oilpan C++ GC 设计:<https://v8.dev/blog/high-performance-cpp-gc>
- MMTk plans/bindings 状态:<https://www.mmtk.io/status>
- MMTk VM porting guide:<https://docs.mmtk.io/portingguide/>
#### 8.8.1 性能比较
TypePHP 的主要热路径是 Native Object 指针传参、变量赋值和 Native 指针属性写入,而不是 GC 本身。候选方案必须优先避免让每次赋值承担额外成本。
| 方案 | 指针赋值热路径 | 分配与回收 | 暂停特征 |
|---|---|---|---|
| Wren 派生 STW mark-sweep | 裸指针写入,无 RC、无 barrier | 简单 free-list/page allocator,完整 heap mark/sweep | heap 很大时 full mark 暂停较长 |
| BDWGC 默认模式 | 普通裸指针写入,无显式 barrier | 高度优化且成熟;扫描 stack、register、globals 和 GC heap | 默认 STW;部分平台可 incremental/parallel |
| Oilpan/cppgc | `Member<T>` 写入;增量/并发 marking 需要 barrier fast path | page heap、并发/增量 marking/sweeping 成熟 | 低暂停能力最好,但 mutator 热路径更复杂 |
| MMTk MarkSweep | 可选择 NoBarrier 的 non-moving MarkSweep | allocator/metadata/parallel worker 基础设施强 | 取决于 binding 和 plan;首版 binding 本身成本很高 |
| MMTk Immix/Generational | 需要 barrier、object logging 或 remembered set;部分 plan 可能移动对象 | 吞吐和空间利用潜力最高 | 可实现更低暂停,但破坏裸指针稳定性的风险更高 |
对于“对象互相传递、赋值、引用非常多”的 TypePHP 程序,Wren 派生 STW 和 BDWGC 默认模式的 mutator 热路径最有优势。Oilpan 和 MMTk 高级 plan 的优势主要体现在大 heap 的暂停和吞吐,而代价会进入每次指针写入或整体 runtime integration。
#### 8.8.2 精确性与长期内存稳定性
BDWGC 是 conservative collector。它把 stack/register/global 中看起来像 GC heap 地址的机器字当作潜在指针。其官方文档明确说明,它不保证回收所有不可访问存储。误识别通常只是延迟回收,但在长期运行程序中,内存上界会依赖 stack 内容、地址布局和编译器行为。
BDWGC 可以使用 typed allocation descriptor 减少 heap 内部的误扫描,但 native stack 仍然是 conservative root。TypePHP 已经能够在编译期准确知道 Native pointer local 和 Native pointer field,因此放弃这些信息改用 conservative scanning 并不理想。
Oilpan 同样是 heap precise、native stack conservative。它在 Chrome/Blink 中可靠,但仍可能因 native stack 上的伪指针延迟回收。Oilpan 的使用场景可以利用 event-loop task 边界选择更干净的 stack 状态;常驻 TypePHP CLI 不一定具备相同条件。
Wren 派生 GC 与 MMTk 都可以使用 TypePHP 生成的 shadow root frame 做到完全 precise。只要 root frame 和 `trace()` 生成正确,不存在伪指针导致的对象滞留。
#### 8.8.3 对象布局兼容性
TypePHP 已确定 Native Object 变量是裸指针,Native struct 只包含 public 字段,方法是 `php_*` 自由函数。
- Wren 派生 GC 可以把 GC header 放在 struct 之前,并通过 `NativeTypeDescriptor` 扫描裸指针字段,完全匹配该布局。
- BDWGC 允许直接返回裸指针,对布局侵入最少,但无法自然复用 TypePHP 的精确 root 信息。
- Oilpan 要求 GC object 使用 `GarbageCollected<T>`,heap pointer 使用 `Member<T>`,并提供 `Trace()`;这会改变已经确定的 struct 和字段设计。
- MMTk 不强制 C++ 基类,但 binding 必须定义 object reference、header/side metadata、copy/pin、root slot 和 object scanning。若使用 moving plan,所有裸指针还必须可更新或永久 pin。
#### 8.8.4 C++ 析构和 Zend 重入
Native Object 可以包含 `php::Str`、`php::Array`、`php::Object` 和 `php::Var`,回收时必须执行 C++ 析构;Zend Object destructor 还可能执行 PHP 用户代码。
- Wren 派生库可以按 TypePHP 需要实现“摘除不可达对象,再在 GC 临界区外析构”的两阶段流程。
- BDWGC 提供 finalizer,但 finalizer 的执行顺序和重新可达语义需要额外适配;它无法直接理解 ZendVM 的异常和 request 生命周期。
- Oilpan 会对具有非平凡析构的对象执行 finalization,但官方约束 finalizer 不应访问其他 on-heap object;复杂场景需要 pre-finalizer,并依赖其运行时规则。
- MMTk 把 finalizer/weak reference 语义留给 VM binding,实现责任仍然落到 TypePHP。
因此四种方案都不能直接解决 Zend 重入;Wren 派生方案虽然需要自行实现,但可以只实现 TypePHP 实际需要的严格语义。
#### 8.8.5 成熟度与集成风险
| 方案 | 上游成熟度 | TypePHP 新增代码风险 | 构建与分发 |
|---|---|---|---|
| Wren 派生 GC | Wren 算法经过长期使用;提取后的派生库需要 TypePHP 自己验证 | 中等,核心小但 root/finalization adapter 必须充分测试 | 小型 C 静态库,容易支持 GCC/Clang/MSVC/WASI |
| BDWGC | 最高,拥有长期 C/C++ 使用历史和多平台代码 | 低到中等,主要风险是 conservative retention 和 Zend finalizer adapter | CMake/静态库成熟,最接近 GMP 类依赖 |
| Oilpan/cppgc | Chrome/Blink 内成熟度很高 | 高,API、对象布局、platform/task integration 都与当前设计冲突 | 源于 V8 工程,GN/platform 依赖和版本升级成本大 |
| MMTk | GC framework 和多个 VM binding 活跃 | 很高,新 TypePHP binding 本身就是大型 runtime 项目 | 增加 Rust/Cargo、C ABI、worker/safepoint 和跨平台构建链 |
Oilpan 和 MMTk 的“上游成熟”不能直接等价为“TypePHP 集成可靠”。真正决定可靠性的会是新建的 adapter/binding,而这两种方案要求的 binding 面远大于 Wren 派生库或 BDWGC。
#### 8.8.6 跨平台与 WASM
TypePHP 需要同时考虑 Linux、Windows、macOS 和 wasm32-wasip2:
- Wren 派生 GC 只依赖显式 root frame 和普通线性内存,最容易跨平台。
- BDWGC 的 native stack/register/dynamic-library 扫描包含平台相关实现。原生桌面平台成熟,但 WASI 需要单独验证;WebAssembly 通常无法像原生程序一样任意检查 VM stack,传统移植往往需要 shadow stack。
- Oilpan/cppgc 依赖 V8 platform/task 基础设施,不适合作为 WASI 静态库依赖。
- MMTk 当前没有 TypePHP/WASI binding;Rust target 可用不代表 GC plan、线程、内存映射和 root scanning 已可用。
#### 8.8.7 最终选择
综合结论:首版继续选择 Wren 派生的精确、非移动、stop-the-world mark-sweep。
选择依据按优先级排列:
1. Native 指针赋值和传参保持真正的裸指针零附加操作。
2. 使用 TypePHP 已知的精确 root/field 信息,避免 conservative retention。
3. 保持对象地址稳定,不引入 handle、pin 或 pointer update。
4. 可以完全控制 C++ 字段析构、Zend 重入和 request shutdown 顺序。
5. C 静态库小,适合现有 CMake、三大桌面平台和 WASI 工具链。
6. GC 功能只影响 `#[Native]` 分支,不把大型 runtime framework 带入普通 TypePHP 程序。
BDWGC 保留为备选验证基线。实现阶段可以用相同 benchmark 对比 Wren 派生 GC 与 BDWGC typed allocation;如果 Wren 派生实现未能通过可靠性、长时运行或性能门槛,可以退回 BDWGC,而不是直接跳到 Oilpan/MMTk。
Oilpan 不采用,主要原因是对象布局、`Member<T>` write barrier、保守 stack 和 V8 platform/build 依赖与当前设计冲突。MMTk 暂不采用,主要原因是 VM binding 和 Rust runtime 集成规模远超首版需求;当 Native Heap 达到数 GB、full mark pause 成为实际瓶颈且项目能够承担专门 GC 团队时,可以重新评估 MMTk MarkSweep/Immix。
在编码前应定义稳定的 GC adapter API,但首版只实现和交付 Wren backend:
```cpp
namespace php::native_gc {
void *allocate(
size_t size,
size_t alignment,
const NativeTypeDescriptor *type
);
void addRoot(NativeRootFrame *frame);
void removeRoot(NativeRootFrame *frame);
void collect();
void shutdown();
} // namespace php::native_gc
```
这里不使用运行时函数表、虚函数或 backend 对象。生成代码只调用固定符号,最终静态链接到 Wren adapter,分配入口可以被 LTO 内联。基准测试若要替换为 BDWGC,可在单独构建目标中链接实现相同 API 的 adapter;正式产物不承担多 backend 的运行时抽象成本。
Wren GC 确定为 Native Heap 的首版算法和代码上游。Wren 使用 MIT License,但它的 GC 实现目前与 Wren VM 对象模型耦合,并不是可以直接链接的独立 GC library。因此 TypePHP 应从固定的 Wren upstream commit 提取最小 collector 子集,并维护为独立第三方派生库,而不是将完整 Wren VM 链入程序。
建议目录:
```text
phpx/thirdparty/wren-gc/
├── include/
│ └── wren_gc.h
├── src/
│ └── wren_gc.c
├── LICENSE
├── UPSTREAM.md
└── CHANGES.md
```
第三方库要求:
- `LICENSE` 保留完整 Wren MIT License 和原始版权声明。
- `UPSTREAM.md` 记录 Wren 仓库 URL、提取文件和固定 commit hash。
- `CHANGES.md` 记录从 Wren Object/VM 模型适配到 TypePHP `NativeTypeDescriptor`/root frame 的修改。
- 上游代码与 TypePHP adapter 分离,避免把编译器逻辑继续写入第三方文件。
- 生成独立静态库,例如 `libwren_gc.a`,构建和链接方式与 GMP、MPFR、libmpdecimal 等第三方依赖保持一致。
- 未使用 `#[Native]` 的程序不需要初始化 Native Heap;是否仍统一链接静态库由最终构建方案决定。
- 不导入 Wren parser、bytecode VM、对象系统、标准库或其他无关模块。
PHPX 的 Native GC adapter 负责类型 descriptor、root frame、C++ 析构回调、Zend 重入保护及面向生成代码的稳定 C++ API。TypePHP 编译器只负责生成 descriptor、trace/finalize/destroy 函数、root frame 操作和调用代码。第三方 Wren GC 只负责对象登记、mark worklist、sweep、阈值与 heap page/free-list 管理。
### 8.9 首版算法选择
首版确定采用 Wren 风格的精确、非移动、stop-the-world mark-sweep:
- Native 指针赋值不执行引用计数。
- Native 指针属性写入不需要 write barrier。
- 普通传参只是复制一个指针。
- GC 只在 Native allocation、显式收集和 shutdown safe point 运行。
- 每次收集都从精确 root 开始遍历完整 Native 对象图。
- 循环对象与普通不可达对象使用同一算法回收。
- 收集完成后使用存活字节数计算下一次阈值。
首版采用以下固定默认值:
- 首次收集阈值:10 MiB。
- 收集后的最低阈值:1 MiB。
- 存活堆增长比例:50%。
- 下一次收集阈值:`max(1 MiB, liveBytes + liveBytes * 50%)`。
这组参数沿用 Wren 的成熟默认值。阈值会随每轮收集后的实际存活字节数自动伸缩,
但不读取 PHP `memory_limit`、主机物理内存或容器内存。PHP `memory_limit` 面向
Zend 请求内存,常见的 128 MiB 默认值不能代表常驻 TypePHP 程序的 Native Heap
预算;按主机内存同比放大阈值也会使相同程序在不同机器上表现不稳定。
10 MiB 只表示首次触发完整收集前允许的累计 Native allocation,并不是预留或立即
申请 10 MiB。1 MiB 下限避免小型存活集反复触发 stop-the-world 收集;50% headroom
在扫描 CPU 与额外内存之间采取比 Go 默认 100% 更保守的折中,因为首版 collector
是单线程 stop-the-world,而不是并发 collector。后续只能依据 TypePHP 的真实分配率、
存活率、暂停时间和峰值内存 benchmark 调整这些内部常量,不开放语言级 GC 调参接口。
选择 stop-the-world 而不是增量 GC 的主要原因,是 TypePHP 程序中的对象传递、字段赋值和引用更新可能非常密集。增量三色 GC 必须在 marking 期间维持颜色不变量,从而在 Native 指针字段写入路径增加 write barrier。即使 barrier 的正常路径只有一次分支,它仍会影响最重要的高频路径。
### 8.10 后续低停顿模式
如果 benchmark 证明完整 mark 阶段停顿不可接受,可以参考 mruby/Lua 增加可选的 incremental 模式,但不能改变默认快速路径:
```cpp
object->child = value;
if (UNLIKELY(native_heap.is_incremental_marking())) {
native_heap.write_barrier(object, value);
}
```
实际生成时应先判断 GC phase,再只在 marking 阶段执行 barrier。普通模式下编译器可以完全不生成 barrier;启用 incremental 模式时才增加 `UNLIKELY` 分支。
Generational GC 需要 remembered set,并使 old-to-young 指针写入长期携带 barrier,不应进入首版。只有在真实应用证明大量 Native Object“朝生夕死”且 full mark 成本明显时再评估。
## 9. 参数传递
普通对象参数按指针值传递:
```php
function move(Point $point, float $x): void
{
$point->x = $x;
}
```
近似生成:
```cpp
void php_move(php_app__point *point, php::Float x);
```
这同时满足:
- 不复制对象实体。
- 不增加引用计数。
- 修改属性对调用者可见。
- 在函数内部重新赋值 `$point` 不影响调用者变量。
这里不需要、也不允许 PHP 引用符号:
```php
function replace(Point &$point): void; // FatalError
$alias =& $point; // FatalError
refval($point); // FatalError
$point->toRef(); // FatalError
```
普通的 `$alias = $point` 已经只复制有类型的指针,二者指向并修改同一个对象。
返回 Native Object 时返回指针:
```cpp
php_app__point *php_create_point();
```
非 nullable class 参数在函数入口执行一次空指针检查。确定非空的成员访问不应重复检查。
nullable class 必须使用 `?Point`,使用相同指针表示,`nullptr` 表示 `null`
`Point $value = null` 这种隐式 nullable 声明不支持,必须写为 `?Point $value = null`
`Point|null`、其他 union/intersection、Native variadic 参数以及 Native 引用返回均不支持。
参数和返回值必须显式声明具体 Native Class(或其 nullable 形式),不能通过 `mixed`
`object` 或 Interface carrier 传递。
例如:
```php
function bar(Point $point): void
{
// 函数入口已经完成非空检查;进入函数体后 $point 一定指向 Point 对象。
echo $point->x;
}
function maybeBar(?Point $point): void
{
// $point 可能是空指针,使用前必须先收窄或由成员访问生成空值检查。
if ($point !== null) {
echo $point->x;
}
}
```
两者的 C++ ABI 都使用 `php_app__point *`,但契约不同:`bar()` 在执行第一条用户
语句前拒绝 `nullptr`;`maybeBar()` 接受 `nullptr`。这项入口保证只约束传入时的值,
函数内部仍可把自己的局部指针槽重新赋为 `null`,且不会改变调用者的变量槽。
## 10. ZendVM 边界
Native Object 没有对应的 `zval` 表示,因此只能传给明确接受相同 Native Class或其 Native 基类的参数。Interface 只用于校验 Native Class 的声明契约,不能作为 Native Object 的参数、属性、变量或返回值 carrier。
Native Class 的字段可以保存 `php::Var`、`php::Array` 或 `php::Object`,但这并不会使外层 Native Object 获得 `zval` 表示。允许“Zend 值进入 Native 字段”,不等于允许“Native Object 进入 ZendVM”。
初版禁止将 Native Object:
- 赋值给 `mixed` 或普通 `object`
- 转换为 `php::Var``php::Object`
- 传给未知 PHP 函数、PHP 扩展函数或动态方法。
- 使用 `$nativeObject->$expr()`、`$nativeObject->{$expr}()` 等变量方法名调用。
- 放入普通 PHP `array`
- 捕获到需要注册为 Zend Closure 的闭包中。
- 作为 TypePHP Generator 的参数、`this`、局部变量、返回类型或 `yield` 值。Generator
由 Zend Closure/Fiber 状态机表示,Native pointer 不进入该 Zend 状态。
- 作为 Fiber API 的传入值、恢复值或 Closure capture。普通 TypePHP 函数可以在自己的
C++ 局部槽中保存 Native Object 并跨越 `Fiber::suspend()`;Native Root Frame 使用
可任意 O(1) 摘除的 thread-local 双向侵入链表,GC 会同时扫描运行中与挂起 Fiber 的
有效 frame,不依赖跨 Fiber 的 LIFO 析构顺序。
- 作为 `call_user_func()` 等动态 callback 的 receiver。
- 保存到 ZendVM 全局变量或对象属性中。
Box 不能保存 Native Object。Std Container 不能作为 Native Class 属性,但局部
`std::array`、`std::vector`、`std::map` 和 `std::ordered_map` 可以使用具体
`NativeClass::class` 作为 value type,并保存该类或其 Native 子类。普通 PHP array
仍然不能保存 Native Object。
任何跨越 ZendVM 边界的行为都应在编译期抛出 FatalError。编译器不得静默装箱或降级,因为这会使性能模型不可预测。
Native Object 必须始终保持 typed object。它不能被擦除为 `var`、`mixed`、普通 `object` 或无类型 callback receiver。即使编译器能够常量折叠 `$expr = 'run'`,变量方法名语法仍不支持;只有源码中明确写出的 `$nativeObject->run()` 才进入 Native method resolution。
## 11. 属性访问
没有 Hook 的属性直接访问字段:
```php
$point->x = 1.0;
echo $point->x;
```
近似生成:
```cpp
point->x = 1.0;
echo(point->x);
```
所有实体属性必须显式声明类型。初版不支持动态属性、字符串属性名、`__get()` 和 `__set()`
Visibility 只在编译期检查,不生成运行时访问控制元数据。
### 11.1 Visibility
`public`、`protected`、`private` 的访问权限完全由 Native Class 编译器静态检查。运行时不保存 visibility flag,也不执行 scope 切换或权限判断。
生成的 C++ `struct` 中所有字段都保持 public:
```cpp
struct php_app__user final {
php::Str name;
php::Int age;
};
```
PHP 源码中的 `private string $name` 不生成 C++ `private:`。这是因为方法是 `php_*` 自由函数,C++ private field 会阻止对应方法函数直接访问字段,并迫使实现引入 friend、成员方法或额外 accessor。
编译器必须在以下位置完成静态权限检查:
- 直接属性读取和写入。
- Getter、Setter 和 Property Hook lowering。
- 方法调用和静态方法调用。
- clone 字段复制。
- Trait AST 注入后的访问。
- 编译器生成的辅助代码。
Native Object 不能进入动态调用、Reflection 或 ZendVM,因此不存在运行时绕过 visibility 的合法入口。手工编写 C++ 代码直接访问字段不属于 TypePHP 语言兼容范围。
## 12. Getter、Setter 和生成器注解
Getter、Setter 等纯编译期生成器注解可以支持。它们应先展开为普通 AST,再由 Native Class 分支生成 `php_*` 自由函数。
```php
#[Native]
final class User
{
#[Getter]
#[Setter]
private string $name;
}
```
近似生成:
```cpp
struct php_app__user final {
php::Str name;
};
php::Str php_app__user__getname(php_app__user &this_) {
return this_.name;
}
void php_app__user__setname(php_app__user &this_, php::Str value) {
this_.name = value;
}
```
简单 Getter/Setter 应允许 C++ 编译器完全内联。Native Class 不注册注解或生成 Reflection 元数据。
原则上可以支持所有只修改 AST、不依赖 ZendVM 的生成器注解。具体支持清单需要在实现前逐项确认。
### 12.1 Trait AST 注入
Native Class 支持 Trait。Trait 不建立独立的 Native runtime 类型,也不生成对象实体;继续复用 TypePHP 现有的编译期 AST 注入机制。
处理顺序固定为:
1. 解析 class 和 Trait,并完成 `use`、`insteadof`、`as` 及冲突检查。
2. 在 convert 阶段把 Trait 的属性、常量、方法和 Property Hook AST 注入目标 class。
3. 保留节点的 Trait 来源、Trait namespace/use context 和 `__TRAIT__` 信息。
4. 对注入后的完整 class AST 执行 Native Class 类型、visibility、继承、Interface 和边界检查。
5. 将注入成员与普通 class member 一样生成字段及 `php_*` 方法。
同一个 Trait 可以同时被普通 TypePHP class 和 Native Class 使用;最终采用哪一种对象模型,由目标 class 决定。Trait 注入后的属性仍必须具有合法的显式类型,方法也必须满足 Native Class 的 ZendVM 边界限制。
### 12.2 Interface
Interface 不接受 `#[Native]`。它仍是普通 PHP/TypePHP Interface,照常注册到
ZendVM;普通 PHP class 实现该 Interface 的行为不变。Native Class 支持
`implements`,但它与该 Interface 的关系只存在于 TypePHP 编译期:
- 使用与 PHP 一致的规则检查 required method 和 hooked property 是否存在、visibility、
static/引用/variadic、参数与返回类型及属性读写约束是否兼容。
- 项目 Interface 使用预处理得到的完整声明;PHP 内置 Interface 使用 Reflection 得到的
正式签名。Tentative return type 保持 PHP 8.4 的非致命语义,不擅自升级为 FatalError。
- 在 Trait AST 注入及继承成员合并完成后检查,因此 Trait 或父类提供的方法可以满足 Interface。
- 支持 Interface 继承和多个 `implements` 声明。
- 不为 Native Class 生成 Interface vtable、runtime interface id、`zend_class_entry` 或
Reflection 元数据;ZendVM 不会看到该 Native Class 是 Interface 的 implementor。
- 当 receiver 的具体 Native Class 在编译期已知时,`$native instanceof SomeInterface`
根据完整 `implements` 关系直接折叠为 `true``false`
- 不支持动态 Interface cast,也不能把 Native Object 交给 ZendVM 的 Interface 参数或
使用 Reflection 查询其实现关系。
Interface 类型不能成为 Native Object 的类型擦除 carrier。即使调用点知道具体 Native
Class,也禁止把 Native Object 传给 Interface typed parameter,或赋值、返回为 Interface
类型。Interface typed 参数和属性仍可正常保存 Zend Object,但不能同时保存 Native
Object。编译器不得为此生成 `reinterpret_cast`、`void *` 转换或临时 Zend Object;错误
转换会破坏对象布局并可能导致 crash,因此必须在 C++ 代码生成前抛出 FatalError。
首版明确不提供调用点静态特化、fat pointer 或 interface table。需要共享 Native 方法实现
时使用 Trait;需要多态传参时使用具有真实 C++ 继承关系的 Native 基类。若未来确实需要在
多个无共同 Native 基类的实现之间做运行时动态分派,应作为新的对象表示单独设计,不能
偷偷把 Native Object 装箱为 Zend Object,也不能改变当前裸指针 Native Call 的热路径。
### 12.3 `instanceof`
Native Class 没有 `zend_class_entry` 或运行时类名查找,因此只支持目标 class
能够在编译期解析的 `instanceof`。编译器依据 Native 静态类型与继承关系直接
折叠为 `true``false`,但仍保留左操作数中构造、函数调用等副作用:
```php
$object instanceof NativeClass;
```
TypePHP 不为 `NativeClass::class` 增加特殊的 `instanceof` 语法。以下运行时
class operand 不支持:
```php
$class = NativeClass::class;
$object instanceof $class; // FatalError
```
如果变量的静态类型是 Native 父类,而目标是其子类,结果依赖对象的运行时动态
类型,编译器同样抛出 FatalError,不伪造错误的布尔结果。
## 13. Property Hook
Property Hook 可以编译为确定的 C++ getter/setter:
```php
#[Native]
final class User
{
public string $name {
get => strtoupper($this->name);
set => trim($value);
}
}
```
近似生成:
```cpp
struct php_app__user final {
php::Str name_storage;
};
php::Str php_app__user__get_name(php_app__user &this_);
void php_app__user__set_name(php_app__user &this_, php::Str value);
```
读取和赋值分别生成:
```cpp
php_app__user__get_name(*user);
php_app__user__set_name(*user, value);
```
为保持 Native 分支简单且不存在隐式运行时分派,首版只支持直接读取和直接赋值:
```php
$value = $user->count;
$user->count = getValue();
```
带 Hook 的属性禁止:
- `+=`、`.=` 等复合写入。
- `++`、`--`。
- `$object->hookedArray[] = ...`、元素赋值或元素 `unset()` 等间接写入。
- `isset()`、`empty()`。
- 取引用。
- 引用返回。
- 返回底层属性 slot。
- 使用 `int_ref`、`float_ref` 等引用优化。
- 绕过 Hook 直接写入 backing field。
Native Property Hook 只有编译期语义,不生成 Zend Property Hook 元数据。
## 14. Clone
`clone` 可以支持,但必须由编译器生成字段级浅复制,不能无条件依赖 C++ 默认 copy constructor。
当 receiver 的静态类型可能保存 Native 子类时,继承层次生成一个内部协变 virtual clone
thunk,由动态子类执行正确尺寸的字段复制并调用其 `__clone()`;不能按静态基类复制,否则会
发生 C++ object slicing。没有 Native 继承关系的类继续使用静态 clone 路径,不增加 vptr。
```php
$copy = clone $source;
```
近似生成:
```cpp
auto *copy = native_heap.make<php_app__user>();
copy->name = source->name;
copy->profile = source->profile;
php_app__user____clone(*copy);
```
复制规则:
- 标量字段按值复制。
- String、PHP Array、Zend Object、Stream 和 mixed 按各自 PHPX/C++ 类型的复制语义处理。
- PHP Array 保持 PHP 的 copy-on-write 行为,不进行无条件深拷贝。
- Zend Object 字段复制对象句柄,继续指向同一 Zend 对象。
- Native Object 字段复制指针,继续指向同一对象,保持浅复制语义。
- 完成字段复制后调用可选的 `__clone()`;子类未重新声明时会解析并调用继承的
`__clone()`,子类重新声明时不会隐式再调用父实现,与普通方法覆盖规则一致。
- `__clone()` 的 public/protected/private 可见性在 clone 表达式所在的编译期作用域检查,
不允许通过直接 Native Call 绕过。
- clone operand 可以是 typed variable、Native function/method call 或 Native property
expression;非变量 operand 会先物化为精确 root 的临时裸指针。
包含不可复制字段的 Native Class 必须显式禁止 clone;对它使用 `clone` 时编译期报错。
## 15. 构造和析构
### 15.1 构造
`new` 在 Native Heap 中创建结构,然后直接调用构造函数对应的 `php_*` 自由函数:
```cpp
auto *object = native_heap.make<php_app__user>();
php_app__user____construct(*object, args...);
```
构造函数抛出异常时,必须销毁已经初始化的字段,并从 Native Heap 活动对象集合中移除该对象。
与其他 TypePHP class 一致,`__construct()` 只能由 `new` 触发。显式调用
`$object->__construct()` 在编译期报错,避免重复初始化已经存活的 Native Object。
### 15.2 析构
`__destruct()` 与 PHP 的精确析构时机存在冲突。Tracing GC 只能保证在对象变为不可达并完成 GC 后执行资源清理,不能保证在最后一个变量离开作用域时立即执行。
Native Class 必须支持用户定义的 `__destruct()`,但采用 tracing GC 的生命周期语义:
- 对象在一次 GC 中被确认不可达,或 Native Heap shutdown 时,调用 `__destruct()`
- 每个对象最多调用一次用户析构逻辑。
- 用户代码不能显式调用 `$object->__destruct()`、`self::__destruct()` 或 `parent::__destruct()`;编译期直接报 FatalError。
- 同一个回收批次内,不同对象之间的析构顺序不保证与 PHP 一致。
- 继承链上的析构按最派生类到基类的顺序自动执行,不要求也不允许用户显式调用父类析构。
用户 `__destruct()` 不能直接作为实际 C++ destructor 的函数体。原因是 TypePHP 方法可能抛出异常、调用 ZendVM 或再次分配 Native Object;让这些行为从 C++ destructor 中发生,尤其是在异常栈展开期间,可能触发 `std::terminate()`,也无法安全处理对象复活。
因此使用两个明确分离的阶段:
```cpp
struct NativeTypeDescriptor {
void (*trace)(void *object, NativeMarker &marker);
void (*finalize)(void *object); // 调用 php_* __destruct 链
void (*destroy)(void *object); // C++ destructor + 释放存储
};
```
1. GC 将不可达对象从 active set 摘除并标记为 `finalizing`
2. 在 GC 标记/扫描临界区之外调用动态类型 descriptor 的 `finalize()`
3. `finalize()` 自动按 derived-to-base 顺序调用各层声明的 `php_*__destruct` 自由函数。
4. 用户析构完成后重新检查 roots;如果对象在析构期间被重新保存到 Native root,则保留对象,但将其标记为 `finalized`,以后不再调用用户析构。
5. 未复活对象调用 `destroy()`;实际 C++ destructor 只负责字段 RAII 和基类子对象清理,保持 `noexcept`。descriptor 已记录最派生类型,因此不依赖通过基类指针执行 `delete`,也不要求仅为销毁而给所有继承层次增加 vtable。
6. finalizer 抛出异常时,GC 必须先恢复内部状态并保证对象最终可清理,再把异常传播到当前 TypePHP 异常边界;shutdown 阶段遵循单独的不可抛出策略。
Request shutdown 会在 finalization 前后各清空一次已注册的 global/static Native root。
这是必要的:`__destruct()` 可能在 finalization 中把 `$this` 重新写入某个全局槽,但 request
heap 随后仍会整体销毁;第二次清空可防止悬空指针进入下一 request。
这种设计保留 `__destruct()` 的资源清理能力,同时避免让复杂用户代码穿过 C++ destructor。它与 PHP 的主要差异是调用时机由 Native GC 决定,而不是引用计数降为零的时刻。
### 15.3 `unset()` 与析构时机
Native 局部变量是一个由 root frame 跟踪的 `native_struct *` 槽。普通赋值只复制指针,因此多个变量可以引用同一对象。
`unset($object)``$object = null` 只把当前指针槽设为 `nullptr`,既不清零对象属性,也不影响其他别名。只有对象已经不存在其他 Native root 或 Native field 引用,并在下一次 GC 或 shutdown 中被确认不可达,才进入 finalization。这保持了 PHP 的对象身份与别名语义,但不保证 PHP 引用计数归零时的立即析构时机。
方法调用的空值检查应由编译器的 nullability 分析决定:`new`、非 nullable 参数完成入口检查后的值以及 Native `this_` 可直接解引用;nullable/global/static 或控制流合并后无法证明非空的值才生成 `UNEXPECTED(ptr == nullptr)` 运行时检查。
### 15.4 关键词转换方法
Native Class 支持 `toArray()`、`toString()`、`toInt()`、`toFloat()`、`toBool()` 等 TypePHP 关键词转换方法,但不会进入 PHPX 的动态转换 helper。编译器要求 Native Class 实际声明对应的零参数方法,并把调用直接 lowering 为 Native Call。
方法返回类型必须与关键词类型完全一致。例如 `toArray(): array`、`toInt(): int`、`toString(): string`;缺少方法、接收参数、按引用返回或返回类型不同均为编译期 FatalError。
对象条件与显式转换是两套语义。`if ($object)`、`!$object`、`$left && $right` 和
`$left || $right` 只判断 Native pointer 是否为 `nullptr`,不会调用 `toBool()`;这与
PHP 对普通对象“存在即为 true”的语义一致,也使 nullable Native pointer 可以直接作为
条件。只有显式 `(bool) $object``$object->toBool()` 才解析为 Native `toBool(): bool`
调用;没有定义该方法时在编译期报错。即使类定义的 `toBool()` 返回 `false`,一个非空
对象在 `if ($object)` 中仍为 `true`
`__toString(): string``toString(): string` 的兼容别名。对 Native Object 使用 `toString()`、`strval($object)`、`(string) $object`、字符串拼接或 `echo` 时,编译器优先使用实际声明的 `toString()`,若不存在则使用 `__toString()`
与 PHP 一致,声明合法 `__toString()` 的 Native Class 在编译期隐式满足 `Stringable`
`$native instanceof Stringable` 折叠为 `true`,但这仍不允许把 Native Object 转换或
传递为 `Stringable` Interface 值。
### 15.5 `count()` 与 `Countable`
当编译器能静态确定 Native Class 实现了 `Countable` 时,`count($nativeObject)` 等价于
`$nativeObject->count()`,并直接 lowering 为同一个 Native Call。Native Object 不会因此构造
Zend Object,也不会进入 `php::fn::count()`
仅仅声明一个名为 `count()` 的方法并不足够;Native Class 必须显式 `implements Countable`,且
实现会经过内部 Interface 签名校验。首版只支持 `count($nativeObject)` 单参数形式;带 `$mode`
的形式不进入这条 Native 特化路径。
### 15.6 Nullsafe operator
Native root 及每一个中间 receiver 都是 Native pointer 时,`?->` 使用专门的短路
lowering。每一级只执行一次 `nullptr` 判断,方法参数只在 receiver 非空后求值。最终结果
为 Native Object 时继续返回 nullable typed pointer;最终结果为 PHP 标量或 PHPX value
时,因为 PHP 语义是 `T|null`,只在结果边界装箱为 `php::Var`
Native nullsafe chain 不能在中间切换到 Zend Object 后继续;该混合对象模型链在编译期
拒绝,用户应拆成两条语句。Native Property Hook 可以作为最终的直接读取;
`isset()/empty()` 不支持 Hook 属性。
### 15.7 `json_encode()`
Native Object 没有 `zval` 表示,不能作为 `json_encode()` 或其他 PHP/ZendVM 函数的参数。编译器不会为 `json_encode()` 增加特殊 lowering,也不会隐式构造临时 Zend Object 或 DTO;`json_encode($nativeObject)` 在编译期直接报错。
需要 JSON 时,Native Class 应显式提供返回 PHP array 的 `toArray(): array`,再由用户调用:
```php
$json = json_encode($nativeObject->toArray());
```
显式转换使分配成本和对象图转换边界在源码中清晰可见,也保持“Native Object 不跨越 ZendVM 边界”的统一规则。
## 16. 首版支持边界
| 特性 | 首版建议 |
|---|---|
| 固定类型属性 | 支持 |
| string/array/object/Stream/mixed 属性 | 支持,使用对应 PHPX RAII 字段 |
| 无类型属性 | 不支持,编译期 FatalError |
| 直接属性读写 | 支持 |
| 普通成员方法 | 支持 |
| Native Object 参数/返回值 | 必须显式声明具体 Native Class;按 pointer value 传递,不复制对象 |
| non-null Native 参数 | `NativeClass $value` 在函数入口统一拒绝 `nullptr`;进入函数体后保证非空 |
| nullable Native 参数/返回值 | 支持 `?NativeClass`,以 `nullptr` 表示;成员访问必须检查或先证明非空 |
| Native 参数/返回值的 `&` | 不支持;编译期 FatalError |
| 对 Native Object 变量取引用 | 不支持;普通赋值已经共享对象身份 |
| 对 Native 属性取引用 | 仅显式声明为 `mixed` / `any` 的无约束字段支持;其他字段编译期 FatalError |
| Native variadic、union/intersection | 不支持;编译期 FatalError |
| `__construct()` | 支持 |
| `clone` / `__clone()` | 支持 |
| Getter/Setter 注解 | 支持 |
| Property Hook | 支持直接 get/set;间接写入、复合写入、引用、isset/empty 不支持 |
| Trait AST 注入 | 支持,注入完成后按普通 Native member 编译 |
| `readonly` | 不支持,编译期 FatalError;PHP readonly 是依赖 Zend 属性初始化状态的运行时机制,与 Native 固定裸字段模型不兼容 |
| `toArray()`/`toInt()` 等关键词转换 | 支持,要求 Native Class 声明零参数且返回类型完全一致的方法 |
| `toString()` / `__toString()` | 支持确定 Native Call;字符串强转、`strval()`、拼接和 `echo` 使用同一规则 |
| `count($nativeObject)` | 支持确定 Native Call;要求 Native Class 实现 `Countable`,首版限单参数形式 |
| `isset()` / `empty()` | 支持裸指针槽及纯 Native 命名属性链,逐级短路,不进入 ZendVM |
| `is_null()` | 支持 Native typed pointer,直接与 `nullptr` 比较 |
| Nullsafe `?->` | 支持纯 Native receiver chain;Native 返回保持 typed pointer,标量返回按 `T|null` 装箱 |
| `__invoke()` | 支持确定 Native Call |
| `__destruct()` | 支持,由 GC finalization 触发且每个对象最多一次 |
| Native Class 单继承 | 支持;与普通 ZendVM class 禁止互相继承 |
| Native abstract class / abstract method | 支持;生成 pure virtual thunk,具体子类在编译期完成实现检查 |
| override method | 支持,继承链同名实例方法生成 virtual dispatch thunk |
| 基于参数签名的同名方法重载 | 不支持;PHP 源码不允许在同一个类中重复声明同名方法 |
| Interface | 普通 Interface 注册到 ZendVM;Native `implements` 只做编译期契约校验,Native Object 不能转换为 Interface 值 |
| `instanceof` | 支持编译期可解析的 Native class 和 Interface,直接折叠;变量 class 不支持 |
| `===` / `!==` | 支持 Native 指针身份及与 `null` 的严格比较 |
| Native 条件的 `match` | 支持,使用与 `===` 相同的指针身份规则 |
| `==` / `!=`、大小及算术/位运算 | 不支持,编译期 FatalError;值相等应使用显式 Native 方法 |
| 一元算术/位运算、`++`/`--`、复合算术赋值、`switch` | 不支持,编译期 FatalError |
| 动态属性 | 不支持 |
| `$nativeObject->$expr()` | 不支持,只允许命名方法调用 |
| `__call()` / `__callStatic()` | 不支持;Native Call 必须在编译期解析为确定符号 |
| `__get()` / `__set()` / `__isset()` / `__unset()` | 不支持,以命名属性与 Property Hook 替代 |
| `__sleep()` / `__wakeup()` / `__serialize()` / `__unserialize()` | 不支持;Native Object 不进入 Zend 序列化系统 |
| `__set_state()` / `__debugInfo()` | 不支持;Native Object 没有相应 Zend object handler |
| Reflection | 不支持 |
| TypePHP Generator 保存或产出 Native Object | 不支持;编译期 FatalError |
| 普通函数的 Native 局部变量跨 `Fiber::suspend()` | 支持;Root Frame 注册表允许非 LIFO Fiber 生命周期 |
| `get_class()` / `get_parent_class()` / `get_called_class()` | 不支持 Native runtime introspection;使用 `self::class`、`parent::class` 或具体类名 |
| WeakReference | 不支持 |
| PHP serialize | 不支持 |
| PHP `json_encode()` | 不支持直接传入 Native Object;先显式调用 `toArray()` |
| 动态 callback | 不支持 |
| 动态 PHP/eval 使用 | 不支持 |
| 普通 PHP array 保存 Native Object | 不支持 |
| Native Object 作为 PHP array key 或 `[]` receiver | 不支持;编译期 FatalError |
| Box/Std Container 属性 | 不支持 |
| Box 保存 Native Object | 不支持 |
| 局部 Std Container 保存 Native Object | 支持具体 Native class value type;容器 Root Frame 参与 GC tracing |
| Native 元素 Std Container 转 PHP array/mixed 或作为 PHP 参数 | 不支持;裸指针不得越过 ZendVM value boundary |
| Native Class 属性循环引用 | 支持,指针字段加 Native tracing GC |
| TypePHP global/static local | 支持;ZTS 使用 thread-local request roots,RSHUTDOWN 清理 |
| global/static local 类型 | 第一次 Native 赋值固定 C++ slot 类型;后续可写入其 Native 子类或 null,不可改为基类/无关类 |
| Native Class 属性循环类型 | 支持;字段零值为 `nullptr`,类型图使用 C++ 前置声明 |
| late static binding / `new static()` | 不支持;Native Class 无运行时 `zend_class_entry`,使用 `self::`、`parent::` 或具体类名 |
## 17. 编译器目录与隔离要求
Native Class 的实现应集中放置在独立目录:
```text
src/NativeClass/
├── Analysis/
│ ├── NativeClassAnalyzer.php
│ ├── NativeEscapeAnalyzer.php
│ ├── NativeBoundaryValidator.php
│ └── NativeRootAnalyzer.php
├── CodeGen/
│ ├── NativeClassGenerator.php
│ ├── NativeMethodGenerator.php
│ ├── NativePropertyGenerator.php
│ ├── NativeTraceGenerator.php
│ └── NativeRootFrameGenerator.php
├── Model/
│ ├── NativeClassDefinition.php
│ ├── NativeFieldDefinition.php
│ └── NativeMethodDefinition.php
├── Transform/
│ ├── NativeAnnotationExpander.php
│ ├── NativeCloneLowering.php
│ └── NativePropertyHookLowering.php
├── Diagnostics/
│ └── NativeClassDiagnostic.php
└── NativeClassCompiler.php
```
隔离原则:
1. 现有普通 class 编译流程不得包含 Native Class 的具体规则。
2. 公共编译流程只负责识别显式标记,并将完整 class AST 交给 `NativeClassCompiler`
3. Native Class 的类型信息优先保存在独立 side table 中,不向现有 Class Definition 持续增加特殊状态字段。
4. Native Class 的边界检查、逃逸分析、Hook lowering 和 C++ 生成都在该目录内完成。
5. 未使用 Native Class 的项目不加载相关分析器,不增加普通编译流程的运行成本。
6. Native Class 需要的运行时支持应放入独立头文件,且只在实际使用时 include。
建议对应测试目录:
```text
tests/compiler/native-class/
```
普通 class 的测试与 Native Class 测试不得混用,以明确两套对象模型的语义边界。
## 18. 诊断原则
所有不支持的行为必须在编译期给出明确错误,不能在运行时崩溃,也不能静默退回 ZendVM。
示例:
```text
Fatal error: Native class object App\Point cannot be passed to parameter $value of type mixed
```
```text
Fatal error: Native class App\Point cannot be used with ReflectionClass
```
```text
Fatal error: Native class objects cannot be stored in a PHP array
```
诊断需要指出具体 ZendVM 边界以及可用的替代方式。
## 19. 性能原则
Native Class 的主要路径必须满足:
- 对象变量为一个裸指针。
- 普通参数传递为一次指针复制。
- 属性访问等价于 C++ 字段访问。
- 非原生 PHP 字段通过固定偏移访问对应 PHPX RAII 对象,不查找属性名称。
- 确定方法调用等价于普通 C++ 函数调用。
- 不使用原子操作。
- 不使用哈希表查找属性或方法。
- 不创建临时 Zend Object。
- 不执行运行时 class name 比较。
- 不为了兼容动态能力插入隐藏的 fallback。
若某个 PHP 特性无法满足这些要求,应优先禁止该特性,而不是降低所有 Native Class 的性能。
## 20. 建议的实施阶段
实现按以下顺序推进:
1. 固定 `#[Native] class` 语法、typed object 规则和诊断边界。
2. 建立独立 AST model、side table 和 C++ struct 生成器。
3. 建立 Native Heap、精确 tracing GC、root frame、循环回收和异常清理机制。
4. 支持构造、析构、强制属性类型、全部 PHP 字段类型、普通方法和对象参数传递。
5. 接入现有 Trait AST 注入,并支持 Getter/Setter 等编译期注解。
6. 支持单继承、override virtual thunk、Interface 编译期契约和相关类型检查。
7. 支持 Property Hook 的直接 getter/setter lowering;复合写入等动态语义编译期拒绝。
8. 支持 clone、Native Class 指针字段、循环类型依赖和构造依赖环诊断。
9. 最后评估 `json_encode()`、栈分配与逃逸分析。
每个阶段都必须先添加 PHPT/PHPUnit 测试,再实现代码。
## 21. 已确定但仍需性能验证的参数
- Native Object 永远不能转换或赋值为 Interface 类型;`implements` 只提供编译期契约
校验,首版不提供调用点特化、fat pointer 或 interface table。
- Native Heap 使用 10 MiB 首次阈值、1 MiB 最低阈值和 50% live-heap headroom。
这些约定已经固定。后续 benchmark 可以调整 GC 的内部数值,但不得改变 Native Object
不做 Interface 类型擦除、没有 Zend 表示、热路径使用裸指针 Native Call 的基本设计。