From 13344ace72f6db42ef08af7ebd15b306b5cef97c Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 17 Aug 2026 16:12:32 +0800 Subject: [PATCH] =?UTF-8?q?Native=20Class=20=E6=94=AF=E6=8C=81=20[?= =?UTF-8?q?=E7=AC=AC=E4=BA=94=E9=98=B6=E6=AE=B5]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE.md | 57 +++----- docs/NATIVE_CLASS_OBJECT.md | 16 ++- package.json | 4 +- .../native-class-global-std-container.php | 14 ++ .../native-class-mixed-property-reference.php | 13 ++ .../native-class-private-constant-access.php | 12 ++ .../native-class-static-std-container.php | 13 ++ ...tive-class-std-container-arrow-capture.php | 13 ++ ...ve-class-std-container-closure-capture.php | 15 ++ .../native-class-std-container-conversion.php | 13 ++ ...native-class-std-container-destructure.php | 13 ++ ...lass-std-container-native-any-property.php | 20 +++ .../native-class-std-container-php-array.php | 14 ++ ...ative-class-std-container-php-property.php | 19 +++ .../native-class-std-container-reference.php | 13 ++ .../native-class-std-container-return.php | 14 ++ ...ve-class-std-container-static-property.php | 18 +++ .../NativeClass/NativeClassValidationTest.php | 99 ++++++++++++- src/CompilerBase.php | 13 ++ src/Entity/PropertyDef.php | 4 +- src/Generator/CallArgumentGenerator.php | 14 +- src/Generator/ClosureGenerator.php | 6 + src/NativeClass/NativeClassSupportTrait.php | 17 ++- src/Parser/AssignOpTrait.php | 27 ++++ src/Parser/MethodCallTrait.php | 3 + src/Parser/StdContainerTrait.php | 65 +++++++++ src/Preprocessor.php | 6 +- src/Translator.php | 31 ++-- .../native-class/any-property-reference.phpt | 23 ++- .../native-class/call-argument-roots.phpt | 11 ++ .../native-class/class-constants.phpt | 64 +++++++++ .../native-class/construction-gc-roots.phpt | 50 +++++++ .../native-class/finalizer-allocation.phpt | 52 +++++++ .../finalizer-inheritance-exception.phpt | 65 +++++++++ .../native-class/lifecycle-exceptions.phpt | 132 ++++++++++++++++++ .../native-class/nested-property-write.phpt | 14 ++ 36 files changed, 897 insertions(+), 80 deletions(-) create mode 100644 phpunit/code/native-class-global-std-container.php create mode 100644 phpunit/code/native-class-mixed-property-reference.php create mode 100644 phpunit/code/native-class-private-constant-access.php create mode 100644 phpunit/code/native-class-static-std-container.php create mode 100644 phpunit/code/native-class-std-container-arrow-capture.php create mode 100644 phpunit/code/native-class-std-container-closure-capture.php create mode 100644 phpunit/code/native-class-std-container-conversion.php create mode 100644 phpunit/code/native-class-std-container-destructure.php create mode 100644 phpunit/code/native-class-std-container-native-any-property.php create mode 100644 phpunit/code/native-class-std-container-php-array.php create mode 100644 phpunit/code/native-class-std-container-php-property.php create mode 100644 phpunit/code/native-class-std-container-reference.php create mode 100644 phpunit/code/native-class-std-container-return.php create mode 100644 phpunit/code/native-class-std-container-static-property.php create mode 100644 tests/compiler/native-class/class-constants.phpt create mode 100644 tests/compiler/native-class/construction-gc-roots.phpt create mode 100644 tests/compiler/native-class/finalizer-allocation.phpt create mode 100644 tests/compiler/native-class/finalizer-inheritance-exception.phpt create mode 100644 tests/compiler/native-class/lifecycle-exceptions.phpt diff --git a/LICENSE.md b/LICENSE.md index 26619be4..eb30e3e4 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,53 +1,28 @@ -## Swoole-Compiler Native AOT 编译器预览版使用须知 +# GNU GENERAL PUBLIC LICENSE -**请在使用本软件前仔细阅读以下条款:** +TypePHP 编译器(原 Swoole-Compiler)遵循 **GPL 开源协议** 发布,完全免费,可自由使用、修改和再分发,包括商业用途。 -### 一、软件状态说明 +本项目采用 **GNU General Public License v3.0**(简称 GPL-3.0)。 -1. 当前版本为 **预览版(Preview Version)**,仅供测试、评估和学习使用 -2. 本版本可能存在未知的缺陷(BUG)、功能不完善、性能不稳定等问题 -3. 软件的功能、API 接口、编译行为等可能在后续版本中发生重大变更,恕不另行通知 +## 核心权利 -### 二、使用限制 +- ✅ 免费使用:个人、企业、政府、事业单位、学校、公益组织均可免费使用,无需购买任何授权 +- ✅ 自由修改:可查看、修改、分发源代码 +- ✅ 可商用:可用于商业项目及商业化产品 +- ✅ 可再分发:在遵循 GPL-3.0 协议的前提下可自由分发 -**严禁将预览版用于以下场景:** +## 义务(Copyleft) -- ❌ 生产环境(Production Environment) -- ❌ 商业项目的正式部署 -- ❌ 关键业务系统 -- ❌ 涉及用户数据安全的应用 -- ❌ 任何对稳定性、可靠性有严格要求的场景 +- 若您修改了源代码并对外分发(包括通过网络提供使用),则修改后的作品必须同样采用 GPL-3.0 协议开源 +- 分发时必须保留版权声明与本许可声明 -**建议使用场景:** +## 完整协议文本 -- ✅ 本地开发测试 -- ✅ 技术评估与验证 -- ✅ 性能基准测试 -- ✅ 学习研究目的 +完整的 GNU GPL v3.0 协议请参见: -### 三、责任限制 - -1. **使用风险自负** - 用户明确知晓并接受使用预览版软件可能带来的所有风险 -2. **无质量保证** - 开发团队不对软件的适用性、稳定性、安全性做任何明示或暗示的保证 -3. **后果自行承担** - 因使用本软件导致的任何直接或间接损失,包括但不限于: - - 数据丢失或损坏 - - 业务中断或服务不可用 - - 经济损失 - - 安全漏洞或数据泄露 - - 第三方索赔 - - **均由用户自行承担全部责任**,开发团队不承担任何法律责任或赔偿义务 - -4. **技术支持** - 预览版仅提供有限的社区支持,不保证问题响应时间和解决方案 - -### 四、知识产权 - -本软件的所有知识产权归上海识沃网络科技有限公司所有。未经授权,不得用于商业用途或进行逆向工程。 - -### 五、条款接受 - -**下载、安装或使用本软件即表示您已完全理解并接受本免责声明的全部条款。如不同意,请立即停止使用并删除软件。** +- 官方文本:https://www.gnu.org/licenses/gpl-3.0.txt +- 中文参考:https://www.gnu.org/licenses/translations.html --- -> ⚠️ **重要提示:** 正式稳定版发布前,请持续关注官方公告。我们将在软件达到生产就绪标准后正式发布 GA(General Availability)版本。 \ No newline at end of file +> 本文件为许可声明,完整的授权条款以 GNU GPL v3.0 官方文本为准。 diff --git a/docs/NATIVE_CLASS_OBJECT.md b/docs/NATIVE_CLASS_OBJECT.md index 0b7cf70e..ed41195b 100644 --- a/docs/NATIVE_CLASS_OBJECT.md +++ b/docs/NATIVE_CLASS_OBJECT.md @@ -240,7 +240,7 @@ final class InvalidContext | `object` | `php::Object` | 保存任意 Zend Object | | Native Class | `native_struct *` | 保存同一 Native Heap 内的裸指针 | | `Stream` | `php::Var` | 保存 stream resource zval,并在赋值入口执行精确类型检查 | -| `mixed` / `any` | `php::Var` | 保存任意 PHP zval;两种声明具有相同的无约束槽语义 | +| `mixed` / `any` | `php::Var` | 保存任意 PHP zval;只有 `any` 明确允许暴露引用 | | 不含 Native Class 的 union/intersection/nullable | `php::Var` | 与普通类属性使用同一类型描述和运行时写入检查 | | `?NativeClass` | `native_struct *` | `nullptr` 表示空值;包含 Native Class 的 union/intersection 不支持 | | BigInt/BigFloat/Decimal | `php::Var` | 保存 PHPX boxed 高精度值;字段寻址仍是固定偏移,运算复用现有 Variant ABI | @@ -281,7 +281,8 @@ struct php_app__requestcontext final { Native 属性是否允许取引用必须完全由声明元数据在编译期决定,不生成运行时类型分支: -- `mixed` / `any` 是无约束的 `php::Var` 槽,允许 `$ref =& $object->property`。 +- 只有 `any` 属性允许 `$ref =& $object->property`;这是显式选择允许 Zend 动态代码替换槽值。 +- `mixed` 虽然也使用 `php::Var` 存储,但仍拒绝取引用;Native Class 中除 `any` 外的所有声明类型都必须维持编译期类型约束。 - `bool`、`int`、`float` 等固定布局字段不能表示 PHP 引用,编译期拒绝。 - `string`、`array`、`object`、Stream 和高精度类型虽然具有 PHPX 包装层,但仍是固定声明类型,引用写入会绕过类型约束,因此编译期拒绝。 - nullable、union、intersection 等受约束的 `php::Var` 字段同样拒绝引用;不能仅因底层存储也是 `php::Var` 就允许。 @@ -857,6 +858,13 @@ Box 不能保存 Native Object。Std Container 不能作为 Native Class 属性 `NativeClass::class` 作为 value type,并保存该类或其 Native 子类。普通 PHP array 仍然不能保存 Native Object。 +Native 元素 Std Container 必须是函数顶层的局部变量。编译器为该局部容器生成与其 +词法生命周期一致的 `NativeContainerRootFrame`;因此它不能保存到 global/static、 +Zend 或 Native 属性、PHP array,也不能被返回、取引用、捕获进 Closure/arrow +function,或通过 `toArray()`/`toAny()` 等方式转换。上述行为都会让保存裸指针的 +`StdContainerBox` 比 root frame 活得更久,必须在编译期统一拒绝。读取或写入单个 +typed Native 元素仍然保持在 Native 指针模型内,不属于容器逃逸。 + 任何跨越 ZendVM 边界的行为都应在编译期抛出 FatalError。编译器不得静默装箱或降级,因为这会使性能模型不可预测。 Native Object 必须始终保持 typed object。它不能被擦除为 `var`、`mixed`、普通 `object` 或无类型 callback receiver。即使编译器能够常量折叠 `$expr = 'run'`,变量方法名语法仍不支持;只有源码中明确写出的 `$nativeObject->run()` 才进入 Native method resolution。 @@ -1224,7 +1232,7 @@ $json = json_encode($nativeObject->toArray()); | nullable Native 参数/返回值 | 支持 `?NativeClass`,以 `nullptr` 表示;成员访问必须检查或先证明非空 | | Native 参数/返回值的 `&` | 不支持;编译期 FatalError | | 对 Native Object 变量取引用 | 不支持;普通赋值已经共享对象身份 | -| 对 Native 属性取引用 | 仅显式声明为 `mixed` / `any` 的无约束字段支持;其他字段编译期 FatalError | +| 对 Native 属性取引用 | 仅显式声明为 `any` 的字段支持;包括 `mixed` 在内的其他字段均编译期 FatalError | | Native variadic、union/intersection | 不支持;编译期 FatalError | | `__construct()` | 支持 | | `clone` / `__clone()` | 支持 | @@ -1269,7 +1277,7 @@ $json = json_encode($nativeObject->toArray()); | Native Object 作为 PHP array key 或 `[]` receiver | 不支持;编译期 FatalError | | Box/Std Container 属性 | 不支持 | | Box 保存 Native Object | 不支持 | -| 局部 Std Container 保存 Native Object | 支持具体 Native class value type;容器 Root Frame 参与 GC tracing | +| 局部 Std Container 保存 Native Object | 仅支持函数顶层局部变量和具体 Native class value type;容器 Root Frame 参与 GC tracing | | Native 元素 Std Container 转 PHP array/mixed 或作为 PHP 参数 | 不支持;裸指针不得越过 ZendVM value boundary | | Native Class 属性循环引用 | 支持,指针字段加 Native tracing GC | | TypePHP global/static local | 支持;ZTS 使用 thread-local request roots,RSHUTDOWN 清理 | diff --git a/package.json b/package.json index a7fbc824..c7697082 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "compiler", + "name": "typephp-compiler", "version": "1.0.0", "description": "- 需要 PHP-8.2 以上版本 - 需要 GCC-9 以上版本,支持 C++17 标准 - 需要 CMake-3.24 以上版本", "main": "index.js", @@ -17,7 +17,7 @@ }, "keywords": [], "author": "", - "license": "ISC", + "license": "GPL-3.0-or-later", "type": "commonjs", "dependencies": { "sharp": "^0.34.5" diff --git a/phpunit/code/native-class-global-std-container.php b/phpunit/code/native-class-global-std-container.php new file mode 100644 index 00000000..cbb838ed --- /dev/null +++ b/phpunit/code/native-class-global-std-container.php @@ -0,0 +1,14 @@ +value; +} diff --git a/phpunit/code/native-class-private-constant-access.php b/phpunit/code/native-class-private-constant-access.php new file mode 100644 index 00000000..4a864a6a --- /dev/null +++ b/phpunit/code/native-class-private-constant-access.php @@ -0,0 +1,12 @@ + count($values); +} diff --git a/phpunit/code/native-class-std-container-closure-capture.php b/phpunit/code/native-class-std-container-closure-capture.php new file mode 100644 index 00000000..1b2907ea --- /dev/null +++ b/phpunit/code/native-class-std-container-closure-capture.php @@ -0,0 +1,15 @@ +toArray(); +} diff --git a/phpunit/code/native-class-std-container-destructure.php b/phpunit/code/native-class-std-container-destructure.php new file mode 100644 index 00000000..1cae2321 --- /dev/null +++ b/phpunit/code/native-class-std-container-destructure.php @@ -0,0 +1,13 @@ +value = $values; +} diff --git a/phpunit/code/native-class-std-container-php-array.php b/phpunit/code/native-class-std-container-php-array.php new file mode 100644 index 00000000..664b998c --- /dev/null +++ b/phpunit/code/native-class-std-container-php-array.php @@ -0,0 +1,14 @@ +value = $values; +} diff --git a/phpunit/code/native-class-std-container-reference.php b/phpunit/code/native-class-std-container-reference.php new file mode 100644 index 00000000..5d6562f3 --- /dev/null +++ b/phpunit/code/native-class-std-container-reference.php @@ -0,0 +1,13 @@ +compile('native-class-std-container-argument.php'); } + public function testRejectsReturningStdContainerHoldingNativeObjects(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Std containers holding Native objects cannot cross a PHP/ZendVM value boundary'); + $this->compile('native-class-std-container-return.php'); + } + + public function testRejectsConvertingStdContainerHoldingNativeObjects(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Std containers holding Native objects cannot cross a PHP/ZendVM value boundary'); + $this->compile('native-class-std-container-conversion.php'); + } + + public function testRejectsCapturingStdContainerHoldingNativeObjectsInClosure(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Std containers holding Native objects cannot be captured by Zend closures'); + $this->compile('native-class-std-container-closure-capture.php'); + } + + public function testRejectsCapturingStdContainerHoldingNativeObjectsInArrowFunction(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Std containers holding Native objects cannot be captured by Zend closures'); + $this->compile('native-class-std-container-arrow-capture.php'); + } + + /** + * @dataProvider nativeStdContainerStorageBoundaryProvider + */ + public function testRejectsStoringStdContainerHoldingNativeObjects(string $fixture): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Std containers holding Native objects cannot cross a PHP/ZendVM value boundary'); + $this->compile($fixture); + } + + public static function nativeStdContainerStorageBoundaryProvider(): array + { + return [ + ['native-class-std-container-php-property.php'], + ['native-class-std-container-static-property.php'], + ['native-class-std-container-php-array.php'], + ['native-class-std-container-native-any-property.php'], + ]; + } + + public function testRejectsReferencingStdContainerHoldingNativeObjects(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Std containers holding Native objects cannot cross a PHP/ZendVM value boundary'); + $this->compile('native-class-std-container-reference.php'); + } + + public function testRejectsDestructuringStdContainerHoldingNativeObjects(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Std containers holding Native objects cannot cross a PHP/ZendVM value boundary'); + $this->compile('native-class-std-container-destructure.php'); + } + + public function testRejectsStaticStdContainerHoldingNativeObjects(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Std containers holding Native objects must be function-local'); + $this->compile('native-class-static-std-container.php'); + } + + public function testRejectsGlobalStdContainerHoldingNativeObjects(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Std containers holding Native objects must be function-local'); + $this->compile('native-class-global-std-container.php'); + } + public function testRejectsCompoundWritesToNativePropertyHooks(): void { $this->expectException(TestError::class); @@ -266,10 +342,17 @@ final class NativeClassValidationTest extends \BaseTest public function testRejectsReferencesToNativeObjectProperties(): void { $this->expectException(TestError::class); - $this->expectExceptionMessage('Only Native object properties declared as any or mixed can be referenced'); + $this->expectExceptionMessage('Only Native object properties declared as any can be referenced'); $this->compile('native-class-property-reference.php'); } + public function testRejectsReferencesToMixedNativeObjectProperties(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Only Native object properties declared as any can be referenced'); + $this->compile('native-class-mixed-property-reference.php'); + } + public function testAllowsReferencesToExplicitAnyNativeObjectProperties(): void { $this->compile('native-class-any-property-reference.php'); @@ -674,6 +757,20 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-late-static-constant.php'); } + public function testRejectsLateStaticNativeMethodSignature(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native classes do not support late static binding in parameter or return types'); + $this->compile('native-class-static-signature.php'); + } + + public function testRejectsInaccessibleNativeClassConstant(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Constant `NativePrivateConstantOwner::VALUE` is not accessible'); + $this->compile('native-class-private-constant-access.php'); + } + public function testRejectsNativeObjectCastToZendObject(): void { $this->expectException(TestError::class); diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 0fa5da95..2c2f5d7c 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1138,6 +1138,12 @@ class CompilerBase implements PropertyAccessContext protected function assertExprCanBeUsedAsValue(NodeAbstract $expr, string $context = 'value'): void { + if ($this->isVarExpr($expr)) { + $this->assertStdContainerDoesNotEscapeNativeObjects( + $expr, + $this->parseIdentifier($expr), + ); + } // PHP permits using a void/never call as an expression; the expression // result is null after the call side effect has run. } @@ -2209,6 +2215,12 @@ class CompilerBase implements PropertyAccessContext protected function parseReturn(Node\Stmt\Return_ $v): string { + if ($v->expr !== null && $this->isVarExpr($v->expr)) { + $this->assertStdContainerDoesNotEscapeNativeObjects( + $v, + $this->parseIdentifier($v->expr), + ); + } if ($this->functionDef->returnsByRef) { if ($v->expr === null) { return 'return ' . Type::REF . '{};'; @@ -3999,6 +4011,7 @@ class CompilerBase implements PropertyAccessContext $type = $var->default ? $this->detectTypeOfExpr($var->default) : Type::VAR; if ($var->default) { $this->assertExprCanBeUsedAsValue($var->default, 'static variable default value'); + $this->assertNativeStdContainerFunctionLocal($var->default); } $globalVar = $this->addStaticVar($var->var, $varName, $type); if ($var->default) { diff --git a/src/Entity/PropertyDef.php b/src/Entity/PropertyDef.php index 79eb96b9..13b3a69f 100644 --- a/src/Entity/PropertyDef.php +++ b/src/Entity/PropertyDef.php @@ -18,8 +18,8 @@ class PropertyDef public ?string $default = null; public ?ArrayInitPlan $arrayInitPlan = null; public bool $nullable = false; - /** The declared type is the unconstrained `mixed`/`any` type. */ - public bool $explicitMixed = false; + /** The declared type is TypePHP's unconstrained, reference-capable `any` type. */ + public bool $explicitAny = false; public string $class = ''; public array $typeCheck = []; public string $typeStr = ''; diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index c891cfa9..8038dd2b 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -493,14 +493,16 @@ trait CallArgumentGenerator if (!$this->hasVar($name)) { $this->fatalError($arg, 'Undefined variable `$' . $name . '`'); } - } elseif ($this->isPropertyFetch($arg->value) and $this->isVarExpr($arg->value->var)) { + } elseif ($this->isPropertyFetch($arg->value)) { if ($byRef) { $this->addPositionalCallArg($this->emitDynamicPropertyFetchRef($arg->value, $arg), $arrayArgsVar, $list_args); continue; } - $objectExpr = $this->parseIdentifier($arg->value->var); - if (!$this->hasVar($objectExpr)) { - $this->fatalError($arg, 'Undefined variable `$' . $objectExpr . '`'); + if ($this->isVarExpr($arg->value->var)) { + $objectExpr = $this->parseIdentifier($arg->value->var); + if (!$this->hasVar($objectExpr)) { + $this->fatalError($arg, 'Undefined variable `$' . $objectExpr . '`'); + } } } elseif ($this->isArrayDimFetch($arg->value) and $this->isVarExpr($arg->value->var)) { $array = $this->parseIdentifier($arg->value->var); @@ -700,7 +702,7 @@ trait CallArgumentGenerator return $this->parseArgRefVar($arg, $this->parseIdentifier($arg->value)); } - if ($this->isPropertyFetch($arg->value) and $this->isVarExpr($arg->value->var)) { + if ($this->isPropertyFetch($arg->value)) { return $this->emitDynamicPropertyFetchRef($arg->value, $arg); } @@ -768,7 +770,7 @@ trait CallArgumentGenerator */ protected function expandRefvalExpr(NodeAbstract $inner, Node\Arg $arg): ?string { - if ($this->isPropertyFetch($inner) and $this->isVarExpr($inner->var)) { + if ($this->isPropertyFetch($inner)) { return $this->emitDynamicPropertyFetchRef($inner, $arg); } if ($this->isArrayDimFetch($inner) and $this->isVarExpr($inner->var)) { diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index 0d8ec6c4..ea7f9bc5 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -133,6 +133,12 @@ trait ClosureGenerator if ($this->isNativeObjectVar($name)) { $this->fatalError($useItem, 'Native objects cannot be captured by Zend closures'); } + if ($this->getStdContainerNativeObjectClass($name) !== '') { + $this->fatalError( + $useItem, + 'Std containers holding Native objects cannot be captured by Zend closures', + ); + } } if ($expr instanceof Expr\ArrowFunction && $this->isNativeObjectClass($this->detectClassOfExpr($expr->expr)) diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index 66f9d64f..cc3b97d6 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -589,10 +589,10 @@ trait NativeClassSupportTrait * * A Native object variable is a typed pointer and must never expose its * pointer slot as a PHP reference. A Native property may expose a reference - * only when it was explicitly declared `mixed`/`any`: that field is an - * unconstrained php::Var slot. Fixed-layout fields and constrained Variant - * fields must reject references because a later reference write could - * bypass their declared type. + * only when it was explicitly declared `any`: that field intentionally + * permits arbitrary PHP values. Every other declaration, including + * `mixed`, must reject references because dynamic Zend code could replace + * the referenced value with one that violates the Native field contract. */ protected function assertNativeObjectReferenceForbidden( NodeAbstract $expr, @@ -614,10 +614,10 @@ trait NativeClassSupportTrait } $this->applyNativePropertyAccessResult($expr, $resolution); $definition = $resolution->propertyDef; - if (!$definition->explicitMixed || $definition->getter !== null || $definition->setter !== null) { + if (!$definition->explicitAny || $definition->getter !== null || $definition->setter !== null) { $this->fatalError( $errorNode, - 'Only Native object properties declared as any or mixed can be referenced', + 'Only Native object properties declared as any can be referenced', ); } return; @@ -1478,9 +1478,12 @@ trait NativeClassSupportTrait } if ($destructors !== []) { $code .= 'static void ' . $prefix . '_finalize(void *object) {' . PHP_EOL; + $code .= ' php::NativeFinalizerChain chain;' . PHP_EOL; foreach ($destructors as [$destructor, $destructorCpp]) { - $code .= ' ' . $destructor . '(*static_cast<' . $destructorCpp . ' *>(object));' . PHP_EOL; + $code .= ' chain.run([&] { ' . $destructor + . '(*static_cast<' . $destructorCpp . ' *>(object)); });' . PHP_EOL; } + $code .= ' chain.rethrow();' . PHP_EOL; $code .= '}' . PHP_EOL; } $code .= 'static void ' . $prefix . '_destroy(void *object) noexcept {' . PHP_EOL; diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index b74d088b..fea5083d 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -221,6 +221,19 @@ trait AssignOpTrait { $this->assertNotNullsafeWriteContext($left); $rightClass = $this->detectClassOfExpr($right); + + // A Native-element std container owns a PHPX Box but its raw pointer + // elements are traced only by the function-local container root frame. + // Copying the whole Box into any other value slot could outlive that + // frame, so reject the escape once at the assignment boundary. Element + // reads/writes do not pass the container variable itself as the RHS. + if ($this->isVarExpr($right)) { + $this->assertStdContainerDoesNotEscapeNativeObjects( + $right, + $this->parseIdentifier($right), + ); + } + if ($left instanceof Expr\List_) { if ($this->isNativeObjectClass($rightClass)) { $this->fatalError($right, 'Native objects cannot be destructured into PHP values'); @@ -430,6 +443,9 @@ trait AssignOpTrait $class = $this->parseIdentifier($right->class); if ($class === 'std') { if (in_array($right->name->toString(), ['array', 'vector', 'map', 'ordered_map'], true)) { + if ($this->hasScopeGlobalVar($var) || $this->hasStaticVar($var)) { + $this->assertNativeStdContainerFunctionLocal($right); + } if ($this->hasVar($var)) { $this->fatalError($left, "Cannot re-assign `\${$var}` to std::{$right->name->toString()}"); } @@ -970,6 +986,14 @@ trait AssignOpTrait $this->assertNativeObjectReferenceForbidden($expr->var, $expr); $this->assertNativeObjectReferenceForbidden($expr->expr, $expr); + foreach ([$expr->var, $expr->expr] as $referenceOperand) { + if ($this->isVarExpr($referenceOperand)) { + $this->assertStdContainerDoesNotEscapeNativeObjects( + $referenceOperand, + $this->parseIdentifier($referenceOperand), + ); + } + } // A reference would outlive the constructor-only write window and // make later mutations invisible to the compiler. It is therefore @@ -1071,6 +1095,9 @@ trait AssignOpTrait } $this->context->beforeStmtLines[] = $rightExpr . ';'; + if ($expr->var instanceof Expr\PropertyFetch && $this->isNativePropertyAccess($expr->var)) { + return $left . '.rebindReference(' . $tmpVar . ')'; + } return $left . ' = &' . $tmpVar; } diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 4d1e19d6..daa5018f 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -455,6 +455,9 @@ trait MethodCallTrait && isset(self::KEYWORD_METHOD_MAP[$methodName]) && !$expr->getAttribute('nativeKeywordCall', false) ) { + if ($this->isVarExpr($expr->var)) { + $this->assertStdContainerDoesNotEscapeNativeObjects($expr, $object); + } if (!isset(self::KEYWORD_METHOD_WITH_ARGUMENTS[$methodName]) && $expr->args !== []) { $this->fatalError($expr, "The {$methodName} method does not accept parameters"); } diff --git a/src/Parser/StdContainerTrait.php b/src/Parser/StdContainerTrait.php index 7f1995b9..1e79cf21 100644 --- a/src/Parser/StdContainerTrait.php +++ b/src/Parser/StdContainerTrait.php @@ -20,6 +20,71 @@ use PhpParser\NodeAbstract; trait StdContainerTrait { + /** + * Resolve the Native value class of a std container factory without + * creating container metadata. This is used before assignment lowering so + * a global/static destination cannot accidentally outlive the temporary + * NativeContainerRootFrame generated for function-local containers. + */ + protected function getStdContainerFactoryNativeClass(NodeAbstract $expr): string + { + if (!$expr instanceof StaticCall + || !$this->isNameExpr($expr->class) + || !$this->isIdExpr($expr->name) + || $this->parseIdentifier($expr->class) !== 'std' + ) { + return ''; + } + + $method = $expr->name->toString(); + if (!in_array($method, ['array', 'vector', 'map', 'ordered_map'], true)) { + return ''; + } + + if ($method === 'array') { + $factory = $expr; + while ($factory instanceof StaticCall + && $this->isNameExpr($factory->class) + && $this->isIdExpr($factory->name) + && $this->parseIdentifier($factory->class) === 'std' + && $factory->name->toString() === 'array' + ) { + if (count($factory->args) !== 2) { + return ''; + } + $value = $factory->args[0]->value; + if (!$value instanceof StaticCall) { + $typeInfo = $this->parseStdValueTypeInfo($value, 'std::array'); + $class = $typeInfo['class'] ?? ''; + return is_string($class) && $this->isNativeObjectClass($class) ? $class : ''; + } + $factory = $value; + } + return ''; + } + + $valueIndex = $method === 'vector' ? 0 : 1; + if (!isset($expr->args[$valueIndex])) { + return ''; + } + $typeInfo = $this->parseStdValueTypeInfo( + $expr->args[$valueIndex]->value, + 'std::' . $method, + ); + $class = $typeInfo['class'] ?? ''; + return is_string($class) && $this->isNativeObjectClass($class) ? $class : ''; + } + + protected function assertNativeStdContainerFunctionLocal(NodeAbstract $expr): void + { + if ($this->getStdContainerFactoryNativeClass($expr) !== '') { + $this->fatalError( + $expr, + 'Std containers holding Native objects must be function-local', + ); + } + } + protected function isStdContainerIterating(string $var): bool { return !empty($this->context->stdContainers[$var]['iterationDepth']); diff --git a/src/Preprocessor.php b/src/Preprocessor.php index e79f31c1..c4654038 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -1361,11 +1361,7 @@ class Preprocessor extends CompilerBase && !$typeNode instanceof UnionType && !$typeNode instanceof IntersectionType ) { - $propDef->explicitMixed = in_array( - strtolower($this->parseIdentifier($typeNode)), - ['mixed', 'any'], - true, - ); + $propDef->explicitAny = strtolower($this->parseIdentifier($typeNode)) === 'any'; } $propDef->readonly = (bool) (($flags | $this->classDef->flags) & Modifiers::READONLY); $propDef->class = $class; diff --git a/src/Translator.php b/src/Translator.php index 3df83744..c14fcef2 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -929,10 +929,11 @@ CODE; $code .= "// class \n"; foreach ($this->getClassLikesWithConstants() as $classDef) { - if ($classDef instanceof ClassDef && $classDef->nativeObject) { - continue; - } - if ($classDef instanceof ClassDef && !$classDef->trait && !$classDef->enum) { + if ($classDef instanceof ClassDef + && !$classDef->nativeObject + && !$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"; } @@ -1104,15 +1105,20 @@ CODE; $constName = self::PREFIX . $this->getNativeName($constant->name, $classDef->namespace, $classDef->name); $code .= $constName . ".unset();\n"; - $classNameStr = $this->genCharPtr($classDef->getNamespacedName(false), true); - $classConstStr = $this->genCharPtr($constant->name); - $code .= "php::updateConstant($classNameStr, $classConstStr, php::null);\n"; + if (!$classDef instanceof ClassDef || !$classDef->nativeObject) { + $classNameStr = $this->genCharPtr($classDef->getNamespacedName(false), true); + $classConstStr = $this->genCharPtr($constant->name); + $code .= "php::updateConstant($classNameStr, $classConstStr, php::null);\n"; + } } } } // Clean up inherited array constants from child classes foreach ($this->symbols->classes() as $className => $classDef) { + if ($classDef->nativeObject) { + continue; + } $ownConstNames = []; foreach ($classDef->constants as $constant) { if ($constant->type === Type::ARRAY) { @@ -2145,9 +2151,11 @@ CODE; $code .= "do {\n"; $code .= $constant->arrayExpr; $code .= $constName . ' = ' . $constant->value . ";\n"; - $classNameStr = $this->genCharPtr($classDef->getNamespacedName(false), true); - $classConstStr = $this->genCharPtr($constant->name); - $code .= "php::updateConstant($classNameStr, $classConstStr, {$constant->value});\n"; + if (!$classDef instanceof ClassDef || !$classDef->nativeObject) { + $classNameStr = $this->genCharPtr($classDef->getNamespacedName(false), true); + $classConstStr = $this->genCharPtr($constant->name); + $code .= "php::updateConstant($classNameStr, $classConstStr, {$constant->value});\n"; + } $code .= "} while(0);\n"; } } @@ -2155,6 +2163,9 @@ CODE; // Propagate array constants to child classes that don't override them foreach ($this->symbols->classes() as $className => $classDef) { + if ($classDef->nativeObject) { + continue; + } $ownConstNames = []; foreach ($classDef->constants as $constant) { if ($constant->type === Type::ARRAY) { diff --git a/tests/compiler/native-class/any-property-reference.phpt b/tests/compiler/native-class/any-property-reference.phpt index 6f149056..3d4b0ccf 100644 --- a/tests/compiler/native-class/any-property-reference.phpt +++ b/tests/compiler/native-class/any-property-reference.phpt @@ -6,8 +6,7 @@ Native any properties support PHP references without runtime type dispatch #[Native] class NativeAnyReference { - // `mixed` is the PHP spelling of TypePHP's equivalent `any` storage. - public mixed $value = 1; + public any $value = 1; public ?NativeAnyReference $child; } @@ -16,6 +15,11 @@ function replaceAny(mixed &$value, mixed $replacement): void $value = $replacement; } +function &getNativeAnyReference(NativeAnyReference $object): mixed +{ + return $object->value; +} + function main(): void { $object = new NativeAnyReference(); @@ -30,6 +34,18 @@ function main(): void $childReference =& $object->child->value; replaceAny($childReference, ['native', 'reference']); var_dump($object->child->value); + + replaceAny($object->child->value, 'direct argument'); + var_dump($childReference); + + $source = 'assigned reference'; + $object->value =& $source; + $source = 'source changed'; + var_dump($object->value); + + $returnedReference =& getNativeAnyReference($object); + $returnedReference = 'returned reference'; + var_dump($source); } ?> @@ -42,3 +58,6 @@ array(2) { [1]=> string(9) "reference" } +string(15) "direct argument" +string(14) "source changed" +string(18) "returned reference" diff --git a/tests/compiler/native-class/call-argument-roots.phpt b/tests/compiler/native-class/call-argument-roots.phpt index 1fe661e6..017cbaec 100644 --- a/tests/compiler/native-class/call-argument-roots.phpt +++ b/tests/compiler/native-class/call-argument-roots.phpt @@ -14,9 +14,20 @@ class NativeArgument } } +#[Native] +class NativeArgumentPressure +{ + public int $value; +} + function makeNativeArgument(string $name): NativeArgument { echo 'make:', $name, PHP_EOL; + if ($name === 'B') { + for ($i = 0; $i < 300000; $i++) { + $filler = new NativeArgumentPressure(); + } + } return new NativeArgument($name); } diff --git a/tests/compiler/native-class/class-constants.phpt b/tests/compiler/native-class/class-constants.phpt new file mode 100644 index 00000000..79d39386 --- /dev/null +++ b/tests/compiler/native-class/class-constants.phpt @@ -0,0 +1,64 @@ +--TEST-- +Native class: constants resolve entirely at compile time across inheritance +--FILE-- +privateValue(), + $config->label(), + ); +} +} + +?> +--EXPECT-- +string(30) "NativeConstantTest\ChildConfig" +string(29) "NativeConstantTest\BaseConfig" +string(6) "native" +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} +int(7) +string(6) "native" diff --git a/tests/compiler/native-class/construction-gc-roots.phpt b/tests/compiler/native-class/construction-gc-roots.phpt new file mode 100644 index 00000000..f602a23f --- /dev/null +++ b/tests/compiler/native-class/construction-gc-roots.phpt @@ -0,0 +1,50 @@ +--TEST-- +Native class: construction and clone hooks root objects across automatic collection +--FILE-- +value = $value; + } + + public function __clone(): void + { + createPressure(); + $this->value++; + } +} + +function main(): void +{ + $object = new NativeRootedConstruction(41); + var_dump($object->value); + + $copy = clone $object; + var_dump($object->value, $copy->value); +} + +?> +--EXPECT-- +int(41) +int(41) +int(42) diff --git a/tests/compiler/native-class/finalizer-allocation.phpt b/tests/compiler/native-class/finalizer-allocation.phpt new file mode 100644 index 00000000..82cef22e --- /dev/null +++ b/tests/compiler/native-class/finalizer-allocation.phpt @@ -0,0 +1,52 @@ +--TEST-- +Native class: objects allocated by a finalizer survive the active sweep and are finalized later +--FILE-- + +--EXPECT-- +bool(true) +bool(true) diff --git a/tests/compiler/native-class/finalizer-inheritance-exception.phpt b/tests/compiler/native-class/finalizer-inheritance-exception.phpt new file mode 100644 index 00000000..3efb5e6b --- /dev/null +++ b/tests/compiler/native-class/finalizer-inheritance-exception.phpt @@ -0,0 +1,65 @@ +--TEST-- +Native class: a throwing derived finalizer does not skip base finalizers +--FILE-- +getMessage(), "\n"; + return; + } + } + echo "finalizer did not run\n"; +} + +function main(): void +{ + global $events; + $events = []; + + $object = new NativeThrowingFinalizerChild(); + $object = null; + collectThrowingFinalizer(); + var_dump($events); +} + +?> +--EXPECT-- +child finalizer failed +array(2) { + [0]=> + string(5) "child" + [1]=> + string(4) "base" +} diff --git a/tests/compiler/native-class/lifecycle-exceptions.phpt b/tests/compiler/native-class/lifecycle-exceptions.phpt new file mode 100644 index 00000000..28a083e4 --- /dev/null +++ b/tests/compiler/native-class/lifecycle-exceptions.phpt @@ -0,0 +1,132 @@ +--TEST-- +Native class: constructor, clone and finalizer exceptions preserve heap invariants +--FILE-- +name; + } +} + +#[Native] +class NativeConstructorFailure +{ + public object $probe; + + public function __construct() + { + $this->probe = new NativeLifecycleProbe('constructor'); + throw new RuntimeException('constructor failed'); + } + + public function __destruct() + { + global $nativeFinalizers; + $nativeFinalizers[] = 'constructor'; + } +} + +#[Native] +class NativeCloneFailure +{ + public object $probe; + + public function __construct() + { + $this->probe = new NativeLifecycleProbe('source'); + } + + public function __clone(): void + { + $this->probe = new NativeLifecycleProbe('clone'); + throw new RuntimeException('clone failed'); + } +} + +#[Native] +class NativeThrowingFinalizer +{ + public function __destruct() + { + global $nativeFinalizers; + $nativeFinalizers[] = 'throwing'; + throw new RuntimeException('finalizer failed'); + } +} + +#[Native] +class NativeLifecycleFiller +{ + public int $value; +} + +function allocateUntilFinalizerRuns(): void +{ + global $nativeFinalizers; + for ($i = 0; $i < 400000; $i++) { + try { + $filler = new NativeLifecycleFiller(); + } catch (RuntimeException $error) { + echo $error->getMessage(), "\n"; + return; + } + } + echo "finalizer did not run\n"; +} + +function main(): void +{ + global $releasedProbes, $nativeFinalizers; + $releasedProbes = []; + $nativeFinalizers = []; + + try { + $failed = new NativeConstructorFailure(); + } catch (RuntimeException $error) { + echo $error->getMessage(), "\n"; + } + var_dump($releasedProbes, $nativeFinalizers); + + $source = new NativeCloneFailure(); + try { + $copy = clone $source; + } catch (RuntimeException $error) { + echo $error->getMessage(), "\n"; + } + var_dump($releasedProbes); + + $throwing = new NativeThrowingFinalizer(); + $throwing = null; + allocateUntilFinalizerRuns(); + var_dump($nativeFinalizers); +} + +?> +--EXPECT-- +constructor failed +array(1) { + [0]=> + string(11) "constructor" +} +array(0) { +} +clone failed +array(2) { + [0]=> + string(11) "constructor" + [1]=> + string(5) "clone" +} +finalizer failed +array(1) { + [0]=> + string(8) "throwing" +} diff --git a/tests/compiler/native-class/nested-property-write.phpt b/tests/compiler/native-class/nested-property-write.phpt index 80e9044e..19414677 100644 --- a/tests/compiler/native-class/nested-property-write.phpt +++ b/tests/compiler/native-class/nested-property-write.phpt @@ -20,6 +20,16 @@ function makeNativeWriteNode(int $value): NativeWriteNode return new NativeWriteNode($value); } +function makeNativeWriteNodeAfterPressure(int $value): NativeWriteNode +{ + // Force an automatic Native GC collection while the expression receiver + // on the left-hand side is only held by the compiler-generated temporary. + for ($i = 0; $i < 300000; $i++) { + $filler = new NativeWriteNode($i); + } + return new NativeWriteNode($value); +} + function main(): void { $root = new NativeWriteNode(1); @@ -32,9 +42,13 @@ function main(): void $replacement = new NativeWriteNode(4); makeNativeWriteNode(5)->child = $replacement; echo $replacement->value, "\n"; + + makeNativeWriteNode(6)->child = makeNativeWriteNodeAfterPressure(7); + echo "receiver survived\n"; } ?> --EXPECT-- 3 4 +receiver survived