Native Class 支持 [第五阶段]

master
韩天峰 1 week ago
parent 7cf265cbc7
commit 13344ace72
  1. 57
      LICENSE.md
  2. 16
      docs/NATIVE_CLASS_OBJECT.md
  3. 4
      package.json
  4. 14
      phpunit/code/native-class-global-std-container.php
  5. 13
      phpunit/code/native-class-mixed-property-reference.php
  6. 12
      phpunit/code/native-class-private-constant-access.php
  7. 13
      phpunit/code/native-class-static-std-container.php
  8. 13
      phpunit/code/native-class-std-container-arrow-capture.php
  9. 15
      phpunit/code/native-class-std-container-closure-capture.php
  10. 13
      phpunit/code/native-class-std-container-conversion.php
  11. 13
      phpunit/code/native-class-std-container-destructure.php
  12. 20
      phpunit/code/native-class-std-container-native-any-property.php
  13. 14
      phpunit/code/native-class-std-container-php-array.php
  14. 19
      phpunit/code/native-class-std-container-php-property.php
  15. 13
      phpunit/code/native-class-std-container-reference.php
  16. 14
      phpunit/code/native-class-std-container-return.php
  17. 18
      phpunit/code/native-class-std-container-static-property.php
  18. 99
      phpunit/src/NativeClass/NativeClassValidationTest.php
  19. 13
      src/CompilerBase.php
  20. 4
      src/Entity/PropertyDef.php
  21. 14
      src/Generator/CallArgumentGenerator.php
  22. 6
      src/Generator/ClosureGenerator.php
  23. 17
      src/NativeClass/NativeClassSupportTrait.php
  24. 27
      src/Parser/AssignOpTrait.php
  25. 3
      src/Parser/MethodCallTrait.php
  26. 65
      src/Parser/StdContainerTrait.php
  27. 6
      src/Preprocessor.php
  28. 31
      src/Translator.php
  29. 23
      tests/compiler/native-class/any-property-reference.phpt
  30. 11
      tests/compiler/native-class/call-argument-roots.phpt
  31. 64
      tests/compiler/native-class/class-constants.phpt
  32. 50
      tests/compiler/native-class/construction-gc-roots.phpt
  33. 52
      tests/compiler/native-class/finalizer-allocation.phpt
  34. 65
      tests/compiler/native-class/finalizer-inheritance-exception.phpt
  35. 132
      tests/compiler/native-class/lifecycle-exceptions.phpt
  36. 14
      tests/compiler/native-class/nested-property-write.phpt

@ -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)版本
> 本文件为许可声明,完整的授权条款以 GNU GPL v3.0 官方文本为准。

@ -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 清理 |

@ -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"

@ -0,0 +1,14 @@
<?php
#[Native]
class NativeGlobalContainerValue
{
public int $value;
}
function native_global_container(): void
{
global $values;
$values = std::vector(NativeGlobalContainerValue::class);
$values[] = new NativeGlobalContainerValue();
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeMixedPropertyReference
{
public mixed $value = null;
}
function native_mixed_property_reference(): void
{
$object = new NativeMixedPropertyReference();
$reference =& $object->value;
}

@ -0,0 +1,12 @@
<?php
#[Native]
class NativePrivateConstantOwner
{
private const int VALUE = 1;
}
function main(): void
{
var_dump(NativePrivateConstantOwner::VALUE);
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeStaticContainerValue
{
public int $value;
}
function native_static_container(): void
{
static $values = std::vector(NativeStaticContainerValue::class);
$values[] = new NativeStaticContainerValue();
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeArrowContainerValue
{
public int $value;
}
function native_container_arrow(): Closure
{
$values = std::vector(NativeArrowContainerValue::class);
return static fn(): int => count($values);
}

@ -0,0 +1,15 @@
<?php
#[Native]
class NativeClosureContainerValue
{
public int $value;
}
function native_container_closure(): Closure
{
$values = std::vector(NativeClosureContainerValue::class);
return static function () use ($values): int {
return count($values);
};
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeConvertedContainerValue
{
public int $value;
}
function native_container_conversion(): array
{
$values = std::vector(NativeConvertedContainerValue::class);
return $values->toArray();
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeDestructuredContainerValue
{
public int $value;
}
function native_container_destructure(): void
{
$values = std::array(NativeDestructuredContainerValue::class, 1);
[$first] = $values;
}

@ -0,0 +1,20 @@
<?php
#[Native]
class NativeAnyPropertyContainerValue
{
public int $value;
}
#[Native]
class NativeContainerAnyPropertyHolder
{
public any $value;
}
function native_container_native_any_property(): void
{
$values = std::vector(NativeAnyPropertyContainerValue::class);
$holder = new NativeContainerAnyPropertyHolder();
$holder->value = $values;
}

@ -0,0 +1,14 @@
<?php
#[Native]
class NativePhpArrayContainerValue
{
public int $value;
}
function native_container_php_array(): void
{
$values = std::vector(NativePhpArrayContainerValue::class);
$array = [];
$array[] = $values;
}

@ -0,0 +1,19 @@
<?php
#[Native]
class NativePhpPropertyContainerValue
{
public int $value;
}
class NativeContainerPhpPropertyHolder
{
public mixed $value;
}
function native_container_php_property(): void
{
$values = std::vector(NativePhpPropertyContainerValue::class);
$holder = new NativeContainerPhpPropertyHolder();
$holder->value = $values;
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeReferencedContainerValue
{
public int $value;
}
function native_container_reference(): void
{
$values = std::vector(NativeReferencedContainerValue::class);
$alias =& $values;
}

@ -0,0 +1,14 @@
<?php
#[Native]
class NativeReturnedContainerValue
{
public int $value;
}
function native_container_return(): array
{
$values = std::vector(NativeReturnedContainerValue::class);
$values[] = new NativeReturnedContainerValue();
return $values;
}

@ -0,0 +1,18 @@
<?php
#[Native]
class NativeStaticPropertyContainerValue
{
public int $value;
}
class NativeContainerStaticPropertyHolder
{
public static mixed $value;
}
function native_container_static_property(): void
{
$values = std::vector(NativeStaticPropertyContainerValue::class);
NativeContainerStaticPropertyHolder::$value = $values;
}

@ -109,6 +109,82 @@ final class NativeClassValidationTest extends \BaseTest
$this->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);

@ -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) {

@ -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 = '';

@ -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)) {

@ -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))

@ -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;

@ -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;
}

@ -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");
}

