From 981b8f54d74fcbbb7afb7cf0ea2962a8b0af6ce3 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 14 Aug 2026 22:27:46 +0800 Subject: [PATCH] =?UTF-8?q?Native=20Class=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/NATIVE_CLASS_OBJECT.md | 1232 +++++++++++++++++ phpunit/code/native-class-closure-capture.php | 15 + .../code/native-class-closure-parameter.php | 14 + phpunit/code/native-class-closure-return.php | 14 + .../native-class-dynamic-magic-method.php | 10 + ...tive-class-dynamic-static-magic-method.php | 10 + phpunit/code/native-class-instanceof.php | 13 + .../code/native-class-interface-argument.php | 25 + .../native-class-interface-assignment.php | 22 + .../code/native-class-interface-property.php | 27 + .../code/native-class-interface-return.php | 20 + ...lass-internal-interface-missing-method.php | 6 + ...ive-class-internal-interface-parameter.php | 23 + ...ve-class-internal-interface-visibility.php | 10 + phpunit/code/native-class-json-encode.php | 16 + phpunit/code/native-class-keyword-missing.php | 13 + .../code/native-class-keyword-return-type.php | 16 + phpunit/code/native-class-mixed-return.php | 11 + phpunit/code/native-class-php-array.php | 12 + phpunit/code/native-class-php-property.php | 13 + .../code/native-class-readonly-property.php | 7 + phpunit/code/native-class-static-method.php | 9 + phpunit/code/native-class-static-property.php | 7 + .../native-class-std-container-property.php | 7 + ...ative-class-trait-dynamic-magic-method.php | 15 + .../code/native-class-trait-static-method.php | 15 + phpunit/code/native-class-union-signature.php | 11 + .../code/native-class-untyped-property.php | 7 + phpunit/code/native-class-untyped-return.php | 11 + .../code/native-class-zend-constructor.php | 14 + .../code/native-class-zend-inheritance.php | 6 + .../native-class-zend-native-property.php | 12 + .../src/CompileTimeAttributeRegistryTest.php | 3 +- phpunit/src/CompilerBaseApiTest.php | 3 +- phpunit/src/Entity/ClassDefTest.php | 1 + .../NativeClassAttributeLoweringTest.php | 38 + .../NativeClass/NativeClassValidationTest.php | 246 ++++ src/CompilerBase.php | 213 ++- src/Context/CompilationStateTrait.php | 21 + src/Context/FunctionContext.php | 12 +- src/Entity/ClassDef.php | 2 + src/Entity/FunctionDef.php | 4 + src/Generator/CallArgumentGenerator.php | 7 + src/Generator/ClosureGenerator.php | 26 + src/Generator/PropertyPromotion.php | 8 + src/NativeClass/NativeClassSupportTrait.php | 866 ++++++++++++ src/Optimizer/FuncCallOptimizer.php | 38 +- src/Parser/ArrayExpressionTrait.php | 4 + src/Parser/AssignOpTrait.php | 122 +- src/Parser/BinaryOpTrait.php | 34 +- src/Parser/FunctionCallTrait.php | 12 + src/Parser/MethodCallTrait.php | 88 +- src/Parser/PropertyAccessTrait.php | 47 +- src/Parser/SelectionExpressionTrait.php | 98 +- src/Parser/TypeConversionTrait.php | 15 + src/Parser/UnaryExpressionTrait.php | 5 +- src/Preprocessor.php | 85 +- .../CompileTimeAttributeRegistry.php | 1 + .../NativeClassAttributeLowering.php | 39 + src/Transform/Visitor.php | 1 + src/Translator.php | 134 +- .../NativeTypeCompatibilityTrait.php | 45 +- src/gen_stub.php | 16 + src/polyfills.php | 5 + tests/compiler/native-class/basic.phpt | 41 + tests/compiler/native-class/by-reference.phpt | 30 + .../native-class/call-argument-roots.phpt | 36 + .../native-class/chained-assignment.phpt | 22 + tests/compiler/native-class/chained-call.phpt | 32 + .../clone-and-zend-invisible.phpt | 29 + .../composite-property-types.phpt | 71 + .../native-class/declaration-order.phpt | 25 + .../native-class/destructor-inheritance.phpt | 34 + tests/compiler/native-class/gc-cycle.phpt | 37 + tests/compiler/native-class/generators.phpt | 33 + .../native-class/global-and-static.phpt | 42 + .../native-class/inherited-property-slot.phpt | 38 + tests/compiler/native-class/instanceof.phpt | 41 + .../native-class/internal-interface.phpt | 24 + .../native-class/keyword-conversions.phpt | 74 + .../compiler/native-class/magic-methods.phpt | 35 + .../native-class/non-null-parameter.phpt | 28 + .../native-class/nullable-signatures.phpt | 50 + .../native-class/phpx-properties.phpt | 43 + .../native-class/private-property-slots.phpt | 51 + .../compiler/native-class/property-hooks.phpt | 32 + .../trait-inheritance-interface.phpt | 73 + tests/compiler/native-class/unset-alias.phpt | 28 + .../native-class/value-selection.phpt | 44 + .../native-class/zend-hidden-method.phpt | 30 + tests/compiler/native-class/zero-values.phpt | 43 + 91 files changed, 4887 insertions(+), 61 deletions(-) create mode 100644 docs/NATIVE_CLASS_OBJECT.md create mode 100644 phpunit/code/native-class-closure-capture.php create mode 100644 phpunit/code/native-class-closure-parameter.php create mode 100644 phpunit/code/native-class-closure-return.php create mode 100644 phpunit/code/native-class-dynamic-magic-method.php create mode 100644 phpunit/code/native-class-dynamic-static-magic-method.php create mode 100644 phpunit/code/native-class-instanceof.php create mode 100644 phpunit/code/native-class-interface-argument.php create mode 100644 phpunit/code/native-class-interface-assignment.php create mode 100644 phpunit/code/native-class-interface-property.php create mode 100644 phpunit/code/native-class-interface-return.php create mode 100644 phpunit/code/native-class-internal-interface-missing-method.php create mode 100644 phpunit/code/native-class-internal-interface-parameter.php create mode 100644 phpunit/code/native-class-internal-interface-visibility.php create mode 100644 phpunit/code/native-class-json-encode.php create mode 100644 phpunit/code/native-class-keyword-missing.php create mode 100644 phpunit/code/native-class-keyword-return-type.php create mode 100644 phpunit/code/native-class-mixed-return.php create mode 100644 phpunit/code/native-class-php-array.php create mode 100644 phpunit/code/native-class-php-property.php create mode 100644 phpunit/code/native-class-readonly-property.php create mode 100644 phpunit/code/native-class-static-method.php create mode 100644 phpunit/code/native-class-static-property.php create mode 100644 phpunit/code/native-class-std-container-property.php create mode 100644 phpunit/code/native-class-trait-dynamic-magic-method.php create mode 100644 phpunit/code/native-class-trait-static-method.php create mode 100644 phpunit/code/native-class-union-signature.php create mode 100644 phpunit/code/native-class-untyped-property.php create mode 100644 phpunit/code/native-class-untyped-return.php create mode 100644 phpunit/code/native-class-zend-constructor.php create mode 100644 phpunit/code/native-class-zend-inheritance.php create mode 100644 phpunit/code/native-class-zend-native-property.php create mode 100644 phpunit/src/NativeClass/NativeClassAttributeLoweringTest.php create mode 100644 phpunit/src/NativeClass/NativeClassValidationTest.php create mode 100644 src/NativeClass/NativeClassSupportTrait.php create mode 100644 src/Transform/NativeClassAttributeLowering.php create mode 100644 tests/compiler/native-class/basic.phpt create mode 100644 tests/compiler/native-class/by-reference.phpt create mode 100644 tests/compiler/native-class/call-argument-roots.phpt create mode 100644 tests/compiler/native-class/chained-assignment.phpt create mode 100644 tests/compiler/native-class/chained-call.phpt create mode 100644 tests/compiler/native-class/clone-and-zend-invisible.phpt create mode 100644 tests/compiler/native-class/composite-property-types.phpt create mode 100644 tests/compiler/native-class/declaration-order.phpt create mode 100644 tests/compiler/native-class/destructor-inheritance.phpt create mode 100644 tests/compiler/native-class/gc-cycle.phpt create mode 100644 tests/compiler/native-class/generators.phpt create mode 100644 tests/compiler/native-class/global-and-static.phpt create mode 100644 tests/compiler/native-class/inherited-property-slot.phpt create mode 100644 tests/compiler/native-class/instanceof.phpt create mode 100644 tests/compiler/native-class/internal-interface.phpt create mode 100644 tests/compiler/native-class/keyword-conversions.phpt create mode 100644 tests/compiler/native-class/magic-methods.phpt create mode 100644 tests/compiler/native-class/non-null-parameter.phpt create mode 100644 tests/compiler/native-class/nullable-signatures.phpt create mode 100644 tests/compiler/native-class/phpx-properties.phpt create mode 100644 tests/compiler/native-class/private-property-slots.phpt create mode 100644 tests/compiler/native-class/property-hooks.phpt create mode 100644 tests/compiler/native-class/trait-inheritance-interface.phpt create mode 100644 tests/compiler/native-class/unset-alias.phpt create mode 100644 tests/compiler/native-class/value-selection.phpt create mode 100644 tests/compiler/native-class/zend-hidden-method.phpt create mode 100644 tests/compiler/native-class/zero-values.phpt diff --git a/docs/NATIVE_CLASS_OBJECT.md b/docs/NATIVE_CLASS_OBJECT.md new file mode 100644 index 00000000..2da5131f --- /dev/null +++ b/docs/NATIVE_CLASS_OBJECT.md @@ -0,0 +1,1232 @@ +# 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 编译流程。 + +## 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++ 成员函数模型。 + +全程序分析发现继承链中存在同名的 public/protected instance method 时,为该方法族生成内部 virtual dispatch thunk: + +```cpp +struct php_app__base; +php::Str php_app__base__name(php_app__base &this_); + +struct php_app__base { + virtual php::Str __native_dispatch_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_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 检查。 + +这会提供继承所必需的有限单分派多态,但不支持变量方法名、运行时 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` | `php::Var` | 保存任意 PHP zval | +| union/intersection/nullable | `php::Var` | 与普通类属性使用同一类型描述和运行时写入检查 | +| BigInt/BigFloat/Decimal | 对应 PHPX 高精度类型 | 直接使用已有 RAII 类型 | + +`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 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.2 赋值检查 + +已确定的赋值在编译期检查。来自 `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(); +auto *b = a; +b->x = 10; +``` + +因此 `$a` 和 `$b` 仍指向同一个对象,不会因为采用 C++ `struct` 而变成值复制。 + +Native Class Object 不使用 `std::shared_ptr`。`std::shared_ptr` 的控制块、原子引用计数和循环引用问题与本特性的极致性能目标不符。 + +### 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 相互指向不会形成引用计数循环,也不会产生永久泄漏。裸指针字段本身没有析构动作。 + +但如果循环中的每一条边都是 non-nullable,并且都要求在各自构造函数返回前完成初始化,就会形成无法构造的初始化死结:创建 A 需要 B,而创建 B 又需要 A。 + +首版不引入“未初始化对象发布”或特殊的两阶段构造 API。循环对象图必须至少使用一条 nullable 边打破初始化环: + +```php +$a = new A(); +$b = new B($a); +$a->b = $b; +``` + +如果编译器发现一个 Native Class 构造依赖环全部由必须在构造阶段赋值的 non-nullable 属性组成,应抛出 FatalError,并提示将至少一条边声明为 nullable。该检查针对构造初始化依赖,而不是简单禁止类型循环。 + +## 8. 内存与生命周期 + +Request Arena 只能作为分配器和 Request Shutdown 兜底,不能作为唯一生命周期机制。对于常驻 CLI、HTTP Server 或单个超长 request,如果对象只能在 Request Shutdown 释放,内存会持续增长。 + +Native Class Object 应使用独立的、非移动、精确 tracing GC。本文将该运行时称为 Native Heap。 + +### 8.1 Native Heap + +Native Heap 使用 Arena/chunk/free-list 提供快速内存分配,但每个对象都具有位于 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(); +``` + +GC header 不属于生成的 C++ struct,也不会改变属性偏移。用户可见对象变量仍然只是一个 `native_struct *`。 + +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(ptr); + visitor.mark(object->b); +} +``` + +`php::Str`、`php::Array`、`php::Object`、`php::Var` 和 Stream 字段由 Zend 引用计数管理,但它们不能反向保存 Native Object,因此无需由 Native GC 深入扫描。 + +禁止 Native Object 进入 PHP Array、Box、Std Container 和 Zend Object,是保证 Native 对象图封闭且可精确遍历的重要条件。 + +### 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 分配量超过自适应阈值。 +- Native 对象数量超过阈值。 +- 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`、`Member`、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: +- BDWGC: +- Oilpan standalone library: +- Oilpan C++ GC 设计: +- MMTk plans/bindings 状态: +- MMTk VM porting guide: + +#### 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` 写入;增量/并发 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`,heap pointer 使用 `Member`,并提供 `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` 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 +function replace(Point &$point): void; +``` + +近似生成: + +```cpp +void php_replace(php_app__point *&point); +``` + +返回 Native Object 时返回指针: + +```cpp +php_app__point *php_create_point(); +``` + +非 nullable class 参数在函数入口执行一次空指针检查。确定非空的成员访问不应重复检查。nullable class 使用相同指针表示,`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 的闭包中。 +- 作为 `call_user_func()` 等动态 callback 的 receiver。 +- 保存到 ZendVM 全局变量或对象属性中。 + +Box 和 Std Container 不能保存 Native Object,也不能作为 Native Class 属性。普通 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、参数与返回类型及属性读写约束是否兼容。 +- 在 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); +``` + +复合写入必须显式展开,并严格保持 PHP 从左到右的求值顺序: + +```php +$user->count += getValue(); +``` + +近似生成: + +```cpp +auto tmp_value = getValue(); +auto tmp_current = php_app__user__get_count(*user); +php_app__user__set_count(*user, tmp_current + tmp_value); +``` + +带 Hook 的属性禁止: + +- 取引用。 +- 引用返回。 +- 返回底层属性 slot。 +- 使用 `int_ref`、`float_ref` 等引用优化。 +- 绕过 Hook 直接写入 backing field。 + +Native Property Hook 只有编译期语义,不生成 Zend Property Hook 元数据。 + +## 14. Clone + +`clone` 可以支持,但必须由编译器生成字段级浅复制,不能无条件依赖 C++ 默认 copy constructor。 + +```php +$copy = clone $source; +``` + +近似生成: + +```cpp +auto *copy = native_heap.make(); +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()`。 + +包含不可复制字段的 Native Class 必须显式禁止 clone;对它使用 `clone` 时编译期报错。 + +## 15. 构造和析构 + +### 15.1 构造 + +`new` 在 Native Heap 中创建结构,然后直接调用构造函数对应的 `php_*` 自由函数: + +```cpp +auto *object = native_heap.make(); +php_app__user____construct(*object, args...); +``` + +构造函数抛出异常时,必须销毁已经初始化的字段,并从 Native Heap 活动对象集合中移除该对象。 + +### 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 阶段遵循单独的不可抛出策略。 + +这种设计保留 `__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。 + +`__toString(): string` 是 `toString(): string` 的兼容别名。对 Native Object 使用 `toString()`、`strval($object)`、`(string) $object`、字符串拼接或 `echo` 时,编译器优先使用实际声明的 `toString()`,若不存在则使用 `__toString()`。 + +### 15.5 `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 | +| 直接属性读写 | 支持 | +| 普通成员方法 | 支持 | +| `__construct()` | 支持 | +| `clone` / `__clone()` | 支持 | +| Getter/Setter 注解 | 支持 | +| Property Hook | 支持 | +| Trait AST 注入 | 支持,注入完成后按普通 Native member 编译 | +| `readonly` | 不支持,编译期 FatalError;PHP readonly 是依赖 Zend 属性初始化状态的运行时机制,与 Native 固定裸字段模型不兼容 | +| `toArray()`/`toInt()` 等关键词转换 | 支持,要求 Native Class 声明零参数且返回类型完全一致的方法 | +| `toString()` / `__toString()` | 支持确定 Native Call;字符串强转、`strval()`、拼接和 `echo` 使用同一规则 | +| `__invoke()` | 支持确定 Native Call | +| `__destruct()` | 支持,由 GC finalization 触发且每个对象最多一次 | +| Native Class 单继承 | 支持;与普通 ZendVM class 禁止互相继承 | +| override method | 支持,继承链同名实例方法生成 virtual dispatch thunk | +| 基于参数签名的同名方法重载 | 不支持;PHP 源码不允许在同一个类中重复声明同名方法 | +| Interface | 普通 Interface 注册到 ZendVM;Native `implements` 只做编译期契约校验,Native Object 不能转换为 Interface 值 | +| `instanceof` | 支持编译期可解析的 Native class 和 Interface,直接折叠;变量 class 不支持 | +| 动态属性 | 不支持 | +| `$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 | 不支持 | +| WeakReference | 不支持 | +| PHP serialize | 不支持 | +| PHP `json_encode()` | 不支持直接传入 Native Object;先显式调用 `toArray()` | +| 动态 callback | 不支持 | +| 动态 PHP/eval 使用 | 不支持 | +| 普通 PHP array 保存 Native Object | 不支持 | +| Box/Std Container 属性 | 不支持 | +| Box/Std Container 保存 Native Object | 不支持 | +| Native Class 属性循环引用 | 支持,指针字段加 Native tracing GC | +| TypePHP global/static local | 支持;ZTS 使用 thread-local request roots,RSHUTDOWN 清理 | +| 全 non-nullable 构造依赖环 | 不支持,至少需要一条 nullable 边 | + +## 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 与复合写入 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 的基本设计。 diff --git a/phpunit/code/native-class-closure-capture.php b/phpunit/code/native-class-closure-capture.php new file mode 100644 index 00000000..08e8cb48 --- /dev/null +++ b/phpunit/code/native-class-closure-capture.php @@ -0,0 +1,15 @@ +value; + }; +} diff --git a/phpunit/code/native-class-closure-return.php b/phpunit/code/native-class-closure-return.php new file mode 100644 index 00000000..bb10077c --- /dev/null +++ b/phpunit/code/native-class-closure-return.php @@ -0,0 +1,14 @@ +value(); +} + +function main(): void +{ + consumeNativeInterfaceArgument(new NativeInterfaceArgumentValue()); +} diff --git a/phpunit/code/native-class-interface-assignment.php b/phpunit/code/native-class-interface-assignment.php new file mode 100644 index 00000000..f6f3908b --- /dev/null +++ b/phpunit/code/native-class-interface-assignment.php @@ -0,0 +1,22 @@ +value = new NativeInterfacePropertyValue(); +} diff --git a/phpunit/code/native-class-interface-return.php b/phpunit/code/native-class-interface-return.php new file mode 100644 index 00000000..a911f273 --- /dev/null +++ b/phpunit/code/native-class-interface-return.php @@ -0,0 +1,20 @@ +toInt(); +} diff --git a/phpunit/code/native-class-keyword-return-type.php b/phpunit/code/native-class-keyword-return-type.php new file mode 100644 index 00000000..2b944df0 --- /dev/null +++ b/phpunit/code/native-class-keyword-return-type.php @@ -0,0 +1,16 @@ +toArray(); +} diff --git a/phpunit/code/native-class-mixed-return.php b/phpunit/code/native-class-mixed-return.php new file mode 100644 index 00000000..1cf29afe --- /dev/null +++ b/phpunit/code/native-class-mixed-return.php @@ -0,0 +1,11 @@ +value = new NativePropertyValue(); +} diff --git a/phpunit/code/native-class-readonly-property.php b/phpunit/code/native-class-readonly-property.php new file mode 100644 index 00000000..587c597c --- /dev/null +++ b/phpunit/code/native-class-readonly-property.php @@ -0,0 +1,7 @@ +assertSame($expected, CompileTimeAttributeRegistry::names()); @@ -22,6 +22,7 @@ final class CompileTimeAttributeRegistryTest extends TestCase } $this->assertNotContains('NoExport', CompileTimeAttributeRegistry::names(true)); $this->assertNotContains('WasmExport', CompileTimeAttributeRegistry::names(true)); + $this->assertNotContains('Native', CompileTimeAttributeRegistry::names(true)); $this->assertContains('Getter', CompileTimeAttributeRegistry::names(true)); $this->assertContains('Override', CompileTimeAttributeRegistry::names(true)); $this->assertSame( diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 7b2ae76c..d51f4275 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -1141,8 +1141,9 @@ YAML); file_get_contents($this->compiler->getArgInfoHeaderFile($file)), ); foreach (\TypePhp\Transform\CompileTimeAttributeRegistry::names() as $attribute) { + $needle = $attribute === 'Native' ? '#[Native' : $attribute; $this->assertStringNotContainsString( - $attribute, + $needle, file_get_contents($this->compiler->getArgInfoHeaderFile($file)), ); } diff --git a/phpunit/src/Entity/ClassDefTest.php b/phpunit/src/Entity/ClassDefTest.php index c449d24d..0c4fa894 100644 --- a/phpunit/src/Entity/ClassDefTest.php +++ b/phpunit/src/Entity/ClassDefTest.php @@ -26,6 +26,7 @@ class ClassDefTest extends TestCase $this->assertFalse($class->enum); $this->assertFalse($class->requireCtor); $this->assertFalse($class->inheritedFromInternalClass); + $this->assertFalse($class->nativeObject); $this->assertNull($class->trait); } diff --git a/phpunit/src/NativeClass/NativeClassAttributeLoweringTest.php b/phpunit/src/NativeClass/NativeClassAttributeLoweringTest.php new file mode 100644 index 00000000..fcb4684d --- /dev/null +++ b/phpunit/src/NativeClass/NativeClassAttributeLoweringTest.php @@ -0,0 +1,38 @@ +createForNewestSupportedVersion(); + $nodes = $parser->parse('addVisitor(new NameResolver(null, ['replaceNodes' => false])); + $traverser->addVisitor(new NativeClassAttributeLowering()); + $nodes = $traverser->traverse($nodes); + + $this->assertInstanceOf(Class_::class, $nodes[0]); + $this->assertTrue(NativeClassAttributeLowering::isNative($nodes[0])); + $this->assertSame([], $nodes[0]->attrGroups); + } + + public function testLeavesOrdinaryClassUnmarked(): void + { + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $nodes = $parser->parse('addVisitor(new NativeClassAttributeLowering()); + $nodes = $traverser->traverse($nodes); + + $this->assertFalse(NativeClassAttributeLowering::isNative($nodes[0])); + } +} diff --git a/phpunit/src/NativeClass/NativeClassValidationTest.php b/phpunit/src/NativeClass/NativeClassValidationTest.php new file mode 100644 index 00000000..7ceea4a8 --- /dev/null +++ b/phpunit/src/NativeClass/NativeClassValidationTest.php @@ -0,0 +1,246 @@ +expectException(TestError::class); + $this->expectExceptionMessage('Native class properties must declare a type'); + $this->compile('native-class-untyped-property.php'); + } + + public function testRejectsStaticProperty(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native class static properties are not supported'); + $this->compile('native-class-static-property.php'); + } + + public function testRejectsInheritanceAcrossObjectModels(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native and ZendVM-backed classes cannot inherit from each other'); + $this->compile('native-class-zend-inheritance.php'); + } + + public function testRejectsStaticMethod(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native class static methods are not supported'); + $this->compile('native-class-static-method.php'); + } + + public function testRejectsReadonlyPropertyUntilNativeWriteStateIsImplemented(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native class readonly properties are not supported'); + $this->compile('native-class-readonly-property.php'); + } + + public function testRejectsStdContainerProperty(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native class properties cannot use Std Container types'); + $this->compile('native-class-std-container-property.php'); + } + + public function testRejectsDynamicInstanceofBecauseNativeClassesHaveNoRuntimeTypeLookup(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Dynamic instanceof is not supported for native objects'); + $this->compile('native-class-instanceof.php'); + } + + public function testRejectsNativeObjectStoredInPhpArray(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be stored in PHP arrays'); + $this->compile('native-class-php-array.php'); + } + + public function testRejectsNativeObjectStoredInPhpObjectProperty(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be stored in PHP arrays, PHP object properties'); + $this->compile('native-class-php-property.php'); + } + + public function testRejectsNativeTypedPropertyOnZendObject(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native object types can only be used as properties of native classes'); + $this->compile('native-class-zend-native-property.php'); + } + + public function testRejectsNativeObjectCapturedByClosure(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be captured by Zend closures'); + $this->compile('native-class-closure-capture.php'); + } + + public function testRejectsNativeObjectClosureParameter(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Zend closures cannot declare native object parameters or return types'); + $this->compile('native-class-closure-parameter.php'); + } + + public function testRejectsNativeObjectReturnedByUntypedClosure(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Zend closures cannot return native objects'); + $this->compile('native-class-closure-return.php'); + } + + public function testRejectsNativeObjectParameterOnZendConstructor(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Zend-backed constructors cannot accept or return native objects'); + $this->compile('native-class-zend-constructor.php'); + } + + public function testRejectsUnsupportedNativeObjectUnion(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native object types cannot be combined with other union or intersection members'); + $this->compile('native-class-union-signature.php'); + } + + public function testRejectsIncorrectNativeKeywordReturnType(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('must return exactly `array`'); + $this->compile('native-class-keyword-return-type.php'); + } + + public function testRejectsMissingNativeKeywordMethod(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('must define `toInt()`'); + $this->compile('native-class-keyword-missing.php'); + } + + public function testRejectsNativeObjectPassedToJsonEncode(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot cross a dynamic PHP/ZendVM call boundary'); + $this->compile('native-class-json-encode.php'); + } + + public function testRejectsNativeObjectFromUntypedReturn(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native object return values require an explicit native class return type'); + $this->compile('native-class-untyped-return.php'); + } + + public function testRejectsNativeObjectFromMixedReturn(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native object return values require an explicit native class return type'); + $this->compile('native-class-mixed-return.php'); + } + + public function testRejectsNativeObjectPassedToInterfaceParameter(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be converted to interface `NativeInterfaceArgumentContract`'); + $this->compile('native-class-interface-argument.php'); + } + + public function testRejectsNativeObjectAssignedToInterfaceVariable(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be assigned to interface-typed variables'); + $this->compile('native-class-interface-assignment.php'); + } + + public function testRejectsNativeObjectAssignedToInterfaceProperty(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be assigned to interface-typed properties'); + $this->compile('native-class-interface-property.php'); + } + + public function testRejectsNativeObjectReturnedAsInterface(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot be returned as interface `NativeInterfaceReturnContract`'); + $this->compile('native-class-interface-return.php'); + } + + public function testRejectsDynamicMagicMethod(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native classes do not support dynamic magic method `__get()`'); + $this->compile('native-class-dynamic-magic-method.php'); + } + + public function testRejectsDynamicStaticMagicMethodBeforeGenericStaticDiagnostic(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native classes do not support dynamic magic method `__callStatic()`'); + $this->compile('native-class-dynamic-static-magic-method.php'); + } + + public function testRejectsDynamicMagicMethodInjectedByTrait(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native classes do not support dynamic magic method `__serialize()`'); + $this->compile('native-class-trait-dynamic-magic-method.php'); + } + + public function testDynamicMagicMethodDenyListIsComplete(): void + { + $trait = new \ReflectionClass(\TypePhp\NativeClass\NativeClassSupportTrait::class); + $constant = $trait->getReflectionConstant('UNSUPPORTED_NATIVE_MAGIC_METHODS'); + $this->assertNotFalse($constant); + $this->assertSame([ + '__call', + '__callstatic', + '__get', + '__set', + '__isset', + '__unset', + '__sleep', + '__wakeup', + '__serialize', + '__unserialize', + '__set_state', + '__debuginfo', + ], array_keys($constant->getValue())); + } + + public function testRejectsStaticMethodInjectedByTrait(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native class static methods are not supported'); + $this->compile('native-class-trait-static-method.php'); + } + + public function testRejectsMissingInternalInterfaceMethod(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('must implement method `Countable::count()`'); + $this->compile('native-class-internal-interface-missing-method.php'); + } + + public function testRejectsNonPublicInternalInterfaceMethod(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('must be compatible with `Countable::count()`'); + $this->compile('native-class-internal-interface-visibility.php'); + } + + public function testRejectsNarrowedInternalInterfaceParameter(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('must be compatible with `ArrayAccess::offsetExists()`'); + $this->compile('native-class-internal-interface-parameter.php'); + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 2f033190..6113c7ab 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -82,6 +82,7 @@ use TypePhp\Resolver\Reflection; use TypePhp\Symbol\SymbolRepository; use TypePhp\TypeSystem\CompositeTypeCheckerTrait; use TypePhp\TypeSystem\NativeTypeCompatibilityTrait; +use TypePhp\NativeClass\NativeClassSupportTrait; use PhpParser\Modifiers; use PhpParser\Node; use PhpParser\Node\ArrayItem; @@ -102,6 +103,7 @@ class CompilerBase implements PropertyAccessContext use CompilerDiagnosticTrait; use CompilationStateTrait; use NativeTypeCompatibilityTrait; + use NativeClassSupportTrait; use NativeBuildConfigurationTrait; use PythonModuleTrait; use DeclarationSymbolTrait; @@ -433,6 +435,10 @@ class CompilerBase implements PropertyAccessContext 'GLOBALS' => Type::ARRAY, ]; protected array $globalVars = []; + /** @var array Global/static Native pointer slot => class name. */ + protected array $nativeGlobalObjects = []; + /** @var array Request-reset initialization flags for Native static locals. */ + protected array $nativeStaticInitializers = []; protected bool $nativeTypes = false; protected bool $decimalTypes = false; protected bool $bigintTypes = false; @@ -1845,9 +1851,7 @@ class CompilerBase implements PropertyAccessContext { $lines = []; foreach ($v->exprs as $expr) { - $type = $this->detectTypeOfExpr($expr); - $parsed = $this->convertExprToStringByType($this->parseExprAsValue($expr), $type); - $lines[] = 'php::echo(' . $parsed . ');'; + $lines[] = 'php::echo(' . $this->parseExprToString($expr) . ');'; } return implode("\n" . $this->getIndent(), $lines); @@ -1875,9 +1879,27 @@ class CompilerBase implements PropertyAccessContext protected function detectClassOfExpr(NodeAbstract $expr): string { + if ($expr instanceof Expr\Clone_) { + return $this->detectClassOfExpr($expr->expr); + } if ($expr instanceof Expr\Closure || $expr instanceof Expr\ArrowFunction) { return 'Closure'; } + if ($expr instanceof Expr\Ternary) { + return $this->getCommonNativeObjectExpressionClass([ + $expr->if ?? $expr->cond, + $expr->else, + ]); + } + if ($expr instanceof Expr\Match_) { + return $this->getCommonNativeObjectExpressionClass(array_map( + static fn (Node\MatchArm $arm): Expr => $arm->body, + $expr->arms, + )); + } + if ($expr instanceof Expr\BinaryOp\Coalesce) { + return $this->getCommonNativeObjectExpressionClass([$expr->left, $expr->right]); + } if ($expr instanceof Expr\MethodCall && $this->isNamedMethod($expr->name)) { $keywordType = $this->findKeywordMethod($this->parseIdentifier($expr->name)); if ($keywordType !== null && $keywordType !== Type::OBJECT) { @@ -1914,6 +1936,18 @@ class CompilerBase implements PropertyAccessContext return $this->getObjectType($object); } } + if ($expr instanceof Expr\PropertyFetch && $this->isIdExpr($expr->name)) { + $receiverClass = $this->detectClassOfExpr($expr->var); + if ($this->isNativeObjectClass($receiverClass)) { + $property = $this->findNativeObjectProperty($receiverClass, $expr->name->toString()); + if ($property !== null + && $property->type === Type::OBJECT + && $this->isNativeObjectClass($property->class) + ) { + return $property->class; + } + } + } if ($this->isArrayDimFetch($expr) and $this->isStdContainerExpr($expr)) { if ($this->isStdArrayExpr($expr)) { if (!$expr->hasAttribute('stdArrayDimFetch')) { @@ -2256,6 +2290,25 @@ class CompilerBase implements PropertyAccessContext } // 实际函数的返回值 $type = $this->detectTypeOfExpr($v->expr); + $nativeExpressionClass = $this->detectClassOfExpr($v->expr); + if ($this->context->inClosure && $this->isNativeObjectClass($nativeExpressionClass)) { + $this->fatalError($v, 'Zend closures cannot return native objects'); + } + if (!$this->context->inClosure && $this->isNativeObjectClass($nativeExpressionClass)) { + $declaredReturnClass = $this->getReturnClass(); + if (!$this->isNativeObjectClass($declaredReturnClass)) { + if ($declaredReturnClass !== '' && $this->isInterface($declaredReturnClass)) { + $this->fatalError( + $v, + "Native objects cannot be returned as interface `{$declaredReturnClass}`", + ); + } + $this->fatalError( + $v, + 'Native object return values require an explicit native class return type', + ); + } + } if ($this->isCurrentConstructor() && !$this->context->inClosure) { $this->fatalError($v, 'Method `' . $this->getCurrentMethodDisplayName() . '()` cannot return a value'); } @@ -2276,6 +2329,27 @@ class CompilerBase implements PropertyAccessContext 'return value' ); } + if (!$this->context->inClosure + && ($nativeReturnClass = $this->getReturnClass()) !== '' + && $this->isNativeObjectClass($nativeReturnClass) + ) { + if ($this->isNull($v->expr)) { + if (!$this->functionDef->returnNullable) { + $this->fatalError($v, "The return type is non-nullable native object `{$nativeReturnClass}`"); + } + return 'return nullptr;'; + } + $objectClass = $this->detectClassOfExpr($v->expr); + if ($objectClass === '' || !$this->isNativeObjectClass($objectClass) + || !$this->isObjectClassStaticallyAssignableTo($objectClass, $nativeReturnClass) + ) { + $this->fatalError( + $v, + "The return type is native object `{$nativeReturnClass}`, `{$objectClass}` given" + ); + } + return 'return ' . $this->parseExprAsValue($v->expr) . ';'; + } $expr = $this->parseExprAsValue($v->expr); $returnType = $this->getReturnType(); @@ -3582,6 +3656,27 @@ class CompilerBase implements PropertyAccessContext . $constructor['className'] . '::__construct()' ); } + if ($this->isNativeObjectClass($className)) { + $cppClass = $this->getNativeObjectCppName($className); + $descriptor = $this->getNativeObjectDescriptorName($className); + if ($constructor === null) { + if ($expr->args !== []) { + $this->fatalError($expr, "Native class `{$className}` does not have a constructor"); + } + return 'php::nativeConstruct<' . $cppClass . '>(' . $descriptor + . ', [&](auto &this_) { ' . $cppClass . '__initialize(this_); })'; + } + $nativeCtor = $this->getNativeMethod($expr, $className, '__construct'); + if ($nativeCtor === false) { + $this->fatalError($expr, "Native constructor `{$className}::__construct()` cannot be resolved"); + } + $args = $expr->args === [] + ? '' + : ', ' . $this->parseNativeCallArgs($expr->args, $nativeCtor); + return 'php::nativeConstruct<' . $cppClass . '>(' . $descriptor + . ', [&](auto &this_) { ' . $cppClass . '__initialize(this_); ' + . self::PREFIX . $nativeCtor . '(this_' . $args . '); })'; + } $cePtr = $this->getClassEntryPtr($className); } } else { @@ -3599,12 +3694,61 @@ class CompilerBase implements PropertyAccessContext protected function parseClone(Expr\Clone_ $expr): string { $this->assertExprCanBeUsedAsValue($expr->expr, 'clone operand'); + $class = $this->detectClassOfExpr($expr->expr); + if ($this->isNativeObjectClass($class)) { + if (!$this->isVarExpr($expr->expr)) { + $this->fatalError($expr, 'Native object clone currently requires a typed variable'); + } + $source = $this->parseIdentifier($expr->expr); + $cpp = $this->getNativeObjectCppName($class); + $descriptor = $this->getNativeObjectDescriptorName($class); + $classDef = $this->getClass($class); + $initializer = ''; + if ($classDef->hasMethod('__clone')) { + $clone = self::PREFIX . $this->getNativeName('__clone', $classDef->namespace, $classDef->name); + $initializer = $clone . '(this_); '; + } + return 'php::nativeClone<' . $cpp . '>(' . $descriptor . ', ' + . $this->getNativeObjectReceiver($source) + . ', [&](auto &this_) { ' . $initializer . '})'; + } return 'php::clone(' . $this->parseExprAsValue($expr->expr) . ')'; } protected function parseInstanceof(Expr\Instanceof_ $expr): string { $this->assertExprCanBeUsedAsValue($expr->expr, 'instanceof operand'); + $valueClass = $this->detectClassOfExpr($expr->expr); + $targetClass = $this->resolveCompileTimeInstanceofClass($expr->class); + $valueIsNative = $this->isNativeObjectClass($valueClass); + $targetIsNative = $targetClass !== null && $this->isNativeObjectClass($targetClass); + $targetIsInterface = $targetClass !== null && $this->isInterface($targetClass); + + if ($valueIsNative && $targetClass === null) { + $this->fatalError($expr, 'Dynamic instanceof is not supported for native objects'); + } + if ($valueIsNative || $targetIsNative) { + $result = $valueIsNative + && ($targetIsNative || $targetIsInterface) + && $this->isObjectClassStaticallyAssignableTo($valueClass, $targetClass); + if (!$result + && $valueIsNative + && $targetIsNative + && $this->isObjectClassStaticallyAssignableTo($targetClass, $valueClass) + ) { + $this->fatalError( + $expr, + 'Native instanceof cannot be resolved from the static base-class type' + ); + } + + // Folding must not discard constructor/function side effects from + // a non-variable left operand. Native objects cannot cross into a + // Variant, so sequence the original pointer expression directly. + $value = $this->parseExprAsValue($expr->expr); + return '(static_cast(' . $value . '), ' . ($result ? 'true' : 'false') . ')'; + } + if ($this->isNameExpr($expr->class)) { $value = $this->parseExprAsValue($expr->expr); $classPtr = $this->resolveInstanceofClassPtr($expr->class); @@ -3619,6 +3763,21 @@ class CompilerBase implements PropertyAccessContext } } + protected function resolveCompileTimeInstanceofClass(NodeAbstract $class): ?string + { + if (!$this->isNameExpr($class)) { + return null; + } + $name = $this->parseIdentifier($class); + if ($name === 'self' || $name === 'static') { + return $this->getFullClassName(); + } + if ($name === 'parent') { + return $this->classDef?->extends ?? ''; + } + return $this->getNamespacedClassName($name); + } + protected function resolveInstanceofClassPtr(NodeAbstract $class): string { $className = $this->parseIdentifier($class); @@ -3672,7 +3831,10 @@ class CompilerBase implements PropertyAccessContext $this->addGlobalVar($name, Type::VAR); } if (!$this->hasScopeGlobalVar($name)) { - $this->addScopeGlobalVar($name, Type::VAR); + $this->addScopeGlobalVar($name, $this->globalVars[$name]); + } + if (isset($this->nativeGlobalObjects[$name])) { + $this->addNativeObject($name, $this->nativeGlobalObjects[$name]); } } return ''; @@ -3710,11 +3872,24 @@ class CompilerBase implements PropertyAccessContext $this->assertExprCanBeUsedAsValue($var->default, 'static variable default value'); } $globalVar = $this->addStaticVar($var->var, $varName, $type); + if ($var->default) { + $class = $this->detectClassOfExpr($var->default); + if ($this->isNativeObjectClass($class)) { + $this->promoteGlobalOrStaticToNativeObject($varName, $class); + } + } - $list[] = Type::VAR . ' &' . $varName . ' = ' . $this->escapeGlobalVar($globalVar) . ';'; + $list[] = 'auto &' . $varName . ' = ' . $this->escapeGlobalVar($globalVar) . ';'; if ($var->default) { $initState = self::STATIC_VAR . $varName . '_initialized'; - $initCode = $this->getIndent() . 'static bool ' . $initState . ' = false;'; + if (isset($this->nativeGlobalObjects[$globalVar])) { + $flag = $globalVar . '__initialized'; + $this->nativeStaticInitializers[$flag] = true; + $initState = $this->escapeGlobalVar($flag); + $initCode = ''; + } else { + $initCode = $this->getIndent() . 'static THREAD_LOCAL bool ' . $initState . ' = false;'; + } $initCode .= $this->getIndent() . "if (!{$initState}) { \n"; $this->indentLevel++; $initCode .= $this->getIndent() . "{$initState} = true;\n"; @@ -4450,7 +4625,9 @@ class CompilerBase implements PropertyAccessContext $code .= Type::VAR . ' ' . $name . ';'; } else { $code .= $type . ' ' . $name; - if ($type === Type::INT or $type === Type::FLOAT or $type === Type::BOOL) { + if ($this->isNativeObjectVar($name)) { + $code .= ' = nullptr'; + } elseif ($type === Type::INT or $type === Type::FLOAT or $type === Type::BOOL) { $code .= ' = 0'; } $code .= ';'; @@ -4476,6 +4653,23 @@ class CompilerBase implements PropertyAccessContext . ', this_);' . PHP_EOL; } $code .= $this->genLocalVarDecl($this->context->localVars); + if ($this->context->nativeObjects !== []) { + $rootSlots = []; + foreach ($this->context->nativeObjects as $name => $_class) { + if ($name === 'this_') { + $code .= $this->getIndent() . 'auto *_native_this_root = &this_;' . PHP_EOL; + $rootSlots[] = 'reinterpret_cast(&_native_this_root)'; + } elseif ($this->hasLocalVar($name)) { + $rootSlots[] = 'reinterpret_cast(&' . $name . ')'; + } + } + if ($rootSlots !== []) { + $code .= $this->getIndent() . 'php::NativeRootSlot _native_root_slots[] = {' + . implode(', ', $rootSlots) . '};' . PHP_EOL; + $code .= $this->getIndent() . 'php::NativeRootFrame _native_root_frame(' + . '_native_root_slots, ' . count($rootSlots) . ');' . PHP_EOL; + } + } // Native static calls pass a lightweight Object containing the called // class entry. A wrapper can be shared by all calls to the same class, // but its initialization must dominate every control-flow branch that @@ -4490,7 +4684,7 @@ class CompilerBase implements PropertyAccessContext if ($name === 'GLOBALS') { continue; } - $code .= $this->getIndent() . Type::VAR . ' &' . $name . ' = ' . $this->escapeGlobalVar($name) . ';' . PHP_EOL; + $code .= $this->getIndent() . 'auto &' . $name . ' = ' . $this->escapeGlobalVar($name) . ';' . PHP_EOL; } foreach ($this->context->objectProps as $name => $info) { if (($info['kind'] ?? 'zval') === 'var') { @@ -4522,6 +4716,9 @@ class CompilerBase implements PropertyAccessContext if ($this->functionDef->returnType === Type::VOID) { return ''; } + if ($this->getNativeObjectReturnType($this->functionDef) !== null) { + return $this->getIndent() . 'return nullptr;'; + } if ($this->functionDef->returnTypeCheck && !$this->context->inClosure) { return $this->genUnionCheckedReturn(self::VALUE_NULL); } diff --git a/src/Context/CompilationStateTrait.php b/src/Context/CompilationStateTrait.php index f6156e80..014dd067 100644 --- a/src/Context/CompilationStateTrait.php +++ b/src/Context/CompilationStateTrait.php @@ -74,6 +74,23 @@ trait CompilationStateTrait $this->globalVars[$name] = $type; } + protected function promoteGlobalOrStaticToNativeObject(string $name, string $class): void + { + $class = ltrim($class, '\\'); + if ($this->hasStaticVar($name)) { + $slot = $this->escapeStaticVar($name); + $this->context->staticVars[$name] = $this->getNativeObjectPointerType($class); + } elseif ($this->hasScopeGlobalVar($name)) { + $slot = $name; + $this->context->globalVars[$name] = $this->getNativeObjectPointerType($class); + } else { + return; + } + $this->globalVars[$slot] = $this->getNativeObjectPointerType($class); + $this->nativeGlobalObjects[$slot] = $class; + $this->addNativeObject($name, $class); + } + protected function addScopeGlobalVar(string $name, string $type): void { $this->context->globalVars[$name] = $type; @@ -81,6 +98,10 @@ trait CompilationStateTrait protected function addObject(string $name, string $class): void { + if ($this->isNativeObjectClass($class)) { + $this->addNativeObject($name, $class); + return; + } // Interfaces have no concrete method body for native calls. Abstract classes may have concrete methods. if ($this->isInterface($class)) { $this->context->declaredObjects[$name] = $class; diff --git a/src/Context/FunctionContext.php b/src/Context/FunctionContext.php index baa227eb..c76c2e9d 100644 --- a/src/Context/FunctionContext.php +++ b/src/Context/FunctionContext.php @@ -29,6 +29,9 @@ class FunctionContext */ public array $objects = []; + /** @var array Native Object pointer variable => fully-qualified class name. */ + public array $nativeObjects = []; + /** * Declared object constraints that are not used for native-call dispatch. * @@ -90,6 +93,7 @@ class FunctionContext $this->staticVars = []; $this->arguments = []; $this->objects = []; + $this->nativeObjects = []; $this->declaredObjects = []; $this->stdArrays = []; $this->stdContainers = []; @@ -124,11 +128,17 @@ class FunctionContext unset($this->scopeLayouts[$this->scopeLevel]); } - public function resetAnalysisTemporaries(array $localVars, int $tmpVarIndex, array $declaredObjects): void + public function resetAnalysisTemporaries( + array $localVars, + int $tmpVarIndex, + array $declaredObjects, + array $nativeObjects = [], + ): void { $this->localVars = $localVars; $this->tmpVarIndex = $tmpVarIndex; $this->declaredObjects = $declaredObjects; + $this->nativeObjects = $nativeObjects; $this->beforeStmtLines = []; $this->afterStmtLines = []; $this->objectProps = []; diff --git a/src/Entity/ClassDef.php b/src/Entity/ClassDef.php index d6103d9b..ad4611cb 100644 --- a/src/Entity/ClassDef.php +++ b/src/Entity/ClassDef.php @@ -32,6 +32,8 @@ class ClassDef extends ClassLikeDef public string $extends = ''; public bool $requireCtor = false; public bool $enum = false; + /** Compile-time-only class using the Native Object layout instead of zend_object. */ + public bool $nativeObject = false; /** Whether this class and its methods are part of the public ABI of a library build. */ public bool $exported = true; public ?string $methodsForTarget = null; diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index c8b5fc1f..c0ac11da 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -24,6 +24,8 @@ class FunctionDef public string $params = ''; public string $namespace; public bool $method = false; + /** Fully-qualified declaring class for methods; empty for free functions. */ + public string $declaringClass = ''; public bool $stub = false; /** Whether this function is part of the public ABI of a library build. */ public bool $exported = true; @@ -61,6 +63,8 @@ class FunctionDef * @var string 必须是带有命名空间的完整类名 */ public string $returnClass = ''; + /** Whether a Native object return may be represented by nullptr. */ + public bool $returnNullable = false; /** * Late-bound return type keyword: 'self', 'static' or 'parent'. diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index 48d5d453..472d365d 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -606,6 +606,13 @@ trait CallArgumentGenerator protected function parseCallArgValue(Node\Arg $arg): string { $this->assertExprCanBeUsedAsValue($arg->value, 'function argument'); + $class = $this->detectClassOfExpr($arg->value); + if ($class !== '' && $this->isNativeObjectClass($class)) { + $this->fatalError( + $arg, + 'Native objects cannot cross a dynamic PHP/ZendVM call boundary' + ); + } // C++17 evaluates php::ArgList{...} elements from left to right, but a // later argument may emit captured beforeStmtLines while being lowered. // Those statements are placed before the whole outer call and would diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index 24601779..0d8ec6c4 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -114,6 +114,32 @@ trait ClosureGenerator private function doGenClosure(Expr\ArrowFunction|Expr\Closure $expr, array $params, array $uses = []): string { + if ($this->classDef?->nativeObject && !$expr->static) { + $this->fatalError($expr, 'Native objects cannot be bound as $this to Zend closures'); + } + foreach ($params as $param) { + if ($this->getNativeObjectClassesFromTypeNode($param->type, self::DECL_TYPE_OF_PARAM) !== []) { + $this->fatalError($param, 'Zend closures cannot declare native object parameters or return types'); + } + } + if ($this->getNativeObjectClassesFromTypeNode($expr->returnType, self::DECL_TYPE_OF_RETURN) !== []) { + $this->fatalError($expr, 'Zend closures cannot declare native object parameters or return types'); + } + foreach ($uses as $useItem) { + if (!$this->isVarExpr($useItem->var)) { + continue; + } + $name = $this->parseIdentifier($useItem->var); + if ($this->isNativeObjectVar($name)) { + $this->fatalError($useItem, 'Native objects cannot be captured by Zend closures'); + } + } + if ($expr instanceof Expr\ArrowFunction + && $this->isNativeObjectClass($this->detectClassOfExpr($expr->expr)) + ) { + $this->fatalError($expr->expr, 'Zend closures cannot return native objects'); + } + $isGenerator = $this->closureContainsYield($expr); if ($isGenerator) { $this->validateGeneratorClosure($expr, $params); diff --git a/src/Generator/PropertyPromotion.php b/src/Generator/PropertyPromotion.php index f46a10aa..f59c1b99 100644 --- a/src/Generator/PropertyPromotion.php +++ b/src/Generator/PropertyPromotion.php @@ -16,6 +16,14 @@ trait PropertyPromotion { $code = ''; $propertyName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name); + if ($this->classDef?->nativeObject) { + $value = $this->getNativeObjectArgumentType($argInfo) !== null + ? $argInfo->name + : $this->convertExprFromType($argInfo->type, $argInfo->name); + $property = $this->classDef->getProperty($propertyName); + return 'this_.' . $this->getNativeObjectPropertyCppName($property, $this->classDef) + . ' = ' . $value . ';' . PHP_EOL; + } $code .= 'this_.setProperty(' . $this->genCharPtr($propertyName) . ', ' . $argInfo->name . ')'; $code .= ";\n"; return $code; diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php new file mode 100644 index 00000000..f4f89def --- /dev/null +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -0,0 +1,866 @@ + true, + '__callstatic' => true, + '__get' => true, + '__set' => true, + '__isset' => true, + '__unset' => true, + '__sleep' => true, + '__wakeup' => true, + '__serialize' => true, + '__unserialize' => true, + '__set_state' => true, + '__debuginfo' => true, + ]; + + protected function assertNativeMagicMethodSupported(NodeAbstract $node, string $method): void + { + if (!$this->classDef?->nativeObject + || !isset(self::UNSUPPORTED_NATIVE_MAGIC_METHODS[strtolower($method)]) + ) { + return; + } + $this->fatalError( + $node, + "Native classes do not support dynamic magic method `{$method}()`", + ); + } + + /** + * Native classes have no zend_class_entry, so Zend cannot perform the + * normal MINIT-time interface verification for them. Convert an internal + * reflection signature into the compiler's existing MethodDef model and + * run the same compatibility checker used for project interfaces. + */ + protected function checkInternalInterfaceImplementation( + NodeAbstract $node, + ClassDef $classDef, + string $interfaceName, + ): void { + $interface = Reflection::getClass($interfaceName); + if ($interface === null) { + $this->fatalError($node, "Internal interface `{$interfaceName}` is not available"); + } + + foreach ($interface->getMethods() as $method) { + $childMethodDef = $this->findClassMethodDef($classDef, $method->getName(), $classDef->isAbstract()); + if ($childMethodDef === null) { + if ($classDef->isAbstract()) { + continue; + } + $this->fatalError( + $node, + "Class `{$classDef->getNamespacedName(false)}` must implement method " . + "`{$interfaceName}::{$method->getName()}()`", + ); + } + $this->validateMethodOverrideSignature( + $childMethodDef->node ?? $node, + $method->getName(), + $childMethodDef, + $this->createInternalInterfaceMethodDef($method), + $interfaceName, + ); + } + } + + private function createInternalInterfaceMethodDef(\ReflectionMethod $method): MethodDef + { + $flags = Modifiers::PUBLIC | Modifiers::ABSTRACT; + if ($method->isStatic()) { + $flags |= Modifiers::STATIC; + } + + $methodDef = new MethodDef($flags, $method->getName()); + $functionDef = new FunctionDef($method->getName(), Type::VAR, ''); + $functionDef->method = true; + $functionDef->declaringClass = $method->getDeclaringClass()->getName(); + $functionDef->returnsByRef = $method->returnsReference(); + $functionDef->returnTypeUndeclared = $method->getReturnType() === null; + $this->applyReflectedReturnType($functionDef, $method->getReturnType(), $method->getDeclaringClass()); + + foreach ($method->getParameters() as $parameter) { + $argument = new ArgInfo(); + $argument->name = $parameter->getName(); + $argument->phpName = $parameter->getName(); + $argument->byRef = $parameter->isPassedByReference(); + $argument->variadic = $parameter->isVariadic(); + $argument->undeclared = $parameter->getType() === null; + $argument->nullable = $parameter->allowsNull(); + $this->applyReflectedParameterType($argument, $parameter->getType(), $method->getDeclaringClass()); + if ($parameter->isOptional() || $parameter->isVariadic()) { + // Compatibility only needs to distinguish required from + // optional parameters; the concrete default is irrelevant. + $argument->defaultValue = new Node\Expr\ConstFetch(new Node\Name('null')); + } + $functionDef->argInfoList[] = $argument; + } + $functionDef->argCountRequired = $method->getNumberOfRequiredParameters(); + $methodDef->functionDef = $functionDef; + return $methodDef; + } + + private function applyReflectedReturnType( + FunctionDef $function, + ?\ReflectionType $type, + \ReflectionClass $declaringClass, + ): void { + if ($type === null) { + $function->returnTypeStr = ''; + return; + } + $node = $this->reflectionTypeToNode($type, $declaringClass); + $function->returnTypeStr = $this->typeCheckNodeToString($node); + if ($node instanceof Node\NullableType + || $node instanceof Node\UnionType + || $node instanceof Node\IntersectionType + ) { + $typeInfo = $this->buildTypeCheckFromNode($node); + $function->returnType = Type::VAR; + $function->returnTypeCheck = $typeInfo['check']; + $function->returnTypeNode = $node; + return; + } + [$function->returnType, $function->returnClass] = $this->resolveReflectedNamedType($node); + } + + private function applyReflectedParameterType( + ArgInfo $argument, + ?\ReflectionType $type, + \ReflectionClass $declaringClass, + ): void { + if ($type === null) { + $argument->type = Type::VAR; + return; + } + $node = $this->reflectionTypeToNode($type, $declaringClass); + $argument->typeStr = $this->typeCheckNodeToString($node); + if ($node instanceof Node\NullableType + || $node instanceof Node\UnionType + || $node instanceof Node\IntersectionType + ) { + $typeInfo = $this->buildTypeCheckFromNode($node); + $argument->type = Type::VAR; + $argument->typeCheck = $typeInfo['check']; + $argument->typeNode = $node; + return; + } + [$argument->type, $argument->declaredClass] = $this->resolveReflectedNamedType($node); + if ($argument->declaredClass !== '' && !$this->isInterface($argument->declaredClass)) { + $argument->class = $argument->declaredClass; + } + $argument->explicitMixed = strtolower($argument->typeStr) === 'mixed'; + } + + /** @return array{string, string} */ + private function resolveReflectedNamedType(NodeAbstract $node): array + { + $name = $this->parseIdentifier($node); + $lower = strtolower(ltrim($name, '\\')); + if (isset($this->zendTypeMap[$lower])) { + return [$this->getTypeFromZendType($lower), '']; + } + return [Type::OBJECT, ltrim($name, '\\')]; + } + + private function reflectionTypeToNode( + \ReflectionType $type, + \ReflectionClass $declaringClass, + bool $allowNullableWrapper = true, + ): NodeAbstract { + if ($type instanceof \ReflectionUnionType) { + return new Node\UnionType(array_map( + fn (\ReflectionType $member): NodeAbstract => + $this->reflectionTypeToNode($member, $declaringClass, false), + $type->getTypes(), + )); + } + if ($type instanceof \ReflectionIntersectionType) { + return new Node\IntersectionType(array_map( + fn (\ReflectionType $member): NodeAbstract => + $this->reflectionTypeToNode($member, $declaringClass, false), + $type->getTypes(), + )); + } + + /** @var \ReflectionNamedType $type */ + $name = $type->getName(); + $lower = strtolower($name); + if ($lower === 'self') { + $node = new Node\Name\FullyQualified($declaringClass->getName()); + } elseif ($lower === 'parent') { + $parent = $declaringClass->getParentClass(); + $node = $parent === false + ? new Node\Name('parent') + : new Node\Name\FullyQualified($parent->getName()); + } elseif ($lower === 'static') { + $node = new Node\Name('static'); + } elseif ($type->isBuiltin()) { + $node = new Node\Identifier($name); + } else { + $node = new Node\Name\FullyQualified($name); + } + + if ($allowNullableWrapper + && $type->allowsNull() + && !in_array($lower, ['mixed', 'null'], true) + ) { + return new Node\NullableType($node); + } + return $node; + } + + protected function isNativeObjectClass(string $class): bool + { + $class = ltrim($class, '\\'); + return $class !== '' && $this->hasClass($class) && $this->getClass($class)->nativeObject; + } + + protected function getNativeObjectCppName(string|ClassDef $class): string + { + $classDef = $class instanceof ClassDef ? $class : $this->getClass(ltrim($class, '\\')); + return self::PREFIX . $this->getNativeName('', $classDef->namespace, $classDef->name); + } + + protected function getNativeObjectDescriptorName(string|ClassDef $class): string + { + return $this->getNativeObjectCppName($class) . '__type'; + } + + protected function getNativeObjectPointerType(string|ClassDef $class): string + { + return $this->getNativeObjectCppName($class) . ' *'; + } + + protected function getNativeObjectArgumentType(ArgInfo $argument): ?string + { + $class = $argument->declaredClass ?: $argument->class; + if ((!$argument->byRef && $argument->type !== Type::OBJECT) + || !$this->isNativeObjectClass($class) + ) { + return null; + } + return $this->getNativeObjectPointerType($class) . ($argument->byRef ? '&' : ''); + } + + protected function resolveNullableNativeObjectType(?NodeAbstract $type, int $declarationKind): ?array + { + $inner = null; + if ($type instanceof Node\NullableType) { + $inner = $type->type; + } elseif ($type instanceof Node\UnionType && count($type->types) === 2) { + foreach ($type->types as $member) { + if ($member instanceof Node\Identifier && strtolower($member->toString()) === 'null') { + continue; + } + if ($inner !== null) { + return null; + } + $inner = $member; + } + } + if (!$inner instanceof Node\Name) { + return null; + } + [$innerType, $class] = $this->resolveTypeDecl($inner, $declarationKind); + if ($innerType !== Type::OBJECT || !$this->isNativeObjectClass($class)) { + return null; + } + return [Type::OBJECT, $class]; + } + + /** @return list */ + protected function getNativeObjectClassesFromTypeNode(?NodeAbstract $type, int $declarationKind): array + { + if ($type === null || $type instanceof Node\Identifier) { + return []; + } + if ($type instanceof Node\NullableType) { + return $this->getNativeObjectClassesFromTypeNode($type->type, $declarationKind); + } + if ($type instanceof Node\UnionType || $type instanceof Node\IntersectionType) { + $classes = []; + foreach ($type->types as $member) { + foreach ($this->getNativeObjectClassesFromTypeNode($member, $declarationKind) as $class) { + $classes[strtolower($class)] = $class; + } + } + return array_values($classes); + } + if (!$type instanceof Node\Name) { + return []; + } + [$resolvedType, $class] = $this->resolveTypeDecl($type, $declarationKind); + return $resolvedType === Type::OBJECT && $this->isNativeObjectClass($class) ? [$class] : []; + } + + /** + * Resolve a common Native pointer type for value-selection branches. + * Null is accepted as the empty state; any Zend/non-object branch makes + * the expression unsuitable for the Native object model. + * + * @param list $expressions + */ + protected function getCommonNativeObjectExpressionClass(array $expressions): string + { + $common = ''; + foreach ($expressions as $expression) { + if ($this->isNull($expression)) { + continue; + } + $class = $this->detectClassOfExpr($expression); + if (!$this->isNativeObjectClass($class)) { + return ''; + } + if ($common === '') { + $common = $class; + continue; + } + if ($this->isObjectClassStaticallyAssignableTo($class, $common)) { + continue; + } + if ($this->isObjectClassStaticallyAssignableTo($common, $class)) { + $common = $class; + continue; + } + return ''; + } + return $common; + } + + protected function assertSupportedNativeObjectTypeNode( + ?NodeAbstract $type, + int $declarationKind, + NodeAbstract $errorNode, + ): void { + if (!$type instanceof Node\UnionType && !$type instanceof Node\IntersectionType) { + return; + } + $nativeClasses = $this->getNativeObjectClassesFromTypeNode($type, $declarationKind); + if ($nativeClasses !== [] && $this->resolveNullableNativeObjectType($type, $declarationKind) === null) { + $this->fatalError( + $errorNode, + 'Native object types cannot be combined with other union or intersection members', + ); + } + } + + protected function getNativeObjectReturnType(FunctionDef $function): ?string + { + if ($function->returnType !== Type::OBJECT || !$this->isNativeObjectClass($function->returnClass)) { + return null; + } + return $this->getNativeObjectPointerType($function->returnClass); + } + + protected function getNativeObjectMethodThisType(FunctionDef $function): ?string + { + if (!$function->method || !$this->isNativeObjectClass($function->declaringClass)) { + return null; + } + return $this->getNativeObjectCppName($function->declaringClass) . ' &'; + } + + protected function functionUsesNativeObject(FunctionDef $function): bool + { + if ($this->getNativeObjectReturnType($function) !== null + || $this->getNativeObjectMethodThisType($function) !== null + ) { + return true; + } + foreach ($function->argInfoList as $argument) { + if ($this->getNativeObjectArgumentType($argument) !== null) { + return true; + } + } + return false; + } + + protected function genNativeObjectParameterChecks(FunctionDef $function): string + { + $code = ''; + foreach ($function->argInfoList as $argument) { + $class = $argument->declaredClass ?: $argument->class; + if (!$argument->nullable && $this->isNativeObjectClass($class)) { + $code .= $this->getIndent() . 'php::nativeGcRequireObject(' + . $argument->name . ', "' . addslashes($class) . '");' . PHP_EOL; + } + } + return $code; + } + + protected function addNativeObject(string $name, string $class): void + { + $this->context->nativeObjects[$name] = ltrim($class, '\\'); + $this->context->objects[$name] = ltrim($class, '\\'); + } + + protected function isNativeObjectVar(string $name): bool + { + return isset($this->context->nativeObjects[$name]); + } + + protected function getNativeObjectVarClass(string $name): string + { + return $this->context->nativeObjects[$name] ?? ''; + } + + protected function getNativeObjectReceiver(string $name): string + { + if ($name === 'this_') { + return 'this_'; + } + $class = $this->getNativeObjectVarClass($name); + return 'php::nativeDeref(' . $name . ', "' . addslashes($class) . '")'; + } + + protected function getNativeObjectMemberReceiver(string $name): string + { + return $this->getNativeObjectReceiver($name) . '.'; + } + + /** + * Materialize a Native-producing expression as a precisely rooted local. + * This is shared by chained method and property access so neither path can + * accidentally pass a Native pointer through php::Variant. + */ + protected function materializeNativeObjectReceiver(NodeAbstract $expr, string $class): string + { + [$receiver, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr); + $this->appendCapturedStmtLinesToContext($beforeStmts); + $object = $this->genTmpVarName(); + $this->addLocalVar($object, $this->getNativeObjectPointerType($class)); + $this->addNativeObject($object, $class); + $this->context->beforeStmtLines[] = $object . ' = ' . $receiver . ';'; + $this->appendCapturedStmtLinesToContext($afterStmts); + // Keep the temporary in the precise root frame while the complete PHP + // statement executes, then release that root at statement end. + $this->context->afterStmtLines[] = $object . ' = nullptr;'; + return $object; + } + + protected function findNativeObjectProperty(string $class, string $property): ?PropertyDef + { + while ($class !== '' && $this->hasClass($class)) { + $classDef = $this->getClass($class); + if ($classDef->hasProperty($property)) { + return $classDef->getProperty($property); + } + $class = $classDef->extends; + } + return null; + } + + protected function findNativeObjectMethod(string $class, string $method): ?MethodDef + { + while ($class !== '' && $this->isNativeObjectClass($class)) { + $classDef = $this->getClass($class); + if ($classDef->hasMethod($method)) { + return $classDef->getMethod($method); + } + $class = $classDef->extends; + } + return null; + } + + /** + * Native objects cannot fall back to PHPX/Zend conversion helpers. A + * keyword conversion is therefore a statically checked ordinary Native + * method call. __toString() is accepted as the PHP-compatible spelling of + * toString(). + */ + protected function resolveNativeObjectKeywordMethod( + NodeAbstract $node, + string $class, + string $method, + ): string { + $expectedType = self::KEYWORD_METHOD_MAP[$method] ?? null; + if ($expectedType === null) { + return $method; + } + $resolvedMethod = $method; + $methodDef = $this->findNativeObjectMethod($class, $resolvedMethod); + if ($method === 'toString' && $methodDef === null) { + $resolvedMethod = '__toString'; + $methodDef = $this->findNativeObjectMethod($class, $resolvedMethod); + } + if ($methodDef === null) { + $this->fatalError($node, "Native class `{$class}` must define `{$method}()` for this conversion"); + } + $function = $methodDef->functionDef; + if ($function->argInfoList !== []) { + $this->fatalError($node, "Native conversion method `{$class}::{$resolvedMethod}()` must not accept arguments"); + } + if ($function->returnsByRef || $function->returnNullable || $function->returnType !== $expectedType) { + $expectedTypeName = match ($expectedType) { + Type::INT => 'int', + Type::FLOAT => 'float', + Type::STR => 'string', + Type::BOOL => 'bool', + Type::ARRAY => 'array', + Type::STREAM => 'Stream', + Type::BIGINT => 'BigInt', + Type::BIGFLOAT => 'BigFloat', + Type::DECIMAL => 'Decimal', + Type::OBJECT => 'object', + Type::VAR => 'mixed', + default => $expectedType, + }; + $this->fatalError( + $node, + "Native conversion method `{$class}::{$resolvedMethod}()` must return exactly `{$expectedTypeName}`", + ); + } + return $resolvedMethod; + } + + protected function getNativeVirtualMethodName(string $method): string + { + return '__typephp_virtual_' . strtolower($method); + } + + protected function isNativeVirtualMethod(ClassDef $class, MethodDef $method): bool + { + if ($method->flags & (Modifiers::STATIC | Modifiers::PRIVATE | Modifiers::FINAL | Modifiers::ABSTRACT)) { + return false; + } + if (in_array(strtolower($method->name), ['__construct', '__destruct', '__clone'], true)) { + return false; + } + if ($this->isOverrideMethod($class->getNamespacedName(false) . '::' . $method->name)) { + return true; + } + $parent = $class->extends; + while ($parent !== '' && $this->hasClass($parent)) { + $parentDef = $this->getClass($parent); + if ($parentDef->hasMethod($method->name)) { + return true; + } + $parent = $parentDef->extends; + } + return false; + } + + protected function getNativeMethodReturnCppType(FunctionDef $function): string + { + return $function->returnsByRef + ? Type::REF + : ($this->getNativeObjectReturnType($function) ?? $function->returnType); + } + + protected function getNativeMethodParameterDeclarations(FunctionDef $function): string + { + $args = []; + foreach ($function->argInfoList as $argument) { + if ($argument->variadic) { + $args[] = Type::ARRAY . ' ' . $argument->name; + } else { + $args[] = $this->genArgumentDeclaration($argument); + } + } + return implode(', ', $args); + } + + protected function getNativeObjectPropertyType(PropertyDef $property): string + { + if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) { + return $this->getNativeObjectPointerType($property->class); + } + return match ($property->type) { + Type::STREAM, Type::BOX => Type::VAR, + default => $property->type, + }; + } + + protected function getNativeObjectPropertyCppName( + string|PropertyDef $property, + string|ClassDef|null $declaringClass = null, + ): string + { + $name = $property instanceof PropertyDef ? $property->name : $property; + if (!$property instanceof PropertyDef || !$property->isPrivate()) { + return $this->escapeVarName($name); + } + if ($declaringClass === null) { + throw new \LogicException('Native private property field requires its declaring class'); + } + return '__private_' . $this->getNativeObjectCppName($declaringClass) + . '__' . $this->escapeVarName($name); + } + + protected function isNativeObjectForbiddenPropertyType(PropertyDef $property): bool + { + if (in_array($property->type, [ + Type::BOX, + Type::STD_ARRAY, + Type::STD_VECTOR, + Type::STD_MAP, + Type::STD_ORDERED_MAP, + ], true)) { + return true; + } + return in_array(strtolower(ltrim($property->class, '\\')), [ + 'std\\array', + 'std\\vector', + 'std\\map', + 'std\\ordered_map', + ], true); + } + + protected function isNativeObjectInheritedPropertyRedeclaration( + ClassDef $class, + PropertyDef $property, + ): bool { + $parent = $class->extends; + while ($parent !== '' && $this->isNativeObjectClass($parent)) { + $parentDef = $this->getClass($parent); + if ($parentDef->hasProperty($property->name)) { + $parentProperty = $parentDef->getProperty($property->name); + if ($property->isPrivate() || $parentProperty->isPrivate()) { + return false; + } + // Property compatibility is validated separately. TypePHP + // treats a compatible public/protected redeclaration as the + // same inherited slot, so a Native child must not emit a + // second C++ field with the same PHP property name. + return true; + } + $parent = $parentDef->extends; + } + return false; + } + + /** + * C++ requires a base struct to be complete before defining a derived + * struct. PHP source order has no such restriction, so emit Native class + * definitions in inheritance order while retaining source order between + * unrelated classes. + * + * @return list + */ + protected function getNativeObjectClassesInDeclarationOrder(): array + { + $classes = array_values(array_filter( + $this->symbols->classes(), + static fn (ClassDef $class): bool => $class->nativeObject, + )); + $byName = []; + foreach ($classes as $class) { + $byName[strtolower(ltrim($class->getNamespacedName(false), '\\'))] = $class; + } + + $ordered = []; + $visited = []; + $visit = function (ClassDef $class) use (&$visit, &$ordered, &$visited, $byName): void { + $key = strtolower(ltrim($class->getNamespacedName(false), '\\')); + if (isset($visited[$key])) { + return; + } + $visited[$key] = true; + $parent = strtolower(ltrim($class->extends, '\\')); + if ($parent !== '' && isset($byName[$parent])) { + $visit($byName[$parent]); + } + $ordered[] = $class; + }; + foreach ($classes as $class) { + $visit($class); + } + return $ordered; + } + + protected function genNativeObjectDeclarations(): string + { + $classes = $this->getNativeObjectClassesInDeclarationOrder(); + if ($classes === []) { + return ''; + } + + $code = '// TypePHP Native Object declarations' . PHP_EOL; + foreach ($classes as $class) { + $code .= 'struct ' . $this->getNativeObjectCppName($class) . ';' . PHP_EOL; + } + $code .= PHP_EOL; + + foreach ($classes as $class) { + $name = $this->getNativeObjectCppName($class); + $parent = $class->extends !== '' && $this->isNativeObjectClass($class->extends) + ? ' : public ' . $this->getNativeObjectCppName($class->extends) + : ''; + $code .= 'struct ' . $name . $parent . ' {' . PHP_EOL; + foreach ($class->properties as $property) { + if ($property->flags & Modifiers::STATIC + || $this->isNativeObjectInheritedPropertyRedeclaration($class, $property) + ) { + continue; + } + $type = $this->getNativeObjectPropertyType($property); + // PHPX value types own their storage and must be initialized by + // their C++ default constructor. PHP-level defaults are applied + // by the generated allocation/constructor path, not in this + // shared declaration header (which cannot reference file-local + // literal tables). + $default = null; + if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) { + $default = 'nullptr'; + } elseif (in_array($property->type, [Type::INT, Type::FLOAT, Type::BOOL], true)) { + $default = '0'; + } + $code .= ' ' . $type . ' ' . $this->getNativeObjectPropertyCppName($property, $class); + if ($default !== null) { + $code .= ' = ' . $default; + } + $code .= ';' . PHP_EOL; + } + foreach ($class->methods as $method) { + if (!$this->isNativeVirtualMethod($class, $method)) { + continue; + } + $parentHasMethod = $class->extends !== '' + && $this->hasClass($class->extends) + && $this->getClass($class->extends)->hasMethod($method->name); + $code .= ' virtual ' . $this->getNativeMethodReturnCppType($method->functionDef) + . ' ' . $this->getNativeVirtualMethodName($method->name) . '(' + . $this->getNativeMethodParameterDeclarations($method->functionDef) . ')' + . ($parentHasMethod ? ' override' : '') . ';' . PHP_EOL; + } + $code .= '};' . PHP_EOL; + $code .= 'void ' . $name . '__initialize(' . $name . ' &object);' . PHP_EOL; + $code .= 'void ' . $name . '__gc_trace(void *object, php::NativeMarker &marker);' . PHP_EOL; + $code .= 'extern const php::NativeTypeDescriptor ' + . $this->getNativeObjectDescriptorName($class) . ';' . PHP_EOL . PHP_EOL; + } + return $code; + } + + protected function genNativeObjectRuntimeDefinition(ClassDef $class): string + { + $cpp = $this->getNativeObjectCppName($class); + $prefix = $cpp . '__gc'; + $code = ''; + $code .= 'void ' . $cpp . '__initialize(' . $cpp . ' &this_) {' . PHP_EOL; + if ($class->extends !== '' && $this->isNativeObjectClass($class->extends)) { + $code .= ' ' . $this->getNativeObjectCppName($class->extends) . '__initialize(this_);' . PHP_EOL; + } + foreach ($class->properties as $property) { + if ($property->isStatic() || $property->default === null) { + continue; + } + if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) { + $value = 'nullptr'; + } else { + $value = $property->default; + } + $code .= ' this_.' . $this->getNativeObjectPropertyCppName($property, $class) . ' = ' . $value . ';' . PHP_EOL; + } + $code .= '}' . PHP_EOL . PHP_EOL; + foreach ($class->methods as $method) { + if (!$this->isNativeVirtualMethod($class, $method)) { + continue; + } + $function = $method->functionDef; + $returnType = $this->getNativeMethodReturnCppType($function); + $args = array_map(static fn (ArgInfo $arg): string => $arg->name, $function->argInfoList); + $nativeFunction = self::PREFIX . $this->getNativeName( + $method->name, + $class->namespace, + $class->name, + ); + $code .= $returnType . ' ' . $cpp . '::' . $this->getNativeVirtualMethodName($method->name) + . '(' . $this->getNativeMethodParameterDeclarations($function) . ') {' . PHP_EOL; + $call = $nativeFunction . '(*this' . ($args === [] ? '' : ', ' . implode(', ', $args)) . ')'; + $code .= ' ' . ($returnType === Type::VOID ? '' : 'return ') . $call . ';' . PHP_EOL; + $code .= '}' . PHP_EOL . PHP_EOL; + } + $code .= 'void ' . $prefix . '_trace(void *object, php::NativeMarker &marker) {' . PHP_EOL; + $hasParentTrace = $class->extends !== '' && $this->isNativeObjectClass($class->extends); + $nativeProperties = array_filter( + $class->properties, + fn (PropertyDef $property): bool => !$property->isStatic() + && !$this->isNativeObjectInheritedPropertyRedeclaration($class, $property) + && $property->type === Type::OBJECT + && $this->isNativeObjectClass($property->class), + ); + if ($hasParentTrace || $nativeProperties !== []) { + $code .= ' auto &this_ = *static_cast<' . $cpp . ' *>(object);' . PHP_EOL; + } else { + $code .= ' (void) object;' . PHP_EOL; + $code .= ' (void) marker;' . PHP_EOL; + } + if ($hasParentTrace) { + $code .= ' ' . $this->getNativeObjectCppName($class->extends) + . '__gc_trace(static_cast<' . $this->getNativeObjectCppName($class->extends) . ' *>(&this_), marker);' + . PHP_EOL; + } + foreach ($nativeProperties as $property) { + $code .= ' marker.mark(this_.' . $this->getNativeObjectPropertyCppName($property, $class) . ');' . PHP_EOL; + } + $code .= '}' . PHP_EOL; + + $destructors = []; + $destructorClass = $class; + while (true) { + if ($destructorClass->hasMethod('__destruct')) { + $destructors[] = [ + self::PREFIX . $this->getNativeName( + '__destruct', + $destructorClass->namespace, + $destructorClass->name, + ), + $this->getNativeObjectCppName($destructorClass), + ]; + } + if ($destructorClass->extends === '' || !$this->isNativeObjectClass($destructorClass->extends)) { + break; + } + $destructorClass = $this->getClass($destructorClass->extends); + } + if ($destructors !== []) { + $code .= 'static void ' . $prefix . '_finalize(void *object) {' . PHP_EOL; + foreach ($destructors as [$destructor, $destructorCpp]) { + $code .= ' ' . $destructor . '(*static_cast<' . $destructorCpp . ' *>(object));' . PHP_EOL; + } + $code .= '}' . PHP_EOL; + } + $code .= 'static void ' . $prefix . '_destroy(void *object) noexcept {' . PHP_EOL; + $code .= ' static_cast<' . $cpp . ' *>(object)->~' . $cpp . '();' . PHP_EOL; + $code .= '}' . PHP_EOL; + $code .= 'const php::NativeTypeDescriptor ' . $this->getNativeObjectDescriptorName($class) . ' = {' . PHP_EOL; + $code .= ' "' . addslashes($class->getNamespacedName(false)) . '",' . PHP_EOL; + $code .= ' sizeof(' . $cpp . '),' . PHP_EOL; + $code .= ' alignof(' . $cpp . '),' . PHP_EOL; + $code .= ' ' . $prefix . '_trace,' . PHP_EOL; + $code .= ' ' . ($destructors !== [] ? $prefix . '_finalize' : 'nullptr') . ',' . PHP_EOL; + $code .= ' ' . $prefix . '_destroy,' . PHP_EOL; + $code .= '};' . PHP_EOL . PHP_EOL; + return $code; + } +} diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index 2c2b369e..cda2e023 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -211,6 +211,23 @@ trait FuncCallOptimizer return false; } + // Optimized php::fn::* calls must obey the same ZendVM escape boundary + // as the generic call generator. The four scalar conversions are + // language-level Native keyword aliases and are lowered to an exact + // Native method; every other PHP function rejects Native pointers. + if (!isset($config['conversion'])) { + foreach ($expr->args as $arg) { + if ($arg instanceof Node\Arg + && $this->isNativeObjectClass($this->detectClassOfExpr($arg->value)) + ) { + $this->fatalError( + $arg, + 'Native objects cannot cross a dynamic PHP/ZendVM call boundary', + ); + } + } + } + // 检测参数中使用的变量是否已定义,若变量不存在则回退到动态调用路径 // 动态路径中的 parseCallArgs() 会给出明确的错误信息 foreach ($expr->args as $arg) { @@ -472,6 +489,22 @@ trait FuncCallOptimizer { $arg = $expr->args[0]->value; $type = $this->detectTypeOfExpr($arg); + $nativeClass = $this->detectClassOfExpr($arg); + if ($this->isNativeObjectClass($nativeClass)) { + $method = match ($convType) { + self::ARG_TYPE_STR => 'toString', + self::ARG_TYPE_INT => 'toInt', + self::ARG_TYPE_FLOAT => 'toFloat', + self::ARG_TYPE_BOOL => 'toBool', + default => null, + }; + if ($method !== null) { + return $this->parseMethodCall(new Node\Expr\MethodCall( + $arg, + new Node\Identifier($method), + )); + } + } $parsed = $this->parseExpr($arg); if ($convType === self::ARG_TYPE_STR) { @@ -589,7 +622,10 @@ trait FuncCallOptimizer protected function doFoldKnownClass(Node\Expr\FuncCall $expr): string|false { $cn = $expr->args[0]->value; - return ($this->isScalarString($cn) && $this->hasClass($cn->value)) ? 'true' : false; + if (!$this->isScalarString($cn) || !$this->hasClass($cn->value)) { + return false; + } + return $this->isNativeObjectClass($cn->value) ? 'false' : 'true'; } protected function doFoldKnownConstant(Node\Expr\FuncCall $expr): string|false diff --git a/src/Parser/ArrayExpressionTrait.php b/src/Parser/ArrayExpressionTrait.php index 1a1186c4..e8f8032b 100644 --- a/src/Parser/ArrayExpressionTrait.php +++ b/src/Parser/ArrayExpressionTrait.php @@ -31,6 +31,10 @@ trait ArrayExpressionTrait $hasNextInsert = false; $hasReference = false; foreach ($items as $item) { + $valueClass = $this->detectClassOfExpr($item->value); + if ($this->isNativeObjectClass($valueClass)) { + $this->fatalError($item->value, 'Native objects cannot be stored in PHP arrays'); + } if ($item->unpack) { $hasUnpack = true; } diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 15f59391..192c5288 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -95,7 +95,13 @@ trait AssignOpTrait $next = $next->expr; } $tmpVar = $this->genTmpVarName(); - $this->addLocalVar($tmpVar, Type::VAR); + $nativeClass = $this->detectClassOfExpr($next); + if ($this->isNativeObjectClass($nativeClass)) { + $this->addLocalVar($tmpVar, $this->getNativeObjectPointerType($nativeClass)); + $this->addNativeObject($tmpVar, $nativeClass); + } else { + $this->addLocalVar($tmpVar, Type::VAR); + } // 翻转赋值链 $chain = array_reverse($chain); @@ -208,10 +214,64 @@ trait AssignOpTrait protected function parseAssignFinally(Expr $left, Expr $right): string { $this->assertNotNullsafeWriteContext($left); + $rightClass = $this->detectClassOfExpr($right); if ($left instanceof Expr\List_) { + if ($this->isNativeObjectClass($rightClass)) { + $this->fatalError($right, 'Native objects cannot be destructured into PHP values'); + } return $this->parseAssignToList($left, $right); } + if ($this->isNativeObjectClass($rightClass)) { + $allowed = false; + if ($this->isVarExpr($left)) { + $leftName = $this->parseVariable($left); + $declaredClass = $this->getDeclaredObjectType($leftName); + if ($declaredClass !== '' && $this->isInterface($declaredClass)) { + $this->fatalError( + $left, + 'Native objects cannot be assigned to interface-typed variables', + ); + } + $allowed = !$this->hasVar($leftName) + || $this->isNativeObjectVar($leftName) + || $this->hasScopeGlobalVar($leftName) + || $this->hasStaticVar($leftName); + if ($allowed && ($this->hasScopeGlobalVar($leftName) || $this->hasStaticVar($leftName))) { + $this->promoteGlobalOrStaticToNativeObject($leftName, $rightClass); + } + } elseif ($left instanceof Expr\PropertyFetch + && $this->isVarExpr($left->var) + && $this->isIdExpr($left->name) + ) { + $receiver = $this->parseVariable($left->var); + if ($this->isNativeObjectVar($receiver)) { + $property = $this->findNativeObjectProperty( + $this->getNativeObjectVarClass($receiver), + $left->name->toString(), + ); + if ($property !== null + && $property->class !== '' + && $this->isInterface($property->class) + ) { + $this->fatalError( + $left, + 'Native objects cannot be assigned to interface-typed properties', + ); + } + $allowed = $property !== null + && $property->type === Type::OBJECT + && $this->isNativeObjectClass($property->class); + } + } + if (!$allowed) { + $this->fatalError( + $left, + 'Native objects cannot be stored in PHP arrays, PHP object properties, static properties, or mixed variables', + ); + } + } + // A direct assignment to readonly must go through write_property so // Zend can enforce scope, initialization state, type and clone rules. $propertyWriteTarget = $this->preparePropertyWriteTarget($left, true); @@ -230,6 +290,45 @@ trait AssignOpTrait $this->fatalError($left, 'Cannot write to read-only hooked property'); } + if ($left instanceof Expr\PropertyFetch + && $this->isVarExpr($left->var) + && $this->isIdExpr($left->name) + ) { + $object = $this->parseIdentifier($left->var); + if ($this->isNativeObjectVar($object)) { + $property = $this->parseIdentifier($left->name); + $class = $this->getNativeObjectVarClass($object); + $access = $this->getNativePropertyAccess($left); + if ($access === null) { + $this->fatalError($left, "Native class `{$class}` has no property `\${$property}`"); + } + $def = $access->getPropertyDef(); + $field = $this->getNativeObjectPropertyCppName($def, $access->getClassDef()); + $rightExpr = $this->parseExprAsValue($right); + if ($def->type === Type::OBJECT && $this->isNativeObjectClass($def->class)) { + if ($this->isNull($right)) { + if (!$def->nullable) { + $this->fatalError($right, "Cannot assign null to native property `{$class}::\${$property}`"); + } + return $this->getNativeObjectMemberReceiver($object) + . $field . ' = nullptr'; + } + $rightClass = $this->detectClassOfExpr($right); + if ($rightClass === '' || !$this->isObjectClassStaticallyAssignableTo($rightClass, $def->class)) { + $this->fatalError($right, "Cannot assign value to native property `{$class}::\${$property}`"); + } + } else { + $this->assertCanAssignPropertyWrite($propertyWriteTarget, $right); + $rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr); + if ($def->type !== Type::VAR) { + $rightExpr = $this->convertExprFromType($def->type, $rightExpr); + } + } + return $this->getNativeObjectMemberReceiver($object) + . $field . ' = ' . $rightExpr; + } + } + if ($propertyWriteTarget !== null && $this->shouldUseDynamicNativePropertyWrite($left, $type)) { return $this->parseAssignPropertyFetch($left, $right, $propertyWriteTarget); } @@ -250,11 +349,28 @@ trait AssignOpTrait } // 类型推断,获取对象的类名,如果不是对象则返回空字符串 $rightClass = $this->detectClassOfExpr($right); + if ($this->isNativeObjectVar($var)) { + $leftClass = $this->getNativeObjectVarClass($var); + if ($this->isNull($right)) { + return $var . ' = nullptr'; + } + if ($rightClass === '' || !$this->isNativeObjectClass($rightClass)) { + $this->fatalError($right, "Native object `\${$var}` cannot be converted to var/object"); + } + if (!$this->isObjectClassStaticallyAssignableTo($rightClass, $leftClass)) { + $this->fatalError($right, "Cannot assign native object `{$rightClass}` to `{$leftClass}`"); + } + } // 右值是一个对象,已获得类的名称,左值必须与右值的类一致 if ($rightClass) { if (!$this->hasVar($var)) { - $this->addLocalVar($var, Type::OBJECT); - $this->addObject($var, $rightClass); + if ($this->isNativeObjectClass($rightClass)) { + $this->addLocalVar($var, $this->getNativeObjectPointerType($rightClass)); + $this->addNativeObject($var, $rightClass); + } else { + $this->addLocalVar($var, Type::OBJECT); + $this->addObject($var, $rightClass); + } } elseif (($leftClass = $this->getDeclaredObjectType($var)) !== '') { if ($this->isObjectClassStaticallyAssignableTo($rightClass, $leftClass)) { // A child object can be assigned to a parent typed object. diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index 7f365f5a..7eacf2f5 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -531,11 +531,21 @@ trait BinaryOpTrait [$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr); $this->appendCapturedStmtLinesToContext($beforeStmts); - $type = $this->getOrderedOperandTmpType($expr, (string) $value); - $tmpVar = $this->addTmpVar($type); + $nativeClass = $this->detectClassOfExpr($expr); + if ($this->isNativeObjectClass($nativeClass)) { + $type = $this->getNativeObjectPointerType($nativeClass); + $tmpVar = $this->genTmpVarName(); + $this->addLocalVar($tmpVar, $type); + $this->addNativeObject($tmpVar, $nativeClass); + } else { + $type = $this->getOrderedOperandTmpType($expr, (string) $value); + $tmpVar = $this->addTmpVar($type); + } $this->context->beforeStmtLines[] = $tmpVar . ' = ' . $value . ';'; $this->appendCapturedStmtLinesToContext($afterStmts); - if (in_array($type, [Type::VAR, Type::STR, Type::ARRAY, Type::OBJECT], true)) { + if ($this->isNativeObjectClass($nativeClass)) { + $this->context->afterStmtLines[] = $tmpVar . ' = nullptr;'; + } elseif (in_array($type, [Type::VAR, Type::STR, Type::ARRAY, Type::OBJECT], true)) { // The declaration is function-scoped, but PHP releases an owned // expression temporary after the statement that consumes it. // All zval-owning PHPX wrappers must be cleared here: an Object is @@ -632,6 +642,13 @@ trait BinaryOpTrait continue; } + $itemClass = $this->detectClassOfExpr($item); + if ($this->isNativeObjectClass($itemClass)) { + $toString = new Expr\MethodCall($item, new Node\Identifier('toString')); + $argList[] = $this->parseOrderedOperand($toString, false); + continue; + } + $type = $this->detectTypeOfExpr($item); // C++17 evaluates the braced-list elements in order. The temporary // is still required because lowering a later operand may append @@ -782,6 +799,17 @@ trait BinaryOpTrait } $left = $this->parseCompareExpr($expr->left); $right = $this->parseCompareExpr($expr->right); + $leftIsNative = $this->isNativeObjectClass($this->detectClassOfExpr($expr->left)); + $rightIsNative = $this->isNativeObjectClass($this->detectClassOfExpr($expr->right)); + if ($leftIsNative && $this->isNull($expr->right)) { + return '(' . $left . ') == nullptr'; + } + if ($rightIsNative && $this->isNull($expr->left)) { + return '(' . $right . ') == nullptr'; + } + if ($leftIsNative && $rightIsNative) { + return 'static_cast(' . $left . ') == static_cast(' . $right . ')'; + } if ($right === 'nullptr') { // The left operand may itself be an assignment or another compound // expression. Parenthesize it before invoking Variant::isNull(), or diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index 76291ad6..db4d58fc 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -80,6 +80,18 @@ trait FunctionCallTrait return $pythonObjectCall; } + $callableClass = $this->detectClassOfExpr($expr->name); + if ($this->isNativeObjectClass($callableClass)) { + if ($expr->isFirstClassCallable()) { + $this->fatalError($expr, 'Native object callables cannot be converted to Zend closures'); + } + return $this->parseMethodCall(new Expr\MethodCall( + $expr->name, + new Node\Identifier('__invoke'), + $expr->args, + )); + } + if ($this->isVarExpr($expr->name)) { $fn = $this->parseIdentifier($expr->name); $placeHolder = $fn; diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index d3fd56d3..88d391d0 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -287,6 +287,24 @@ trait MethodCallTrait $this->fatalError($expr, 'Cannot call parent method because class `' . $this->classDef->name . '` does not extend any class'); } $parentClass = $this->classDef->extends; + if ($this->classDef->nativeObject) { + if (!$this->isIdExpr($expr->name)) { + $this->fatalError($expr, 'Dynamic parent method calls are not supported for native objects'); + } + $method = $this->parseIdentifier($expr->name); + if (strtolower($method) === '__destruct') { + $this->fatalError($expr, 'Explicit calls to native object destructors are not supported'); + } + $nativeFunc = $this->getNativeMethod($expr, $parentClass, $method); + if ($nativeFunc === false) { + $this->fatalError($expr, "Native parent class `{$parentClass}` has no method `{$method}()`"); + } + if ($expr->args === []) { + return self::PREFIX . $nativeFunc . '(this_)'; + } + return self::PREFIX . $nativeFunc . '(this_, ' + . $this->parseNativeCallArgs($expr->args, $nativeFunc) . ')'; + } $staticCall = false; if ($this->isIdExpr($expr->name)) { $method = $this->parseIdentifier($expr->name); @@ -348,12 +366,20 @@ trait MethodCallTrait } $class = ''; + $materializedNativeReceiver = false; // C++17 sequences a member-call receiver before its arguments, but // lowering an argument may hoist captured beforeStmtLines ahead of the // whole call. Materialize an effectful receiver before parsing args. - $object = empty($expr->args) - ? $this->parseIdentifier($expr->var) - : $this->parseOrderedOperand($expr->var, false); + $receiverClass = !$this->isVarExpr($expr->var) ? $this->detectClassOfExpr($expr->var) : ''; + if ($this->isNativeObjectClass($receiverClass)) { + $object = $this->materializeNativeObjectReceiver($expr->var, $receiverClass); + $class = $receiverClass; + $materializedNativeReceiver = true; + } else { + $object = empty($expr->args) + ? $this->parseIdentifier($expr->var) + : $this->parseOrderedOperand($expr->var, false); + } if ($this->isVarExpr($expr->var)) { if (!$this->hasVar($object)) { $this->errorUndefinedVariable($expr->var); @@ -379,6 +405,27 @@ trait MethodCallTrait ); } + if ($class !== '' && $this->isNativeObjectClass($class)) { + if (!$this->isNamedMethod($expr->name)) { + $this->fatalError($expr, 'Dynamic native object method calls are not supported'); + } + $nativeMethodName = strtolower($expr->name->toString()); + if ($nativeMethodName === '__destruct') { + $this->fatalError($expr, 'Explicit calls to native object destructors are not supported'); + } + $nativeKeyword = $expr->name->toString(); + if (isset(self::KEYWORD_METHOD_MAP[$nativeKeyword])) { + if (!isset(self::KEYWORD_METHOD_WITH_ARGUMENTS[$nativeKeyword]) && $expr->args !== []) { + $this->fatalError($expr, "The {$nativeKeyword} method does not accept parameters"); + } + $resolvedKeyword = $this->resolveNativeObjectKeywordMethod($expr, $class, $nativeKeyword); + if ($resolvedKeyword !== $nativeKeyword) { + $expr->name = new Node\Identifier($resolvedKeyword, $expr->name->getAttributes()); + } + $expr->setAttribute('nativeKeywordCall', true); + } + } + $magicMethod = false; $method = $this->identifierToStr($expr->name, literal: true); @@ -395,7 +442,10 @@ trait MethodCallTrait $receiverType = Type::VAR; } $keywordType = $this->findKeywordMethod($methodName); - if ($keywordType !== null && isset(self::KEYWORD_METHOD_MAP[$methodName])) { + if ($keywordType !== null + && isset(self::KEYWORD_METHOD_MAP[$methodName]) + && !$expr->getAttribute('nativeKeywordCall', false) + ) { if (!isset(self::KEYWORD_METHOD_WITH_ARGUMENTS[$methodName]) && $expr->args !== []) { $this->fatalError($expr, "The {$methodName} method does not accept parameters"); } @@ -448,8 +498,36 @@ trait MethodCallTrait } // 可转为原生调用的 MethodCall - if ($this->isVarExpr($expr->var) and $this->isNamedMethod($expr->name)) { + if (($this->isVarExpr($expr->var) || $materializedNativeReceiver) and $this->isNamedMethod($expr->name)) { $type = $this->getVarType($object); + if ($class !== '' && $this->isNativeObjectClass($class)) { + $methodName = $expr->name->toString(); + // Native objects have their own C++ virtual thunk for an + // overridden family; do not let the Zend-object devirtualizer + // downgrade this call to the dynamic path. + $nativeFunc = $this->getNativeMethod($expr, $class, $methodName); + if ($nativeFunc === false) { + $this->fatalError($expr, "Native class `{$class}` has no method `{$methodName}()`"); + } + $expr->setAttribute('nativeCall', $nativeFunc); + $nativeFunctionDef = $this->getFunction($nativeFunc); + $declaringClass = $this->getClass($nativeFunctionDef->declaringClass); + $declaringMethod = $declaringClass->getMethod($methodName); + if ($this->isNativeVirtualMethod($declaringClass, $declaringMethod)) { + $call = $this->getNativeObjectMemberReceiver($object) + . $this->getNativeVirtualMethodName($methodName); + if ($expr->args === []) { + return $call . '()'; + } + return $call . '(' . $this->parseNativeCallArgs($expr->args, $nativeFunc) . ')'; + } + $receiver = $this->getNativeObjectReceiver($object); + if ($expr->args === []) { + return self::PREFIX . $nativeFunc . '(' . $receiver . ')'; + } + return self::PREFIX . $nativeFunc . '(' . $receiver . ', ' + . $this->parseNativeCallArgs($expr->args, $nativeFunc) . ')'; + } // 引用参数允许方法调用:有class信息走原生调用,无class信息走动态调用 if (!$this->checkArgType($type, Type::OBJECT) and $type !== Type::REF) { $methodName = $expr->name->toString(); diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 24274944..a7c99be9 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -780,6 +780,12 @@ trait PropertyAccessTrait $lines[] = $array . '.offsetUnset(' . $dim . ');'; } } elseif ($this->isPropertyFetch($var)) { + if ($this->isVarExpr($var->var)) { + $nativeObject = $this->parseIdentifier($var->var); + if ($this->isNativeObjectVar($nativeObject)) { + $this->fatalError($var, 'Native object properties cannot be unset'); + } + } // unset has its own unconditional readonly diagnostic below; // it is forbidden even while __construct is running. $propertyWriteTarget = $this->preparePropertyWriteTarget($var, true); @@ -828,7 +834,9 @@ trait PropertyAccessTrait $this->errorUndefinedVariable($var); } $type = $this->getVarType($name); - if ($this->isNativeType($type)) { + if ($this->isNativeObjectVar($name)) { + $lines[] = "{$name} = nullptr;"; + } elseif ($this->isNativeType($type)) { $this->warning($var, "Variable of native type `\${$name}` cannot be unset"); } elseif ($type === Type::OBJECT) { // A PHP local read after unset() evaluates to null (and may @@ -925,17 +933,52 @@ trait PropertyAccessTrait $object = $expr->var; $property = $expr->name; - $id = $this->getPropertyIdentifier($expr, $object, $property); + $nativeExpressionClass = !$this->isVarExpr($object) ? $this->detectClassOfExpr($object) : ''; + if ($this->isNativeObjectClass($nativeExpressionClass)) { + if (!$property instanceof Node\Identifier) { + $this->fatalError($expr, 'Dynamic native object property access is not supported'); + } + $propertyName = $property->toString(); + $resolution = $this->resolveNativeInstanceProperty($expr, $propertyName, $nativeExpressionClass); + if ($resolution === null) { + $this->fatalError( + $expr, + "Native class `{$nativeExpressionClass}` has no property `\${$propertyName}`" + ); + } + $id = $this->applyNativePropertyAccessResult($expr, $resolution); + } else { + $id = $this->getPropertyIdentifier($expr, $object, $property); + } $hook = $this->getPropertyHookGetter($expr); if ($hook !== null) { return $this->emitPropertyHookGetterCall($expr, $hook); } + if ($this->isNativeObjectClass($nativeExpressionClass)) { + $objectName = $this->materializeNativeObjectReceiver($object, $nativeExpressionClass); + return $this->getNativeObjectMemberReceiver($objectName) + . $this->getNativeObjectPropertyCppName($resolution->propertyDef, $resolution->classDef); + } + $update = $this->isPropertyFetchUpdate($expr); $objectName = $update ? $this->parseWritableIdentifier($object) : $this->parseIdentifier($object); if ($this->isVarExpr($object) and !$this->hasVar($objectName)) { $this->errorUndefinedVariable($object); } + if ($this->isVarExpr($object) && $this->isNativeObjectVar($objectName)) { + if (!$property instanceof Node\Identifier) { + $this->fatalError($expr, 'Dynamic native object property access is not supported'); + } + $propertyName = $property->toString(); + $class = $this->getNativeObjectVarClass($objectName); + $resolution = $this->resolveNativeInstanceProperty($expr, $propertyName, $class); + if ($resolution === null) { + $this->fatalError($expr, "Native class `{$class}` has no property `\${$propertyName}`"); + } + return $this->getNativeObjectMemberReceiver($objectName) + . $this->getNativeObjectPropertyCppName($resolution->propertyDef, $resolution->classDef); + } $objectVar = $objectName; if ($this->usesTraitPropertyScope($objectVar)) { $getProperty = 'typephp_read_property_scoped(' diff --git a/src/Parser/SelectionExpressionTrait.php b/src/Parser/SelectionExpressionTrait.php index d7e1caaf..c9a6bd61 100644 --- a/src/Parser/SelectionExpressionTrait.php +++ b/src/Parser/SelectionExpressionTrait.php @@ -24,7 +24,9 @@ trait SelectionExpressionTrait $this->assertExprCanBeUsedAsValue($expr->else, 'ternary branch'); $ifType = $this->detectTypeOfExpr($expr->if); $elseType = $this->detectTypeOfExpr($expr->else); - $typeChanged = $ifType !== $elseType; + $nativeClass = $this->detectClassOfExpr($expr); + $nativeSelection = $this->isNativeObjectClass($nativeClass); + $typeChanged = !$nativeSelection && $ifType !== $elseType; [$cond, $condBeforeStmts, $condAfterStmts] = $this->parseExprWithCapturedStmts($expr->cond); $ifBeforeStmtCount = count($this->context->beforeStmtLines); $ifAfterStmtCount = count($this->context->afterStmtLines); @@ -42,6 +44,15 @@ trait SelectionExpressionTrait $this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $elseBeforeStmtCount); $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $elseAfterStmtCount); + if ($nativeSelection) { + if ($this->isNull($expr->if)) { + $if = 'nullptr'; + } + if ($this->isNull($expr->else)) { + $else = 'nullptr'; + } + } + $hasBranchStmts = $condBeforeStmts || $condAfterStmts || $ifBeforeStmts || $ifAfterStmts || $elseBeforeStmts || $elseAfterStmts; if (!$hasBranchStmts && $typeChanged) { $if = 'php::Var(' . $if . ')'; @@ -50,7 +61,9 @@ trait SelectionExpressionTrait if ($hasBranchStmts) { // REF and VOID are expression implementation types, not valid // by-value result types for the materializing lambda. - $ternaryType = $this->getNormalAssignType($typeChanged ? Type::VAR : $ifType); + $ternaryType = $nativeSelection + ? $this->getNativeObjectPointerType($nativeClass) + : $this->getNormalAssignType($typeChanged ? Type::VAR : $ifType); $code = '[&]() -> ' . $ternaryType . '{'; $code .= $this->formatCapturedStmtLines($condBeforeStmts); if ($condAfterStmts) { @@ -61,9 +74,9 @@ trait SelectionExpressionTrait } $cond = $this->convertConditionExpr($expr->cond, $cond); $code .= $this->getIndent() . 'if (' . $cond . ') {'; - $code .= $this->formatTernaryReturn($expr->if, $if, $ifBeforeStmts, $ifAfterStmts, $ternaryType, $ifType); + $code .= $this->formatTernaryReturn($expr->if, $if, $ifBeforeStmts, $ifAfterStmts, $ternaryType, $ifType, $nativeClass); $code .= $this->getIndent() . '} else {'; - $code .= $this->formatTernaryReturn($expr->else, $else, $elseBeforeStmts, $elseAfterStmts, $ternaryType, $elseType); + $code .= $this->formatTernaryReturn($expr->else, $else, $elseBeforeStmts, $elseAfterStmts, $ternaryType, $elseType, $nativeClass); $code .= $this->getIndent() . '}'; $code .= $this->getIndent() . '}()'; return $code; @@ -79,16 +92,23 @@ trait SelectionExpressionTrait array $afterStmts, string $returnType, string $valueType, + string $nativeClass = '', ): string { $code = $this->formatCapturedStmtLines($beforeStmts); $returnsReference = $valueType === Type::REF || ($valueExpr instanceof Expr\CallLike && $this->resolveRefReturningCall($valueExpr) !== false); - if ($returnType !== Type::VAR && ($beforeStmts || $afterStmts)) { + if ($nativeClass === '' && $returnType !== Type::VAR && ($beforeStmts || $afterStmts)) { $value = $this->convertExprFromType($returnType, $value); } if ($afterStmts || $returnsReference) { - $tmpVar = $this->addTmpVar($returnType); + if ($nativeClass !== '') { + $tmpVar = $this->genTmpVarName(); + $this->addLocalVar($tmpVar, $returnType); + $this->addNativeObject($tmpVar, $nativeClass); + } else { + $tmpVar = $this->addTmpVar($returnType); + } $code .= $this->getIndent() . "{$tmpVar} = {$value};"; $code .= $this->formatCapturedStmtLines($afterStmts); $code .= $this->getIndent() . 'return ' . $tmpVar . ';'; @@ -102,6 +122,11 @@ trait SelectionExpressionTrait protected function parseMatch(Expr\Match_ $expr): string { + $nativeClass = $this->detectClassOfExpr($expr); + $nativeSelection = $this->isNativeObjectClass($nativeClass); + $returnType = $nativeSelection + ? $this->getNativeObjectPointerType($nativeClass) + : Type::VAR; $this->assertExprCanBeUsedAsValue($expr->cond, 'match condition'); $var = $this->parseIdentifier($expr->cond); if ($this->isVarExpr($expr->cond)) { @@ -114,7 +139,7 @@ trait SelectionExpressionTrait $var = $tmpVar; } - $code = '[&]() -> ' . Type::VAR . '{'; + $code = '[&]() -> ' . $returnType . '{'; $default = null; foreach ($expr->arms as $arm) { if ($arm->conds === null) { @@ -141,29 +166,40 @@ trait SelectionExpressionTrait $code .= $this->getIndent() . '}'; } $code .= $this->getIndent() . 'if (' . $matched . ') {'; - $code .= $this->formatMatchReturn($arm->body); + $code .= $this->formatMatchReturn($arm->body, $nativeClass); $code .= $this->getIndent() . '}'; } if ($default) { $code .= $this->getIndent() . '{'; - $code .= $this->formatMatchReturn($default); + $code .= $this->formatMatchReturn($default, $nativeClass); $code .= $this->getIndent() . '}'; } else { - $code .= $this->getIndent() . '{ return php::throwException("UnhandledMatchError", "Unhandled match case"); }'; + $code .= $nativeSelection + ? $this->getIndent() . '{ php::throwException("UnhandledMatchError", "Unhandled match case"); return nullptr; }' + : $this->getIndent() . '{ return php::throwException("UnhandledMatchError", "Unhandled match case"); }'; } $code .= '}()'; return $code; } - protected function formatMatchReturn(NodeAbstract $body): string + protected function formatMatchReturn(NodeAbstract $body, string $nativeClass = ''): string { $this->assertExprCanBeUsedAsValue($body, 'match arm'); [$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($body); + if ($nativeClass !== '' && $this->isNull($body)) { + $value = 'nullptr'; + } $code = $this->formatCapturedStmtLines($beforeStmts); if ($afterStmts) { - $tmpVar = $this->addTmpVar(Type::VAR); + if ($nativeClass !== '') { + $tmpVar = $this->genTmpVarName(); + $this->addLocalVar($tmpVar, $this->getNativeObjectPointerType($nativeClass)); + $this->addNativeObject($tmpVar, $nativeClass); + } else { + $tmpVar = $this->addTmpVar(Type::VAR); + } $code .= $this->getIndent() . "{$tmpVar} = {$value};"; $code .= $this->formatCapturedStmtLines($afterStmts); $code .= $this->getIndent() . 'return ' . $tmpVar . ';'; @@ -178,6 +214,10 @@ trait SelectionExpressionTrait { $this->assertExprCanBeUsedAsValue($left, 'selection value'); $this->assertExprCanBeUsedAsValue($right, 'selection value'); + $nativeClass = $this->detectClassOfExpr($expr); + if ($this->isNativeObjectClass($nativeClass)) { + return $this->parseNativeValueSelection($left, $right, $nativeClass); + } $leftExpr = $this->parseIdentifier($left); if ($this->isVarExpr($left)) { $this->checkVarMustExist($left, $leftExpr); @@ -231,4 +271,38 @@ trait SelectionExpressionTrait return $tmpVar; } + protected function parseNativeValueSelection(Expr $left, Expr $right, string $nativeClass): string + { + $pointerType = $this->getNativeObjectPointerType($nativeClass); + [$leftValue, $leftBefore, $leftAfter] = $this->parseExprWithCapturedStmts($left); + [$rightValue, $rightBefore, $rightAfter] = $this->parseExprWithCapturedStmts($right); + if ($this->isNull($left)) { + $leftValue = 'nullptr'; + } + if ($this->isNull($right)) { + $rightValue = 'nullptr'; + } + + $leftTmp = $this->genTmpVarName(); + $this->addLocalVar($leftTmp, $pointerType); + $this->addNativeObject($leftTmp, $nativeClass); + + $code = '[&]() -> ' . $pointerType . '{'; + $code .= $this->formatCapturedStmtLines($leftBefore); + $code .= $this->getIndent() . $leftTmp . ' = ' . $leftValue . ';'; + $code .= $this->formatCapturedStmtLines($leftAfter); + $code .= $this->getIndent() . 'if (' . $leftTmp . ' != nullptr) { return ' . $leftTmp . '; }'; + $code .= $this->formatCapturedStmtLines($rightBefore); + if ($rightAfter) { + $rightTmp = $this->genTmpVarName(); + $this->addLocalVar($rightTmp, $pointerType); + $this->addNativeObject($rightTmp, $nativeClass); + $code .= $this->getIndent() . $rightTmp . ' = ' . $rightValue . ';'; + $code .= $this->formatCapturedStmtLines($rightAfter); + $rightValue = $rightTmp; + } + $code .= $this->getIndent() . 'return ' . $rightValue . ';'; + return $code . $this->getIndent() . '}()'; + } + } diff --git a/src/Parser/TypeConversionTrait.php b/src/Parser/TypeConversionTrait.php index 2985bd8b..03222e5d 100644 --- a/src/Parser/TypeConversionTrait.php +++ b/src/Parser/TypeConversionTrait.php @@ -15,6 +15,21 @@ use PhpParser\NodeAbstract; trait TypeConversionTrait { + protected function parseExprToString(NodeAbstract $node): string + { + $class = $this->detectClassOfExpr($node); + if ($this->isNativeObjectClass($class)) { + return $this->parseMethodCall(new Node\Expr\MethodCall( + $node, + new Node\Identifier('toString'), + )); + } + return $this->convertExprToStringByType( + $this->parseExprAsValue($node), + $this->detectTypeOfExpr($node), + ); + } + protected function convertExprToStringByType(string $expr, $type): string { if ($type === Type::STR) { diff --git a/src/Parser/UnaryExpressionTrait.php b/src/Parser/UnaryExpressionTrait.php index 4b7f7ad7..d2e1e868 100644 --- a/src/Parser/UnaryExpressionTrait.php +++ b/src/Parser/UnaryExpressionTrait.php @@ -54,10 +54,7 @@ trait UnaryExpressionTrait protected function parseCastString(Expr\Cast\String_ $node): string { $this->assertExprCanBeUsedAsValue($node->expr, 'cast operand'); - return $this->convertExprToStringByType( - $this->parseExprAsValue($node->expr), - $this->detectTypeOfExpr($node->expr) - ); + return $this->parseExprToString($node->expr); } protected function parseCastBool(Expr\Cast\Bool_ $node): string diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 07f21134..ef58806e 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -21,6 +21,7 @@ use TypePhp\Entity\PropertyDef; use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic; use TypePhp\Exception\SyntaxError; use TypePhp\Transform\PropertyHookLowering; +use TypePhp\Transform\NativeClassAttributeLowering; use TypePhp\Transform\PrinterLowering; use TypePhp\Transform\ArrayableLowering; use TypePhp\Transform\ClassFieldSelection; @@ -77,6 +78,10 @@ class Preprocessor extends CompilerBase protected function genArgumentDeclaration(ArgInfo $argInfo): string { + $nativeObjectType = $this->getNativeObjectArgumentType($argInfo); + if ($nativeObjectType !== null) { + return $nativeObjectType . $argInfo->name; + } $type = $argInfo->type; if ($type === Type::STREAM || $type === Type::BOX) { $type = Type::VAR; @@ -392,9 +397,6 @@ class Preprocessor extends CompilerBase protected function parseParameterType(Node\Param $param, ArgInfo $argInfo, string $var): string { - if ($param->byRef) { - return Type::REF; - } // Capture the late-bound parameter type keyword *before* resolveTypeDecl // runs, because resolveTypeDecl mutates the `self`/`static`/`parent` node // name to the declaring class when the method belongs to a trait. @@ -406,6 +408,12 @@ class Preprocessor extends CompilerBase } } [$type, $class] = $this->resolveTypeDecl($param->type, self::DECL_TYPE_OF_PARAM); + $this->assertSupportedNativeObjectTypeNode($param->type, self::DECL_TYPE_OF_PARAM, $param); + $nullableNative = $this->resolveNullableNativeObjectType($param->type, self::DECL_TYPE_OF_PARAM); + if ($nullableNative !== null) { + [$type, $class] = $nullableNative; + $argInfo->nullable = true; + } $argInfo->undeclared = $param->type === null; if ( $param->type !== null @@ -424,7 +432,10 @@ class Preprocessor extends CompilerBase // Record late-bound parameter type keywords so they can be re-resolved // to the consuming class when a trait method is flattened into a class. $argInfo->typeKeyword = $typeKeyword; - return $type; + // Ordinary PHP references use php::Ref at the native ABI. Keep the + // resolved declaration metadata above, however: Native object + // references need it to lower to `native_struct *&` instead. + return $param->byRef ? Type::REF : $type; } /** @@ -499,7 +510,7 @@ class Preprocessor extends CompilerBase } if ($param->type instanceof NullableType || $param->type instanceof UnionType || $param->type instanceof IntersectionType) { $typeInfo = $this->buildTypeCheckFromNode($param->type); - if (!empty($typeInfo['check'])) { + if (!empty($typeInfo['check']) && !$this->isNativeObjectClass($argInfo->declaredClass)) { $argInfo->typeCheck = $typeInfo['check']; $argInfo->typeStr = $typeInfo['typeStr']; $argInfo->typeNode = $param->type; @@ -585,6 +596,14 @@ class Preprocessor extends CompilerBase } } [$returnType, $class] = $this->resolveTypeDecl($v->returnType, self::DECL_TYPE_OF_RETURN); + $this->assertSupportedNativeObjectTypeNode($v->returnType, self::DECL_TYPE_OF_RETURN, $v); + $nullableNativeReturn = $this->resolveNullableNativeObjectType( + $v->returnType, + self::DECL_TYPE_OF_RETURN, + ); + if ($nullableNativeReturn !== null) { + [$returnType, $class] = $nullableNativeReturn; + } // 构造、析构、克隆方法不能有返回值 if ($this->method and in_array($this->method, ['__construct', '__destruct', '__clone'])) { $returnType = Type::VOID; @@ -607,6 +626,7 @@ class Preprocessor extends CompilerBase } $functionDef->exported = !($this->classDef?->exported === false || $this->hasNoExportAttribute($v)); $functionDef->returnClass = $class; + $functionDef->returnNullable = $nullableNativeReturn !== null; $functionDef->returnTypeStr = $v->returnType === null ? '' : $this->typeCheckNodeToString($v->returnType); @@ -622,6 +642,7 @@ class Preprocessor extends CompilerBase } if (!$functionDef->generator + && !$this->isNativeObjectClass($functionDef->returnClass) && ($v->returnType instanceof NullableType || $v->returnType instanceof UnionType || $v->returnType instanceof IntersectionType)) { @@ -641,6 +662,14 @@ class Preprocessor extends CompilerBase $this->parseParams($v->params, $functionDef); + if ($this->classDef !== null + && !$this->classDef->nativeObject + && strtolower($fnName) === '__construct' + && $this->functionUsesNativeObject($functionDef) + ) { + $this->fatalError($v, 'Zend-backed constructors cannot accept or return native objects'); + } + // main 函数,返回值必须为 void 类型,参数必须为空或者 argc, argv 两个参数 if (!$this->class and !$this->namespace and $fnName === self::ENTRY_FUNCTION) { if (count($v->params) > 0) { @@ -741,6 +770,9 @@ class Preprocessor extends CompilerBase $functionDef->sourceFile = $this->file; $functionDef->startLine = $v->getStartLine(); $functionDef->method = $this->methodDef !== null; + $functionDef->declaringClass = $functionDef->method + ? $this->classDef->getNamespacedName(false) + : ''; $functionDef->displayName = $functionDef->method ? $this->classDef->getNamespacedName(false) . '::' . $functionDef->name : $functionDef->getNamespacedName(); @@ -767,6 +799,7 @@ class Preprocessor extends CompilerBase } $this->classDef = new ClassDef($this->class, $flags, $this->namespace); + $this->classDef->nativeObject = NativeClassAttributeLowering::isNative($class); $this->classDef->exported = !$this->hasNoExportAttribute($class); $this->classDef->methodsForTarget = $this->parseMethodsForTarget($class); $this->addClass($fullClassName, $this->classDef); @@ -1200,6 +1233,21 @@ class Preprocessor extends CompilerBase { $flags = $this->parseModifiers($flags); [$type, $class] = $this->resolveTypeDecl($typeNode, self::DECL_TYPE_OF_PROPERTY); + $this->assertSupportedNativeObjectTypeNode($typeNode, self::DECL_TYPE_OF_PROPERTY, $errorNode); + $nullableNative = $this->resolveNullableNativeObjectType( + $typeNode, + self::DECL_TYPE_OF_PROPERTY, + ); + if ($nullableNative !== null) { + [$type, $class] = $nullableNative; + $nullable = true; + } + if ($this->isNativeObjectClass($class) && !$this->classDef->nativeObject) { + $this->fatalError( + $errorNode, + 'Native object types can only be used as properties of native classes', + ); + } $default = null; $arrayInitPlan = null; @@ -1440,6 +1488,23 @@ class Preprocessor extends CompilerBase protected function parseClassPropertyDef(Node\Stmt\Property $v): void { + if ($this->classDef->nativeObject) { + if ($v->type === null) { + $this->fatalError($v, 'Native class properties must declare a type'); + } + if ($v->isStatic()) { + $this->fatalError($v, 'Native class static properties are not supported'); + } + // A Zend readonly property carries runtime initialization state. + // Native fields deliberately have no Zend property slot, so a raw + // C++ assignment would silently bypass the readonly contract. + // Reject it until Native objects have an equally explicit state + // representation instead of emitting behavior that only appears + // readonly at compile time. + if ($v->isReadonly() || ($this->classDef->flags & Modifiers::READONLY)) { + $this->fatalError($v, 'Native class readonly properties are not supported'); + } + } $oriCtx = $this->context; $this->context = $this->classDef->propertyContext; $nullable = $v->type instanceof NullableType; @@ -1447,6 +1512,12 @@ class Preprocessor extends CompilerBase foreach ($v->props as $prop) { $propName = $this->parseIdentifier($prop->name); $propDef = $this->addClassProperty($propName, $v->flags, $v->type, $prop->default, $nullable, $v); + if ($this->classDef->nativeObject && $this->isNativeObjectForbiddenPropertyType($propDef)) { + $message = $propDef->type === Type::BOX + ? 'Native class properties cannot use Box types' + : 'Native class properties cannot use Std Container types'; + $this->fatalError($v, $message); + } $hookMetadata = $v->getAttribute(PropertyHookLowering::PROPERTY_ATTRIBUTE, []); $propDef->virtual = (bool) ($hookMetadata['virtual'] ?? false); foreach ($v->hooks as $hook) { @@ -1467,8 +1538,12 @@ class Preprocessor extends CompilerBase $this->resetMethod(); $name = $this->getMethodName($v); $this->method = $name; + $this->assertNativeMagicMethodSupported($v, $name); $flags = $this->parseModifiers($v->flags); $abstract = $flags & Modifiers::ABSTRACT; + if ($this->classDef->nativeObject && ($flags & Modifiers::STATIC)) { + $this->fatalError($v, 'Native class static methods are not supported'); + } if (!$abstract) { $this->methodDef = new MethodDef($flags, $name); diff --git a/src/Transform/CompileTimeAttributeRegistry.php b/src/Transform/CompileTimeAttributeRegistry.php index f85bdc86..79f481c4 100644 --- a/src/Transform/CompileTimeAttributeRegistry.php +++ b/src/Transform/CompileTimeAttributeRegistry.php @@ -72,6 +72,7 @@ final class CompileTimeAttributeRegistry ]; }; + $add('Native', [self::TARGET_NAMED_CLASS], 'Native can only be applied to named classes', self::ARGUMENTS_NONE, self::PHASE_PREPROCESS, false); $add('MethodsFor', [self::TARGET_NAMED_CLASS], 'MethodsFor can only be applied to classes', self::ARGUMENTS_METHODS_FOR, self::PHASE_PREPROCESS); $add('NoExport', [self::TARGET_CLASS_LIKE, self::TARGET_FUNCTION, self::TARGET_METHOD], 'NoExport can only be applied to classes, functions, or methods', self::ARGUMENTS_NONE, self::PHASE_PREPROCESS, false); $add('WasmExport', [self::TARGET_FUNCTION], 'WasmExport can only be applied to named functions', self::ARGUMENTS_WASM_EXPORT, self::PHASE_PREPROCESS, false); diff --git a/src/Transform/NativeClassAttributeLowering.php b/src/Transform/NativeClassAttributeLowering.php new file mode 100644 index 00000000..0e05bad0 --- /dev/null +++ b/src/Transform/NativeClassAttributeLowering.php @@ -0,0 +1,39 @@ +setAttribute(self::ATTRIBUTE, true); + } + } + + public static function isNative(Node $node): bool + { + return $node instanceof Node\Stmt\Class_ + && $node->getAttribute(self::ATTRIBUTE, false) === true; + } +} diff --git a/src/Transform/Visitor.php b/src/Transform/Visitor.php index 611e8081..0ac5d414 100644 --- a/src/Transform/Visitor.php +++ b/src/Transform/Visitor.php @@ -28,6 +28,7 @@ class Visitor extends NodeVisitorAbstract public function enterNode(Node $node): null { $this->guard($node, static fn () => CompileTimeAttribute::validateNode($node)); + $this->guard($node, static fn () => NativeClassAttributeLowering::lower($node), 'Native'); $this->guard($node, static fn () => FunctionAttributeLowering::lower($node)); $this->guard($node, static fn () => GetterLowering::validateTarget($node), 'Getter'); $this->guard($node, static fn () => PropertyMethodLowering::validateTarget($node)); diff --git a/src/Translator.php b/src/Translator.php index c1b84b08..ef7d8cac 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -733,7 +733,13 @@ class Translator extends Preprocessor } foreach ($this->globalVars as $name => $type) { - $lines[] = 'extern THREAD_LOCAL ' . Type::VAR . ' ' . $this->escapeGlobalVar($name) . ';'; + $cppType = isset($this->nativeGlobalObjects[$name]) + ? $this->getNativeObjectPointerType($this->nativeGlobalObjects[$name]) + : Type::VAR; + $lines[] = 'extern THREAD_LOCAL ' . $cppType . ' ' . $this->escapeGlobalVar($name) . ';'; + } + foreach ($this->nativeStaticInitializers as $name => $_) { + $lines[] = 'extern THREAD_LOCAL bool ' . $this->escapeGlobalVar($name) . ';'; } if ($this->literalStrings) { @@ -811,7 +817,14 @@ class Translator extends Preprocessor $code .= "// global vars \n"; foreach ($this->globalVars as $name => $type) { - $code .= 'THREAD_LOCAL ' . Type::VAR . ' ' . $this->escapeGlobalVar($name) . ';' . PHP_EOL; + $cppType = isset($this->nativeGlobalObjects[$name]) + ? $this->getNativeObjectPointerType($this->nativeGlobalObjects[$name]) + : Type::VAR; + $code .= 'THREAD_LOCAL ' . $cppType . ' ' . $this->escapeGlobalVar($name) + . (isset($this->nativeGlobalObjects[$name]) ? ' = nullptr' : '') . ';' . PHP_EOL; + } + foreach ($this->nativeStaticInitializers as $name => $_) { + $code .= 'THREAD_LOCAL bool ' . $this->escapeGlobalVar($name) . ' = false;' . PHP_EOL; } $code .= "// class register functions \n"; @@ -916,6 +929,9 @@ CODE; $code .= "// class \n"; foreach ($this->getClassLikesWithConstants() as $classDef) { + if ($classDef instanceof ClassDef && $classDef->nativeObject) { + continue; + } if ($classDef instanceof ClassDef && !$classDef->trait && !$classDef->enum) { $code .= 'static zend_object* (*create_object_' . $classDef->getNamespacedName() . ")(zend_class_entry *class_type);\n"; $code .= 'static zend_object_handlers property_handlers_' . $classDef->getNamespacedName() . ";\n"; @@ -945,6 +961,9 @@ CODE; if ($functionDef->method) { continue; } + if ($this->functionUsesNativeObject($functionDef)) { + continue; + } $fullName = $functionDef->getNamespacedName(); $zifName = $this->escapeZendFnName($fullName); // TypePHP is always strict. Store the flag in the registered @@ -1006,6 +1025,11 @@ CODE; if ($name == 'GLOBALS') { continue; } + if (isset($this->nativeGlobalObjects[$name])) { + $code .= 'php::nativeGcRegisterRequestRoot(reinterpret_cast(&' + . $this->escapeGlobalVar($name) . '));' . PHP_EOL; + continue; + } $code .= 'php::initGlobal(' . $this->genCharPtr($name) . ', ' . $this->escapeGlobalVar($name) . ');' . PHP_EOL; } @@ -1053,10 +1077,17 @@ CODE; $code .= 'void php_app_clean() {' . PHP_EOL; foreach ($this->globalVars as $name => $type) { if ($name != 'GLOBALS') { + if (isset($this->nativeGlobalObjects[$name])) { + $code .= $this->escapeGlobalVar($name) . ' = nullptr;' . PHP_EOL; + continue; + } $code .= $this->escapeGlobalVar($name) . '.unset();' . PHP_EOL; $code .= 'php::unsetGlobal("' . $name . '");' . PHP_EOL; } } + foreach ($this->nativeStaticInitializers as $name => $_) { + $code .= $this->escapeGlobalVar($name) . ' = false;' . PHP_EOL; + } foreach ($this->constants as $name => $const) { if ($const->type !== Type::VAR) { continue; @@ -1693,6 +1724,8 @@ CODE; $code .= '#include ' . PHP_EOL; $code .= PHP_EOL; + $code .= $this->genNativeObjectDeclarations(); + if ($this->isBuildModeLib()) { $code .= $this->genLibraryApiMacro($this->targetName); } @@ -1712,7 +1745,7 @@ CODE; $functionDeclarationPrefix = $this->getFunctionDeclarationPrefix($func); $list = []; if ($func->method) { - $list[] = Type::OBJECT . ' &this_'; + $list[] = ($this->getNativeObjectMethodThisType($func) ?? (Type::OBJECT . ' &')) . 'this_'; } $argInfoList = $func->argInfoList; if ($argInfoList) { @@ -1731,7 +1764,10 @@ CODE; } $params = implode(', ', $list); $functionAttribute = $this->getFunctionOptimizationAttribute($func); - $code .= $functionDeclarationPrefix . $functionAttribute . ($func->returnsByRef ? Type::REF : $func->returnType) . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL; + $returnType = $func->returnsByRef + ? Type::REF + : ($this->getNativeObjectReturnType($func) ?? $func->returnType); + $code .= $functionDeclarationPrefix . $functionAttribute . $returnType . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL; if ($func->hasMultiReturn()) { $code .= 'namespace ' . self::MULTI_RETURN_NAMESPACE . ' {' . PHP_EOL; $code .= $functionDeclarationPrefix . $functionAttribute . $func->getMultiReturnCppType() . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL; @@ -1866,6 +1902,25 @@ CODE; return str_replace('-', '_', $rs); } + public function isNativeClassForStub(string $class): bool + { + return $this->isNativeObjectClass($class); + } + + public function isNativeFunctionForStub(string $function): bool + { + return $this->hasFunction($function) + && $this->functionUsesNativeObject($this->getFunction($function)); + } + + public function isNativeMethodForStub(string $class, string $method): bool + { + $class = ltrim($class, '\\'); + return $this->hasClass($class) + && $this->getClass($class)->hasMethod($method) + && $this->functionUsesNativeObject($this->getClass($class)->getMethod($method)->functionDef); + } + public function getArgInfoHeaderFile(string $file, bool $relative = false): string { $filePath = $this->getRelativePath(str_replace(['.stub.php', '.php'], '', $file)); @@ -2511,6 +2566,9 @@ CODE; } foreach ($this->classesDefineInFile as $classDef) { + if ($classDef->nativeObject) { + continue; + } $cppCode .= $this->genClassWrapper($classDef); } @@ -2519,7 +2577,7 @@ CODE; } foreach ($this->functionDefineInFile as $functionDef) { - if ($functionDef->attributeFactory) { + if ($functionDef->attributeFactory || $this->functionUsesNativeObject($functionDef)) { continue; } $cppCode .= $this->genFunctionWrapper($functionDef); @@ -2565,7 +2623,7 @@ CODE; } foreach ($this->symbols->classes() as $classDef) { - if ($classDef->trait !== null) { + if ($classDef->trait !== null || $classDef->nativeObject) { continue; } $ce = $this->getClassCe($classDef); @@ -3241,6 +3299,12 @@ CODE; $parentClass = $this->getNamespacedClassName($this->parseIdentifier($class->extends)); if ($this->hasClass($parentClass)) { $parent = $this->getClass($parentClass); + if ($this->classDef->nativeObject !== $parent->nativeObject) { + $this->fatalError( + $class, + 'Native and ZendVM-backed classes cannot inherit from each other' + ); + } // 父类是 final 无法继承 if ($parent->flags & Modifiers::FINAL) { $this->fatalError($class, "Class `{$this->class}` cannot extend final class `{$parentClass}`"); @@ -3325,6 +3389,9 @@ CODE; $this->checkInheritedAbstractMethodsAreImplemented($class); } $code = $this->genNativeMethod($methodCodes); + if ($this->classDef->nativeObject) { + $code .= $this->genNativeObjectRuntimeDefinition($this->classDef); + } $oriCtx = $this->context; $this->context = $this->classDef->propertyContext; @@ -3531,6 +3598,9 @@ CODE; // 接口没有方法实体 if ($classDef instanceof ClassDef && $classDef->trait === null) { + if ($classDef->nativeObject) { + return ''; + } $defaultPropCount = 0; foreach ($classDef->properties as $property) { if (!$property->isStatic() && $property->default !== null) { @@ -3542,6 +3612,9 @@ CODE; } $methods = $classDef->methods; foreach ($methods as $methodDef) { + if ($this->functionUsesNativeObject($methodDef->functionDef)) { + continue; + } $cppCode .= $this->genMethodWrapper($classDef, $methodDef); } } @@ -3643,10 +3716,18 @@ CODE; } if ($this->class) { - $this->addArgument('this_', Type::OBJECT); + if ($this->classDef->nativeObject) { + $this->addArgument('this_', $this->getNativeObjectCppName($this->classDef) . ' &'); + $this->addNativeObject('this_', $this->classDef->getNamespacedName(false)); + } else { + $this->addArgument('this_', Type::OBJECT); + } } foreach ($this->functionDef->argInfoList as $argInfo) { - $this->addArgument($argInfo->name, $argInfo->variadic ? Type::ARRAY : $argInfo->type); + $argumentType = $argInfo->variadic + ? Type::ARRAY + : ($this->getNativeObjectArgumentType($argInfo) ?? $argInfo->type); + $this->addArgument($argInfo->name, $argumentType); if (!$argInfo->variadic and $argInfo->declaredClass) { $this->addObject($argInfo->name, $argInfo->declaredClass); } @@ -3665,6 +3746,7 @@ CODE; $oriLocalVars = $this->context->localVars; $oriTmpVarIndex = $this->context->tmpVarIndex; $oriDeclaredObjects = $this->context->declaredObjects; + $oriNativeObjects = $this->context->nativeObjects; /** SSA/e-SSA analysis for the current function. Built once per function, discarded with the context. */ $ssaBuilder = new SsaBuilder($v->stmts, $this->functionDef->argInfoList); $ssaBuilder->build(); @@ -3677,7 +3759,12 @@ CODE; $this->optimizeLoopVars($ssaBuilder); $this->optimizeObjectProps($ssaBuilder); } - $this->context->resetAnalysisTemporaries($oriLocalVars, $oriTmpVarIndex, $oriDeclaredObjects); + $this->context->resetAnalysisTemporaries( + $oriLocalVars, + $oriTmpVarIndex, + $oriDeclaredObjects, + $oriNativeObjects, + ); } $stmts = ''; @@ -3699,21 +3786,29 @@ CODE; $multiReturn = $this->functionDef->hasMultiReturn(); $cppReturnType = $multiReturn ? $this->functionDef->getMultiReturnCppType() - : ($this->functionDef->returnsByRef ? Type::REF : $this->getReturnType()); + : ($this->functionDef->returnsByRef + ? Type::REF + : ($this->getNativeObjectReturnType($this->functionDef) ?? $this->getReturnType())); $nativeName = self::PREFIX . $name; $functionAttribute = $this->getFunctionOptimizationAttribute($this->functionDef); $functionDeclCode = $functionAttribute . $cppReturnType . ' ' . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '('; if ($this->class) { - $functionDeclCode .= Type::OBJECT . ' &this_'; + $functionDeclCode .= ($this->getNativeObjectMethodThisType($this->functionDef) + ?? (Type::OBJECT . ' &')) . 'this_'; if ($this->functionDef->params) { $functionDeclCode .= ', '; } } - $functionDeclCode .= $this->functionDef->params . ')'; + // Rebuild parameter declarations from ArgInfo at code-generation time. + // Native classes may be discovered after an earlier declaration was + // normalized; the final ABI must use the precise native pointer type, + // not a stale php::Object spelling cached during preprocessing. + $functionDeclCode .= $this->getNativeMethodParameterDeclarations($this->functionDef) . ')'; $code = $functionDeclCode . ' {' . PHP_EOL; $this->indentLevel++; $code .= $this->genScopeVarDecl(); + $code .= $this->genNativeObjectParameterChecks($this->functionDef); $code .= "\n"; // Runtime union/nullable parameter type checks foreach ($this->functionDef->argInfoList as $i => $argInfo) { @@ -4375,6 +4470,7 @@ CODE; private function checkInterfaceImplementation(NodeAbstract $node, ClassDef $classDef, string $interfaceName): void { if ($this->isInternalInterface($interfaceName)) { + $this->checkInternalInterfaceImplementation($node, $classDef, $interfaceName); return; } if (!$this->hasInterface($interfaceName)) { @@ -4581,11 +4677,17 @@ CODE; if ($chainNode->hasProperty($name)) { $parentProp = $chainNode->getProperty($name); // A parent private property would be a separate PHP slot - // hidden by the child declaration. TypePHP forbids that - // dual-slot model. Public/protected declarations instead + // hidden by the child declaration. Zend-backed TypePHP + // classes still forbid that dual-slot model, while Native + // classes have declaring-class-qualified C++ fields and + // can represent it without a runtime property table. + // Public/protected declarations instead // describe the same inherited property slot and must obey // PHP-compatible type, visibility and readonly rules. if ($parentProp->flags & Modifiers::PRIVATE) { + if ($classDef->nativeObject) { + continue; + } $this->fatalError($classStmt, "Declaration of `{$className}::\${$name}` conflicts with private property " . "`{$parentClass}::\${$name}`; property shadowing across inheritance is not allowed"); @@ -4764,11 +4866,15 @@ CODE; private function installComposedTraitMethod(Node\Stmt\ClassMethod $methodStmt): void { $name = $methodStmt->name->toString(); + $this->assertNativeMagicMethodSupported($methodStmt, $name); if ($this->classDef->hasMethod($name)) { return; } $flags = $this->parseModifiers($methodStmt->flags); + if ($this->classDef->nativeObject && ($flags & Modifiers::STATIC)) { + $this->fatalError($methodStmt, 'Native class static methods are not supported'); + } $methodDef = new MethodDef($flags, $name); $methodDef->node = $methodStmt; $methodDef->traitOrigin = (string) $methodStmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE, ''); diff --git a/src/TypeSystem/NativeTypeCompatibilityTrait.php b/src/TypeSystem/NativeTypeCompatibilityTrait.php index c1dda088..3158a5f5 100644 --- a/src/TypeSystem/NativeTypeCompatibilityTrait.php +++ b/src/TypeSystem/NativeTypeCompatibilityTrait.php @@ -159,6 +159,50 @@ trait NativeTypeCompatibilityTrait ); } + $declaredClass = $argInfo->declaredClass ?: $argInfo->class; + $argumentClass = $this->detectClassOfExpr($arg->value); + if ($this->isNativeObjectClass($argumentClass) + && !$this->isNativeObjectClass($declaredClass) + ) { + if ($declaredClass !== '' && $this->isInterface($declaredClass)) { + $this->fatalError( + $arg, + "Native objects cannot be converted to interface `{$declaredClass}`", + ); + } + $this->fatalError( + $arg, + 'Native objects cannot cross a PHP/ZendVM argument boundary', + ); + } + if ($this->isNativeObjectClass($declaredClass)) { + if ($argInfo->nullable && $this->isNull($arg->value)) { + return 'nullptr'; + } + $class = $argumentClass; + if ($class === '' || !$this->isNativeObjectClass($class) + || !$this->isObjectClassStaticallyAssignableTo($class, $declaredClass) + ) { + $argName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name); + $this->fatalError( + $arg, + "Argument `{$argName}` must be a native object of type `{$declaredClass}`" + ); + } + if ($argInfo->byRef) { + if (!$this->isVarExpr($arg->value)) { + $this->fatalError($arg, 'Native object reference arguments must be variables'); + } + $var = $this->parseIdentifier($arg->value); + if (!$this->isNativeObjectVar($var)) { + $this->fatalError($arg, 'Native object reference arguments must be typed native variables'); + } + return $var; + } + $expr = $this->parseOrderedArg($arg); + return $this->materializeCallArgValue($arg->value, $expr); + } + if ($argInfo->byRef) { if ($this->isReferenceWrapperCall($arg->value)) { $inner = $this->unwrapReferenceWrapperCall($arg->value, $arg); @@ -217,7 +261,6 @@ trait NativeTypeCompatibilityTrait } if ($argInfo->type === Type::OBJECT) { - $declaredClass = $argInfo->declaredClass ?: $argInfo->class; if ($declaredClass !== '') { $class = $this->detectDeclaredClassOfExpr($arg->value); if ($class !== '') { diff --git a/src/gen_stub.php b/src/gen_stub.php index e9dda816..273e0f63 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -4553,6 +4553,9 @@ class FileInfo { } if ($stmt instanceof Stmt\Function_) { + if (getTranslator()->isNativeFunctionForStub($stmt->namespacedName->toString())) { + continue; + } $this->funcInfos[] = parseFunctionLike( $prettyPrinter, new FunctionName($stmt->namespacedName), @@ -4574,6 +4577,13 @@ class FileInfo { if ($stmt instanceof Stmt\ClassLike) { $className = $stmt->namespacedName; + // #[Native] classes are C++-only types. They intentionally have + // no zend_class_entry, arginfo method table or Zend wrappers. + if ($stmt instanceof Class_ + && getTranslator()->isNativeClassForStub($className->toString()) + ) { + continue; + } $constInfos = []; $propertyInfos = []; $methodInfos = []; @@ -4631,6 +4641,12 @@ class FileInfo { ); } } else if ($classStmt instanceof Stmt\ClassMethod) { + if (getTranslator()->isNativeMethodForStub( + $className->toString(), + $classStmt->name->toString(), + )) { + continue; + } if (!($classStmt->flags & Class_::VISIBILITY_MODIFIER_MASK)) { $classStmt->flags |= Modifiers::PUBLIC; } diff --git a/src/polyfills.php b/src/polyfills.php index fcca006f..b0e72271 100644 --- a/src/polyfills.php +++ b/src/polyfills.php @@ -6,6 +6,11 @@ * @contact service@swoole.com */ +#[Attribute(Attribute::TARGET_CLASS)] +final readonly class Native +{ +} + #[Attribute(Attribute::TARGET_CLASS)] final readonly class MethodsFor { diff --git a/tests/compiler/native-class/basic.phpt b/tests/compiler/native-class/basic.phpt new file mode 100644 index 00000000..b303dae9 --- /dev/null +++ b/tests/compiler/native-class/basic.phpt @@ -0,0 +1,41 @@ +--TEST-- +Native class: construction, typed properties and direct method calls +--FILE-- +x = $x; + $this->y = $y; + } + + public function sum(): int + { + return $this->x + $this->y; + } +} + +function main(): void +{ + $point = new Point(20, 22); + var_dump($point->x, $point->y, $point->sum()); + unset($point); + try { + $point->sum(); + } catch (Error $error) { + echo "null guarded\n"; + } +} + +?> +--EXPECT-- +int(20) +int(22) +int(42) +null guarded diff --git a/tests/compiler/native-class/by-reference.phpt b/tests/compiler/native-class/by-reference.phpt new file mode 100644 index 00000000..159b10f1 --- /dev/null +++ b/tests/compiler/native-class/by-reference.phpt @@ -0,0 +1,30 @@ +--TEST-- +Native class: by-reference parameters replace the caller pointer +--FILE-- +value = $value; + } +} + +function replaceNativeReference(NativeReferenceValue &$value): void +{ + $value = new NativeReferenceValue(42); +} + +function main(): void +{ + $value = new NativeReferenceValue(1); + replaceNativeReference($value); + var_dump($value->value); +} +?> +--EXPECT-- +int(42) diff --git a/tests/compiler/native-class/call-argument-roots.phpt b/tests/compiler/native-class/call-argument-roots.phpt new file mode 100644 index 00000000..1fe661e6 --- /dev/null +++ b/tests/compiler/native-class/call-argument-roots.phpt @@ -0,0 +1,36 @@ +--TEST-- +Native class: call result arguments are evaluated left-to-right and precisely rooted +--FILE-- +name = $name; + } +} + +function makeNativeArgument(string $name): NativeArgument +{ + echo 'make:', $name, PHP_EOL; + return new NativeArgument($name); +} + +function consumeNativeArguments(NativeArgument $first, NativeArgument $second): void +{ + echo $first->name, ':', $second->name, PHP_EOL; +} + +function main(): void +{ + consumeNativeArguments(makeNativeArgument('A'), makeNativeArgument('B')); +} +?> +--EXPECT-- +make:A +make:B +A:B diff --git a/tests/compiler/native-class/chained-assignment.phpt b/tests/compiler/native-class/chained-assignment.phpt new file mode 100644 index 00000000..782eaf0c --- /dev/null +++ b/tests/compiler/native-class/chained-assignment.phpt @@ -0,0 +1,22 @@ +--TEST-- +Native class: chained assignment remains in the native object model +--FILE-- +value, $second->value); + var_dump($first === $second); +} +?> +--EXPECT-- +int(42) +int(42) +bool(true) diff --git a/tests/compiler/native-class/chained-call.phpt b/tests/compiler/native-class/chained-call.phpt new file mode 100644 index 00000000..bba227a3 --- /dev/null +++ b/tests/compiler/native-class/chained-call.phpt @@ -0,0 +1,32 @@ +--TEST-- +Native class: method calls accept native object expressions as receivers +--FILE-- +value; + } +} + +function makeNativeChainValue(): NativeChainValue +{ + return new NativeChainValue(); +} + +function main(): void +{ + echo (new NativeChainValue())->getValue(), PHP_EOL; + echo makeNativeChainValue()->getValue(), PHP_EOL; + echo makeNativeChainValue()->value, PHP_EOL; +} +?> +--EXPECT-- +42 +42 +42 diff --git a/tests/compiler/native-class/clone-and-zend-invisible.phpt b/tests/compiler/native-class/clone-and-zend-invisible.phpt new file mode 100644 index 00000000..51d02229 --- /dev/null +++ b/tests/compiler/native-class/clone-and-zend-invisible.phpt @@ -0,0 +1,29 @@ +--TEST-- +Native class: clone is native and the class remains invisible to ZendVM +--FILE-- +value++; + } +} + +function main(): void +{ + $first = new NativeCloneValue(); + $second = clone $first; + var_dump($first->value, $second->value); + var_dump(class_exists('NativeCloneValue', false)); +} + +?> +--EXPECT-- +int(1) +int(2) +bool(false) diff --git a/tests/compiler/native-class/composite-property-types.phpt b/tests/compiler/native-class/composite-property-types.phpt new file mode 100644 index 00000000..14e76cde --- /dev/null +++ b/tests/compiler/native-class/composite-property-types.phpt @@ -0,0 +1,71 @@ +--TEST-- +Native class: nullable, union and intersection fields use Var with runtime type checks +--FILE-- +nullableInt = $value; +} + +function writeUnion(NativeCompositeProperties $object, mixed $value): void +{ + $object->unionValue = $value; +} + +function writeIntersection(NativeCompositeProperties $object, mixed $value): void +{ + $object->intersectionValue = $value; +} + +function main(): void +{ + $object = new NativeCompositeProperties(); + var_dump($object->nullableInt, $object->unionValue, $object->intersectionValue); + + writeNullable($object, 42); + writeUnion($object, 'ok'); + writeIntersection($object, new NativeCompositeBoth()); + var_dump($object->nullableInt, $object->unionValue, $object->intersectionValue::class); + + try { + writeNullable($object, 'bad'); + } catch (TypeError $error) { + echo "type error\n"; + } + try { + writeUnion($object, []); + } catch (TypeError $error) { + echo "type error\n"; + } + try { + writeIntersection($object, new NativeCompositeLeftOnly()); + } catch (TypeError $error) { + echo "type error\n"; + } +} +?> +--EXPECT-- +NULL +NULL +NULL +int(42) +string(2) "ok" +string(19) "NativeCompositeBoth" +type error +type error +type error diff --git a/tests/compiler/native-class/declaration-order.phpt b/tests/compiler/native-class/declaration-order.phpt new file mode 100644 index 00000000..9d9a955b --- /dev/null +++ b/tests/compiler/native-class/declaration-order.phpt @@ -0,0 +1,25 @@ +--TEST-- +Native class: declarations are emitted in inheritance order +--FILE-- +parent, ':', $value->child, PHP_EOL; +} +?> +--EXPECT-- +1:2 diff --git a/tests/compiler/native-class/destructor-inheritance.phpt b/tests/compiler/native-class/destructor-inheritance.phpt new file mode 100644 index 00000000..659d6a12 --- /dev/null +++ b/tests/compiler/native-class/destructor-inheritance.phpt @@ -0,0 +1,34 @@ +--TEST-- +Native class: GC finalization runs destructors from derived to base exactly once +--FILE-- + +--EXPECT-- +done +CB diff --git a/tests/compiler/native-class/gc-cycle.phpt b/tests/compiler/native-class/gc-cycle.phpt new file mode 100644 index 00000000..93ce899f --- /dev/null +++ b/tests/compiler/native-class/gc-cycle.phpt @@ -0,0 +1,37 @@ +--TEST-- +Native class: tracing GC collects cycles and runs destructors once +--FILE-- +name = $name; + } + + public function __destruct() + { + echo $this->name; + } +} + +function main(): void +{ + $a = new NativeNode('A'); + $b = new NativeNode('B'); + $a->next = $b; + $b->next = $a; + $a = null; + $b = null; + echo "done\n"; +} + +?> +--EXPECT-- +done +BA diff --git a/tests/compiler/native-class/generators.phpt b/tests/compiler/native-class/generators.phpt new file mode 100644 index 00000000..38a0f2a6 --- /dev/null +++ b/tests/compiler/native-class/generators.phpt @@ -0,0 +1,33 @@ +--TEST-- +Native class: Getter and Setter generators lower to direct native methods +--FILE-- +getValue()); + var_dump($object->getName()); + $object->setValue(42); + var_dump($object->getValue()); +} + +?> +--EXPECT-- +int(1) +string(6) "native" +int(42) diff --git a/tests/compiler/native-class/global-and-static.phpt b/tests/compiler/native-class/global-and-static.phpt new file mode 100644 index 00000000..738b6eda --- /dev/null +++ b/tests/compiler/native-class/global-and-static.phpt @@ -0,0 +1,42 @@ +--TEST-- +Native class: TypePHP globals and static locals retain request-rooted native objects +--FILE-- +value = 40; +} + +function readGlobal(): int +{ + global $nativeGlobal; + return $nativeGlobal->value; +} + +function nextStatic(): int +{ + static $counter = new NativeCounter(); + return ++$counter->value; +} + +function main(): void +{ + initializeGlobal(); + var_dump(readGlobal()); + var_dump(nextStatic()); + var_dump(nextStatic()); +} +?> +--EXPECT-- +int(40) +int(1) +int(2) diff --git a/tests/compiler/native-class/inherited-property-slot.phpt b/tests/compiler/native-class/inherited-property-slot.phpt new file mode 100644 index 00000000..76d6f73c --- /dev/null +++ b/tests/compiler/native-class/inherited-property-slot.phpt @@ -0,0 +1,38 @@ +--TEST-- +Native class: compatible inherited property declarations reuse one C++ field +--FILE-- +value = $value; + } +} + +#[Native] +class NativePropertyChild extends NativePropertyBase +{ + public int $value = 2; + + public function readFromChild(): int + { + return $this->value; + } +} + +function main(): void +{ + $value = new NativePropertyChild(); + var_dump($value->readFromChild()); + $value->writeFromBase(42); + var_dump($value->readFromChild()); +} +?> +--EXPECT-- +int(2) +int(42) diff --git a/tests/compiler/native-class/instanceof.phpt b/tests/compiler/native-class/instanceof.phpt new file mode 100644 index 00000000..5a525052 --- /dev/null +++ b/tests/compiler/native-class/instanceof.phpt @@ -0,0 +1,41 @@ +--TEST-- +Native class: statically resolved instanceof is folded at compile time +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(false) +made +bool(true) diff --git a/tests/compiler/native-class/internal-interface.phpt b/tests/compiler/native-class/internal-interface.phpt new file mode 100644 index 00000000..61b3f77f --- /dev/null +++ b/tests/compiler/native-class/internal-interface.phpt @@ -0,0 +1,24 @@ +--TEST-- +Native class: internal interfaces are compile-time contracts +--FILE-- +count()); + var_dump($value instanceof Countable); +} +?> +--EXPECT-- +int(3) +bool(true) diff --git a/tests/compiler/native-class/keyword-conversions.phpt b/tests/compiler/native-class/keyword-conversions.phpt new file mode 100644 index 00000000..b7ddcb9f --- /dev/null +++ b/tests/compiler/native-class/keyword-conversions.phpt @@ -0,0 +1,74 @@ +--TEST-- +Native class: keyword conversions lower to exactly typed native methods +--FILE-- +value]; + } + + public function toInt(): int + { + return $this->value; + } + + public function toFloat(): float + { + return $this->value + 0.5; + } + + public function toBool(): bool + { + return $this->value !== 0; + } + + public function toString(): string + { + return 'value=' . $this->value; + } +} + +#[Native] +class NativeMagicString +{ + public function __toString(): string + { + return 'magic'; + } +} + +function main(): void +{ + $value = new NativeConversions(); + var_dump($value->toArray()); + var_dump($value->toInt()); + var_dump($value->toFloat()); + var_dump($value->toBool()); + var_dump($value->toString()); + var_dump((string) $value); + var_dump(strval($value)); + + $magic = new NativeMagicString(); + var_dump($magic->toString()); + var_dump((string) $magic); +} +?> +--EXPECT-- +array(1) { + [0]=> + int(7) +} +int(7) +float(7.5) +bool(true) +string(7) "value=7" +string(7) "value=7" +string(7) "value=7" +string(5) "magic" +string(5) "magic" diff --git a/tests/compiler/native-class/magic-methods.phpt b/tests/compiler/native-class/magic-methods.phpt new file mode 100644 index 00000000..aadcc14e --- /dev/null +++ b/tests/compiler/native-class/magic-methods.phpt @@ -0,0 +1,35 @@ +--TEST-- +Native class: __toString and __invoke lower to direct native calls +--FILE-- +label; + } + + public function __invoke(int $number): string + { + return $this->label . ':' . $number; + } +} + +function main(): void +{ + $value = new NativeCallableLabel(); + echo $value, "\n"; + var_dump((string) $value); + var_dump('value=' . $value); + var_dump($value(42)); +} +?> +--EXPECT-- +native +string(6) "native" +string(12) "value=native" +string(9) "native:42" diff --git a/tests/compiler/native-class/non-null-parameter.phpt b/tests/compiler/native-class/non-null-parameter.phpt new file mode 100644 index 00000000..6e399bf2 --- /dev/null +++ b/tests/compiler/native-class/non-null-parameter.phpt @@ -0,0 +1,28 @@ +--TEST-- +Native class: non-null parameters are validated at function entry +--FILE-- + +--EXPECT-- +rejected diff --git a/tests/compiler/native-class/nullable-signatures.phpt b/tests/compiler/native-class/nullable-signatures.phpt new file mode 100644 index 00000000..5b4b54b1 --- /dev/null +++ b/tests/compiler/native-class/nullable-signatures.phpt @@ -0,0 +1,50 @@ +--TEST-- +Native class: nullable parameters and returns stay native pointers +--FILE-- +value; +} + +function maybeNativeUnion(bool $create): NativeNullableValue|null +{ + return $create ? new NativeNullableValue() : null; +} + +function readMaybeNativeUnion(NativeNullableValue|null $value): int +{ + return $value === null ? -2 : $value->value; +} + +function main(): void +{ + var_dump(readMaybeNative(maybeNative(false))); + var_dump(readMaybeNative(maybeNative(true))); + var_dump(readMaybeNativeUnion(maybeNativeUnion(false))); + var_dump(readMaybeNativeUnion(maybeNativeUnion(true))); +} +?> +--EXPECT-- +int(-1) +int(42) +int(-2) +int(42) diff --git a/tests/compiler/native-class/phpx-properties.phpt b/tests/compiler/native-class/phpx-properties.phpt new file mode 100644 index 00000000..ba0b5576 --- /dev/null +++ b/tests/compiler/native-class/phpx-properties.phpt @@ -0,0 +1,43 @@ +--TEST-- +Native class: PHPX value properties retain normal string and array behavior +--FILE-- +values[] = $value; + } +} + +function main(): void +{ + $object = new NativePhpValues(); + $object->append(3); + $object->anything = 42; + $object->maybe = 7; + var_dump($object->class, $object->name, $object->values, $object->anything, $object->maybe); +} + +?> +--EXPECT-- +string(8) "reserved" +string(6) "native" +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} +int(42) +int(7) diff --git a/tests/compiler/native-class/private-property-slots.phpt b/tests/compiler/native-class/private-property-slots.phpt new file mode 100644 index 00000000..8c6b0983 --- /dev/null +++ b/tests/compiler/native-class/private-property-slots.phpt @@ -0,0 +1,51 @@ +--TEST-- +Native class: parent and child private properties use independent native slots +--FILE-- +value; + } + + public function setBaseValue(int $value): void + { + $this->value = $value; + } +} + +#[Native] +class NativePrivateChild extends NativePrivateBase +{ + private int $value = 20; + + public function childValue(): int + { + return $this->value; + } + + public function setChildValue(int $value): void + { + $this->value = $value; + } +} + +function main(): void +{ + $value = new NativePrivateChild(); + var_dump($value->baseValue(), $value->childValue()); + $value->setBaseValue(11); + $value->setChildValue(22); + var_dump($value->baseValue(), $value->childValue()); +} +?> +--EXPECT-- +int(10) +int(20) +int(11) +int(22) diff --git a/tests/compiler/native-class/property-hooks.phpt b/tests/compiler/native-class/property-hooks.phpt new file mode 100644 index 00000000..8e84e69d --- /dev/null +++ b/tests/compiler/native-class/property-hooks.phpt @@ -0,0 +1,32 @@ +--TEST-- +Native class: PHP 8.4 property hooks use native getter and setter calls +--FILE-- +stored * 2; + } + set(int $value) { + $this->stored = $value; + } + } +} + +function main(): void +{ + $object = new NativeHookValue(); + var_dump($object->value); + $object->value = 21; + var_dump($object->value); +} + +?> +--EXPECT-- +int(2) +int(42) diff --git a/tests/compiler/native-class/trait-inheritance-interface.phpt b/tests/compiler/native-class/trait-inheritance-interface.phpt new file mode 100644 index 00000000..86395e44 --- /dev/null +++ b/tests/compiler/native-class/trait-inheritance-interface.phpt @@ -0,0 +1,73 @@ +--TEST-- +Native class: trait composition, inheritance and interface contracts +--FILE-- +count++; + } +} + +#[Native] +class NativeBase +{ + use HasCounter; + + public function __construct(int $initial) + { + $this->count = $initial; + } + + public function label(): string + { + return 'base'; + } +} + +#[Native] +class NativeChild extends NativeBase implements Named +{ + public function __construct() + { + parent::__construct(2); + } + + public function label(): string + { + return 'child'; + } +} + +function nativeLabel(NativeBase $value): string +{ + return $value->label(); +} + +function makeNativeChild(): NativeChild +{ + return new NativeChild(); +} + +function main(): void +{ + $value = makeNativeChild(); + $value->increment(); + var_dump($value->label(), nativeLabel($value), $value->count, $value instanceof Named); +} + +?> +--EXPECT-- +string(5) "child" +string(5) "child" +int(3) +bool(true) diff --git a/tests/compiler/native-class/unset-alias.phpt b/tests/compiler/native-class/unset-alias.phpt new file mode 100644 index 00000000..16b32ae6 --- /dev/null +++ b/tests/compiler/native-class/unset-alias.phpt @@ -0,0 +1,28 @@ +--TEST-- +Native class: unset and null clear only the local pointer slot +--FILE-- +value); + + $second = $alias; + $alias = null; + var_dump($alias === null, $second->value); +} +?> +--EXPECT-- +bool(true) +int(42) +bool(true) +int(42) diff --git a/tests/compiler/native-class/value-selection.phpt b/tests/compiler/native-class/value-selection.phpt new file mode 100644 index 00000000..fcc0249c --- /dev/null +++ b/tests/compiler/native-class/value-selection.phpt @@ -0,0 +1,44 @@ +--TEST-- +Native class: ternary, match and coalesce preserve native pointer types +--FILE-- +value = $value; + } +} + +function selectWithMatch(int $kind): NativeSelectedValue +{ + return match ($kind) { + 1 => new NativeSelectedValue(10), + default => new NativeSelectedValue(20), + }; +} + +function selectWithCoalesce(?NativeSelectedValue $value): NativeSelectedValue +{ + return $value ?? new NativeSelectedValue(30); +} + +function main(): void +{ + $first = true ? new NativeSelectedValue(1) : new NativeSelectedValue(2); + var_dump($first->value); + var_dump(selectWithMatch(1)->value, selectWithMatch(2)->value); + var_dump(selectWithCoalesce(null)->value); + var_dump(selectWithCoalesce($first)->value); +} +?> +--EXPECT-- +int(1) +int(10) +int(20) +int(30) +int(1) diff --git a/tests/compiler/native-class/zend-hidden-method.phpt b/tests/compiler/native-class/zend-hidden-method.phpt new file mode 100644 index 00000000..4e1f1847 --- /dev/null +++ b/tests/compiler/native-class/zend-hidden-method.phpt @@ -0,0 +1,30 @@ +--TEST-- +Native class: native-only methods on Zend objects use direct calls and stay out of Zend metadata +--FILE-- +value; + } +} + +function main(): void +{ + $host = new ZendMethodHost(); + $value = new NativeMethodArgument(); + var_dump($host->read($value)); + var_dump(method_exists($host, 'read')); +} +?> +--EXPECT-- +int(42) +bool(false) diff --git a/tests/compiler/native-class/zero-values.phpt b/tests/compiler/native-class/zero-values.phpt new file mode 100644 index 00000000..51a7c4b2 --- /dev/null +++ b/tests/compiler/native-class/zero-values.phpt @@ -0,0 +1,43 @@ +--TEST-- +Native class: properties without explicit defaults use their type zero values +--FILE-- +enabled, + $value->count, + $value->ratio, + $value->name, + $value->items, + $value->value, + $value->object, + $value->child === null, + ); +} +?> +--EXPECT-- +bool(false) +int(0) +float(0) +string(0) "" +array(0) { +} +NULL +NULL +bool(true)