@ -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']);

@ -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;

@ -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) {

@ -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"

@ -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);
}

@ -0,0 +1,64 @@
--TEST--
Native class: constants resolve entirely at compile time across inheritance
--FILE--
<?php
namespace NativeConstantTest {
#[\Native]
class BaseConfig
{
private const int PRIVATE_VALUE = 7;
protected const string PREFIX = 'native';
public const array ITEMS = [1, 2, 3];
public function privateValue(int $value = self::PRIVATE_VALUE): int
{
return $value;
}
}
#[\Native]
class ChildConfig extends BaseConfig
{
public const string CLASS_NAME = self::class;
public const string PARENT_NAME = parent::class;
public const string LABEL = parent::PREFIX;
public function label(string $value = self::LABEL): string
{
return $value;
}
}
}
namespace {
function main(): void
{
$config = new \NativeConstantTest\ChildConfig();
var_dump(
\NativeConstantTest\ChildConfig::CLASS_NAME,
\NativeConstantTest\ChildConfig::PARENT_NAME,
\NativeConstantTest\ChildConfig::LABEL,
\NativeConstantTest\ChildConfig::ITEMS,
$config->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"

@ -0,0 +1,50 @@
--TEST--
Native class: construction and clone hooks root objects across automatic collection
--FILE--
<?php
#[Native]
class NativeConstructionPressure
{
public int $value;
}
function createPressure(): void
{
for ($i = 0; $i < 300000; $i++) {
$filler = new NativeConstructionPressure();
}
}
#[Native]
class NativeRootedConstruction
{
public int $value;
public function __construct(int $value)
{
createPressure();
$this->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)

@ -0,0 +1,52 @@
--TEST--
Native class: objects allocated by a finalizer survive the active sweep and are finalized later
--FILE--
<?php
#[Native]
class NativeFinalizerNewborn
{
public function __destruct()
{
global $newbornFinalized;
$newbornFinalized++;
}
}
#[Native]
class NativeFinalizerCreator
{
public function __destruct()
{
global $creatorFinalized;
$creatorFinalized++;
$newborn = new NativeFinalizerNewborn();
}
}
#[Native]
class NativeFinalizerFiller
{
public int $value;
}
function main(): void
{
global $creatorFinalized, $newbornFinalized;
$creatorFinalized = 0;
$newbornFinalized = 0;
$creator = new NativeFinalizerCreator();
$creator = null;
for ($i = 0; $i < 400000; $i++) {
$filler = new NativeFinalizerFiller();
}
var_dump($creatorFinalized === 1);
var_dump($newbornFinalized === 1);
}
?>
--EXPECT--
bool(true)
bool(true)

@ -0,0 +1,65 @@
--TEST--
Native class: a throwing derived finalizer does not skip base finalizers
--FILE--
<?php
#[Native]
class NativeThrowingFinalizerBase
{
public function __destruct()
{
global $events;
$events[] = 'base';
}
}
#[Native]
class NativeThrowingFinalizerChild extends NativeThrowingFinalizerBase
{
public function __destruct()
{
global $events;
$events[] = 'child';
throw new RuntimeException('child finalizer failed');
}
}
#[Native]
class NativeFinalizerPressureObject
{
public int $value;
}
function collectThrowingFinalizer(): void
{
for ($i = 0; $i < 400000; $i++) {
try {
$filler = new NativeFinalizerPressureObject();
} catch (RuntimeException $error) {
echo $error->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"
}

@ -0,0 +1,132 @@
--TEST--
Native class: constructor, clone and finalizer exceptions preserve heap invariants
--FILE--
<?php
class NativeLifecycleProbe
{
public function __construct(private string $name)
{
}
public function __destruct()
{
global $releasedProbes;
$releasedProbes[] = $this->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"
}

@ -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

Loading…
Cancel
Save