Native Class 支持 [第三阶段]

debug/ci-class-id-cache
韩天峰 1 week ago
parent d5f0a113e6
commit de8fdaf464
  1. 48
      docs/NATIVE_CLASS_OBJECT.md
  2. 10
      phpunit/code/native-class-array-access-isset.php
  3. 10
      phpunit/code/native-class-array-access-unset.php
  4. 10
      phpunit/code/native-class-array-access-write.php
  5. 10
      phpunit/code/native-class-array-access.php
  6. 11
      phpunit/code/native-class-array-dim-key.php
  7. 10
      phpunit/code/native-class-array-key.php
  8. 7
      phpunit/code/native-class-box-property.php
  9. 18
      phpunit/code/native-class-coalesce-assign-type.php
  10. 19
      phpunit/code/native-class-coalesce-property-type.php
  11. 10
      phpunit/code/native-class-compound-assignment.php
  12. 13
      phpunit/code/native-class-dynamic-class-constant.php
  13. 10
      phpunit/code/native-class-dynamic-new.php
  14. 13
      phpunit/code/native-class-dynamic-static-call.php
  15. 14
      phpunit/code/native-class-foreach.php
  16. 19
      phpunit/code/native-class-generator-factory.php
  17. 14
      phpunit/code/native-class-generator-local.php
  18. 11
      phpunit/code/native-class-generator-method.php
  19. 13
      phpunit/code/native-class-generator-parameter.php
  20. 17
      phpunit/code/native-class-generator-yield.php
  21. 10
      phpunit/code/native-class-increment.php
  22. 12
      phpunit/code/native-class-reflection-class.php
  23. 18
      phpunit/code/native-class-reflection-member.php
  24. 13
      phpunit/code/native-class-switch-case.php
  25. 13
      phpunit/code/native-class-switch.php
  26. 13
      phpunit/code/native-class-throw.php
  27. 10
      phpunit/code/native-class-unary-operator.php
  28. 14
      phpunit/code/native-class-variable-method.php
  29. 14
      phpunit/code/native-class-variable-property.php
  30. 196
      phpunit/src/NativeClass/NativeClassValidationTest.php
  31. 52
      src/CompilerBase.php
  32. 14
      src/Generator/FiberGenerator.php
  33. 150
      src/NativeClass/NativeClassSupportTrait.php
  34. 4
      src/Parser/ArrayExpressionTrait.php
  35. 82
      src/Parser/AssignOpTrait.php
  36. 7
      src/Parser/BinaryOpTrait.php
  37. 3
      src/Parser/ClassConstantFetchTrait.php
  38. 4
      src/Parser/ExceptionControlFlowTrait.php
  39. 6
      src/Parser/ForeachTrait.php
  40. 3
      src/Parser/MethodCallTrait.php
  41. 1
      src/Parser/PropertyAccessTrait.php
  42. 39
      src/Parser/SelectionExpressionTrait.php
  43. 10
      src/Parser/SwitchTrait.php
  44. 3
      src/Parser/UnaryExpressionTrait.php
  45. 12
      src/Preprocessor.php
  46. 2
      src/Translator.php
  47. 17
      tests/compiler/native-class/conditions.phpt
  48. 22
      tests/compiler/native-class/exit-conversion.phpt
  49. 43
      tests/compiler/native-class/fiber-lifetime.phpt
  50. 40
      tests/compiler/native-class/fiber-shutdown.phpt
  51. 28
      tests/compiler/native-class/gc-threshold.phpt
  52. 4
      tests/compiler/native-class/include-native-scope.inc
  53. 19
      tests/compiler/native-class/include-native-scope.phpt
  54. 5
      tests/compiler/native-class/keyword-conversions.phpt
  55. 60
      tests/compiler/native-class/match-identity.phpt
  56. 46
      tests/compiler/native-class/strict-identity.phpt
  57. 27
      tests/compiler/native-class/value-selection.phpt

@ -328,11 +328,17 @@ b->x = 10;
Native Class Object 不使用 `std::shared_ptr`。`std::shared_ptr` 的控制块、原子引用计数和循环引用问题与本特性的极致性能目标不符。
Native Object 的严格比较使用指针身份:`===`/`!==` 判断两个槽是否指向同一个 Native
对象,也支持与 `null` 比较。PHP 的 `==`/`!=` 会递归比较 Zend Object 属性;Native
对象,也支持与 `null` 比较。与任何 Zend 标量或 Zend Object 的严格比较恒为
`false`,不会把裸指针隐式转换为 `bool`。`match` 条件同样使用这一指针身份规则。
PHP 的 `==`/`!=` 会递归比较 Zend Object 属性;Native
Object 没有 Zend object handler,而且对象图可能包含环,因此不提供隐式字段值比较。
松散比较、大小比较和算术/位运算在编译期直接报错。需要值相等语义时应声明一个具有
明确字段和循环处理规则的普通 Native 方法。
一元算术/位运算、`++`/`--`、复合算术赋值和 `switch` 也依赖 PHP 的数值或松散比较
语义,因此禁止用于 Native Object。该检查必须发生在 C++ 生成前,避免裸指针意外进入
合法但危险的 C++ 指针算术。
`isset($native)``empty($native)` 直接检查裸指针是否为 `nullptr`。命名 Native
属性链使用短路 lambda 逐级检查中间指针,不会把指针传入 `php::Variant`,因此
`isset($node->next->next)` 在中间槽为空时返回 `false`,而不是触发空对象调用。
@ -789,6 +795,28 @@ nullable class 必须使用 `?Point`,使用相同指针表示,`nullptr` 表
参数和返回值必须显式声明具体 Native Class(或其 nullable 形式),不能通过 `mixed`
`object` 或 Interface carrier 传递。
例如:
```php
function bar(Point $point): void
{
// 函数入口已经完成非空检查;进入函数体后 $point 一定指向 Point 对象。
echo $point->x;
}
function maybeBar(?Point $point): void
{
// $point 可能是空指针,使用前必须先收窄或由成员访问生成空值检查。
if ($point !== null) {
echo $point->x;
}
}
```
两者的 C++ ABI 都使用 `php_app__point *`,但契约不同:`bar()` 在执行第一条用户
语句前拒绝 `nullptr`;`maybeBar()` 接受 `nullptr`。这项入口保证只约束传入时的值,
函数内部仍可把自己的局部指针槽重新赋为 `null`,且不会改变调用者的变量槽。
## 10. ZendVM 边界
Native Object 没有对应的 `zval` 表示,因此只能传给明确接受相同 Native Class或其 Native 基类的参数。Interface 只用于校验 Native Class 的声明契约,不能作为 Native Object 的参数、属性、变量或返回值 carrier。
@ -803,6 +831,12 @@ Native Class 的字段可以保存 `php::Var`、`php::Array` 或 `php::Object`
- 使用 `$nativeObject->$expr()`、`$nativeObject->{$expr}()` 等变量方法名调用。
- 放入普通 PHP `array`
- 捕获到需要注册为 Zend Closure 的闭包中。
- 作为 TypePHP Generator 的参数、`this`、局部变量、返回类型或 `yield` 值。Generator
由 Zend Closure/Fiber 状态机表示,Native pointer 不进入该 Zend 状态。
- 作为 Fiber API 的传入值、恢复值或 Closure capture。普通 TypePHP 函数可以在自己的
C++ 局部槽中保存 Native Object 并跨越 `Fiber::suspend()`;Native Root Frame 使用
可任意 O(1) 摘除的 thread-local 双向侵入链表,GC 会同时扫描运行中与挂起 Fiber 的
有效 frame,不依赖跨 Fiber 的 LIFO 析构顺序。
- 作为 `call_user_func()` 等动态 callback 的 receiver。
- 保存到 ZendVM 全局变量或对象属性中。
@ -1118,6 +1152,13 @@ Native Class 支持 `toArray()`、`toString()`、`toInt()`、`toFloat()`、`toBo
方法返回类型必须与关键词类型完全一致。例如 `toArray(): array`、`toInt(): int`、`toString(): string`;缺少方法、接收参数、按引用返回或返回类型不同均为编译期 FatalError。
对象条件与显式转换是两套语义。`if ($object)`、`!$object`、`$left && $right` 和
`$left || $right` 只判断 Native pointer 是否为 `nullptr`,不会调用 `toBool()`;这与
PHP 对普通对象“存在即为 true”的语义一致,也使 nullable Native pointer 可以直接作为
条件。只有显式 `(bool) $object``$object->toBool()` 才解析为 Native `toBool(): bool`
调用;没有定义该方法时在编译期报错。即使类定义的 `toBool()` 返回 `false`,一个非空
对象在 `if ($object)` 中仍为 `true`
`__toString(): string``toString(): string` 的兼容别名。对 Native Object 使用 `toString()`、`strval($object)`、`(string) $object`、字符串拼接或 `echo` 时,编译器优先使用实际声明的 `toString()`,若不存在则使用 `__toString()`
与 PHP 一致,声明合法 `__toString()` 的 Native Class 在编译期隐式满足 `Stringable`
@ -1193,7 +1234,9 @@ $json = json_encode($nativeObject->toArray());
| Interface | 普通 Interface 注册到 ZendVM;Native `implements` 只做编译期契约校验,Native Object 不能转换为 Interface 值 |
| `instanceof` | 支持编译期可解析的 Native class 和 Interface,直接折叠;变量 class 不支持 |
| `===` / `!==` | 支持 Native 指针身份及与 `null` 的严格比较 |
| Native 条件的 `match` | 支持,使用与 `===` 相同的指针身份规则 |
| `==` / `!=`、大小及算术/位运算 | 不支持,编译期 FatalError;值相等应使用显式 Native 方法 |
| 一元算术/位运算、`++`/`--`、复合算术赋值、`switch` | 不支持,编译期 FatalError |
| 动态属性 | 不支持 |
| `$nativeObject->$expr()` | 不支持,只允许命名方法调用 |
| `__call()` / `__callStatic()` | 不支持;Native Call 必须在编译期解析为确定符号 |
@ -1201,6 +1244,8 @@ $json = json_encode($nativeObject->toArray());
| `__sleep()` / `__wakeup()` / `__serialize()` / `__unserialize()` | 不支持;Native Object 不进入 Zend 序列化系统 |
| `__set_state()` / `__debugInfo()` | 不支持;Native Object 没有相应 Zend object handler |
| Reflection | 不支持 |
| TypePHP Generator 保存或产出 Native Object | 不支持;编译期 FatalError |
| 普通函数的 Native 局部变量跨 `Fiber::suspend()` | 支持;Root Frame 注册表允许非 LIFO Fiber 生命周期 |
| `get_class()` / `get_parent_class()` / `get_called_class()` | 不支持 Native runtime introspection;使用 `self::class`、`parent::class` 或具体类名 |
| WeakReference | 不支持 |
| PHP serialize | 不支持 |
@ -1208,6 +1253,7 @@ $json = json_encode($nativeObject->toArray());
| 动态 callback | 不支持 |
| 动态 PHP/eval 使用 | 不支持 |
| 普通 PHP array 保存 Native Object | 不支持 |
| Native Object 作为 PHP array key 或 `[]` receiver | 不支持;编译期 FatalError |
| Box/Std Container 属性 | 不支持 |
| Box 保存 Native Object | 不支持 |
| 局部 Std Container 保存 Native Object | 支持具体 Native class value type;容器 Root Frame 参与 GC tracing |

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeArrayAccessIsset {}
function main(): void
{
$value = new NativeArrayAccessIsset();
var_dump(isset($value[0]));
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeArrayAccessUnset {}
function main(): void
{
$value = new NativeArrayAccessUnset();
unset($value[0]);
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeArrayAccessWrite {}
function main(): void
{
$value = new NativeArrayAccessWrite();
$value[0] = 1;
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeArrayAccessOperand {}
function main(): void
{
$value = new NativeArrayAccessOperand();
var_dump($value[0]);
}

@ -0,0 +1,11 @@
<?php
#[Native]
class NativeArrayDimKey {}
function main(): void
{
$key = new NativeArrayDimKey();
$values = [];
$values[$key] = 'value';
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeArrayKey {}
function main(): void
{
$key = new NativeArrayKey();
$values = [$key => 'value'];
}

@ -0,0 +1,7 @@
<?php
#[Native]
class NativeBoxProperty
{
public Box $value;
}

@ -0,0 +1,18 @@
<?php
#[Native]
class NativeCoalesceExpected {}
#[Native]
class NativeCoalesceWrong {}
function maybeExpected(): ?NativeCoalesceExpected
{
return null;
}
function main(): void
{
$value = maybeExpected();
$value ??= new NativeCoalesceWrong();
}

@ -0,0 +1,19 @@
<?php
#[Native]
class NativeCoalescePropertyExpected {}
#[Native]
class NativeCoalescePropertyWrong {}
#[Native]
class NativeCoalesceHolder
{
public ?NativeCoalescePropertyExpected $value;
}
function main(): void
{
$holder = new NativeCoalesceHolder();
$holder->value ??= new NativeCoalescePropertyWrong();
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeCompoundOperand {}
function main(): void
{
$value = new NativeCompoundOperand();
$value += 1;
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeDynamicConstantTarget
{
public const VALUE = 1;
}
function main(): void
{
$target = new NativeDynamicConstantTarget();
var_dump($target::VALUE);
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeDynamicNewTarget {}
function main(): void
{
$target = new NativeDynamicNewTarget();
$value = new $target();
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeDynamicStaticTarget
{
public function method(): void {}
}
function main(): void
{
$target = new NativeDynamicStaticTarget();
$target::method();
}

@ -0,0 +1,14 @@
<?php
#[Native]
class NativeForeachValue
{
}
function main(): void
{
$value = new NativeForeachValue();
foreach ($value as $item) {
}
}

@ -0,0 +1,19 @@
<?php
#[Native]
class NativeGeneratorFactoryValue
{
public int $value = 1;
}
function createNativeGeneratorValue(): NativeGeneratorFactoryValue
{
return new NativeGeneratorFactoryValue();
}
function nativeGeneratorFactory(): iterable
{
$value = createNativeGeneratorValue();
yield $value->value;
}

@ -0,0 +1,14 @@
<?php
#[Native]
class NativeGeneratorLocal
{
public int $value = 1;
}
function nativeGeneratorLocal(): iterable
{
$value = new NativeGeneratorLocal();
yield $value->value;
}

@ -0,0 +1,11 @@
<?php
#[Native]
class NativeGeneratorMethod
{
public function values(): iterable
{
yield 1;
}
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeGeneratorParameter
{
public int $value = 1;
}
function nativeGeneratorParameter(NativeGeneratorParameter $value): iterable
{
yield $value->value;
}

@ -0,0 +1,17 @@
<?php
#[Native]
class NativeGeneratorYieldValue
{
}
function createNativeGeneratorYieldValue(): NativeGeneratorYieldValue
{
return new NativeGeneratorYieldValue();
}
function nativeGeneratorYield(): iterable
{
yield createNativeGeneratorYieldValue();
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeIncrementOperand {}
function main(): void
{
$value = new NativeIncrementOperand();
$value++;
}

@ -0,0 +1,12 @@
<?php
#[Native]
class NativeReflectionClassValue
{
}
function main(): void
{
new ReflectionClass(NativeReflectionClassValue::class);
}

@ -0,0 +1,18 @@
<?php
#[Native]
class NativeReflectionMemberValue
{
public int $value;
public function read(): int
{
return $this->value;
}
}
function main(): void
{
new ReflectionMethod(NativeReflectionMemberValue::class, 'read');
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeSwitchCase {}
function main(): void
{
$value = new NativeSwitchCase();
switch (1) {
case $value:
break;
}
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeSwitchOperand {}
function main(): void
{
$value = new NativeSwitchOperand();
switch ($value) {
case null:
break;
}
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeThrownValue
{
}
function main(): void
{
$value = new NativeThrownValue();
throw $value;
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeUnaryOperand {}
function main(): void
{
$value = new NativeUnaryOperand();
$result = -$value;
}

@ -0,0 +1,14 @@
<?php
#[Native]
class NativeVariableMethod
{
public function method(): void {}
}
function main(): void
{
$value = new NativeVariableMethod();
$method = 'method';
$value->$method();
}

@ -0,0 +1,14 @@
<?php
#[Native]
class NativeVariableProperty
{
public int $property = 1;
}
function main(): void
{
$value = new NativeVariableProperty();
$property = 'property';
var_dump($value->$property);
}

@ -69,6 +69,13 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-std-container-property.php');
}
public function testRejectsBoxProperty(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native class properties cannot use Box types');
$this->compile('native-class-box-property.php');
}
public function testRejectsNativeStdContainerConversionToPhpArray(): void
{
$this->expectException(TestError::class);
@ -125,6 +132,27 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-instanceof.php');
}
public function testRejectsNativeObjectAsDynamicNewTarget(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be used as dynamic class targets');
$this->compile('native-class-dynamic-new.php');
}
public function testRejectsNativeObjectAsDynamicStaticCallTarget(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be used as dynamic class targets');
$this->compile('native-class-dynamic-static-call.php');
}
public function testRejectsNativeObjectAsDynamicClassConstantTarget(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be used as dynamic class targets');
$this->compile('native-class-dynamic-class-constant.php');
}
public function testRejectsNativeObjectStoredInPhpArray(): void
{
$this->expectException(TestError::class);
@ -328,6 +356,20 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-dynamic-magic-method.php');
}
public function testRejectsVariableNativeMethodCalls(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Dynamic native object method calls are not supported');
$this->compile('native-class-variable-method.php');
}
public function testRejectsVariableNativePropertyAccess(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Dynamic native object property access is not supported');
$this->compile('native-class-variable-property.php');
}
public function testRejectsDynamicStaticMagicMethodBeforeGenericStaticDiagnostic(): void
{
$this->expectException(TestError::class);
@ -426,6 +468,97 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-arithmetic-operator.php');
}
public function testRejectsNativeUnaryOperators(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects do not support the unary `-` operator');
$this->compile('native-class-unary-operator.php');
}
public function testRejectsIncrementingNativePointerSlots(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects do not support the `++` operator');
$this->compile('native-class-increment.php');
}
public function testRejectsNativeCompoundAssignments(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects do not support the `+=` operator');
$this->compile('native-class-compound-assignment.php');
}
public function testRejectsIncompatibleNativeCoalesceAssignment(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Cannot assign native object `NativeCoalesceWrong` to `NativeCoalesceExpected`');
$this->compile('native-class-coalesce-assign-type.php');
}
public function testRejectsIncompatibleNativePropertyCoalesceAssignment(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Cannot assign object of class `NativeCoalescePropertyWrong` to object property `value` of class `NativeCoalescePropertyExpected`');
$this->compile('native-class-coalesce-property-type.php');
}
public function testRejectsNativeSwitchConditions(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be used as switch values');
$this->compile('native-class-switch.php');
}
public function testRejectsNativeSwitchCaseValues(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be used as switch values');
$this->compile('native-class-switch-case.php');
}
public function testRejectsNativeArrayLiteralKeys(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be used as PHP array keys');
$this->compile('native-class-array-key.php');
}
public function testRejectsNativeArrayDimensionKeys(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be used as PHP array keys');
$this->compile('native-class-array-dim-key.php');
}
public function testRejectsArrayAccessOnNativeObjects(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects do not support array dimension access');
$this->compile('native-class-array-access.php');
}
public function testRejectsArrayWritesOnNativeObjects(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects do not support array dimension access');
$this->compile('native-class-array-access-write.php');
}
public function testRejectsArrayIssetOnNativeObjects(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects do not support array dimension access');
$this->compile('native-class-array-access-isset.php');
}
public function testRejectsArrayUnsetOnNativeObjects(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects do not support array dimension access');
$this->compile('native-class-array-access-unset.php');
}
public function testRejectsInaccessibleNativeCloneMethod(): void
{
$this->expectException(TestError::class);
@ -524,4 +657,67 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-null-return.php');
}
public function testRejectsNativeObjectGeneratorParameter(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Generator functions cannot accept, capture, or return Native objects');
$this->compile('native-class-generator-parameter.php');
}
public function testRejectsNativeObjectGeneratorMethod(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Generator functions cannot accept, capture, or return Native objects');
$this->compile('native-class-generator-method.php');
}
public function testRejectsNativeObjectConstructionInsideGenerator(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be created inside Generator functions');
$this->compile('native-class-generator-local.php');
}
public function testRejectsNativeObjectRetainedByGeneratorFromFactory(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Generator functions cannot retain Native objects across suspension');
$this->compile('native-class-generator-factory.php');
}
public function testRejectsYieldingNativeObject(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be yielded through a Zend Generator');
$this->compile('native-class-generator-yield.php');
}
public function testRejectsNativeClassReflection(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native class `NativeReflectionClassValue` cannot be used with ReflectionClass');
$this->compile('native-class-reflection-class.php');
}
public function testRejectsNativeMemberReflection(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native class `NativeReflectionMemberValue` cannot be used with ReflectionMethod');
$this->compile('native-class-reflection-member.php');
}
public function testRejectsThrowingNativeObject(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be thrown as Zend exceptions');
$this->compile('native-class-throw.php');
}
public function testRejectsIteratingNativeObjectThroughZendForeach(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be iterated by PHP foreach');
$this->compile('native-class-foreach.php');
}
}

@ -1480,6 +1480,7 @@ class CompilerBase implements PropertyAccessContext
protected function parseArrayKey(NodeAbstract $expr): string
{
$this->assertNotNativeObjectArrayKey($expr);
$key = $this->parseIdentifier($expr);
if (str_starts_with($key, self::LITERAL_STRINGS)) {
$key = "{$key}.str()";
@ -3179,6 +3180,7 @@ class CompilerBase implements PropertyAccessContext
protected function parsePreInc(Expr\PreInc $expr): string
{
$this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, '++');
$this->assertNotNullsafeWriteContext($expr->var);
$this->assertNativePropertyHookDirectWriteTarget($expr->var);
$result = $this->genDynamicPropIncDec($expr->var, '+', true);
@ -3566,6 +3568,7 @@ class CompilerBase implements PropertyAccessContext
protected function parsePostOp(Expr\PostDec|Expr\PostInc $expr, string $op): string
{
$this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, str_repeat($op, 2));
$this->assertNotNullsafeWriteContext($expr->var);
$this->assertNativePropertyHookDirectWriteTarget($expr->var);
$result = $this->genDynamicPropIncDec($expr->var, $op, false);
@ -3615,6 +3618,7 @@ class CompilerBase implements PropertyAccessContext
protected function parsePreDec(Expr\PreDec $expr): string
{
$this->assertNativeObjectOperatorOperandSupported($expr->var, $expr, '--');
$this->assertNotNullsafeWriteContext($expr->var);
$this->assertNativePropertyHookDirectWriteTarget($expr->var);
$result = $this->genDynamicPropIncDec($expr->var, '-', true);
@ -3633,6 +3637,9 @@ class CompilerBase implements PropertyAccessContext
protected function parsePrint(Expr\Print_ $expr): string
{
$this->assertExprCanBeUsedAsValue($expr->expr, 'print operand');
if ($this->isNativeObjectClass($this->detectClassOfExpr($expr->expr))) {
return 'php::print(' . $this->parseExprToString($expr->expr) . ')';
}
return 'php::print(' . $this->parseExprAsValue($expr->expr) . ')';
}
@ -3667,6 +3674,9 @@ class CompilerBase implements PropertyAccessContext
protected function parseNew(Expr\New_ $expr): string
{
if (!$expr->class instanceof Node\Stmt\Class_ && !$this->isNameExpr($expr->class)) {
$this->assertNotNativeObjectDynamicClassTarget($expr->class, $expr);
}
$ctorClassName = '';
// 匿名类
if ($expr->class instanceof Node\Stmt\Class_) {
@ -3719,6 +3729,7 @@ class CompilerBase implements PropertyAccessContext
$className = $this->getNamespacedClassName($className);
}
$ctorClassName = $className;
$this->assertNativeClassNotUsedWithReflection($expr, $className);
if ($this->isAbstractClass($className)) {
$this->fatalError($expr, "abstract class `{$className}` cannot be instantiated");
}
@ -3732,6 +3743,14 @@ class CompilerBase implements PropertyAccessContext
);
}
if ($this->isNativeObjectClass($className)) {
if ($this->inGeneratorBody) {
// Generator lowering deliberately keeps Native
// values out of the Zend Closure/Fiber state.
$this->fatalError(
$expr,
'Native objects cannot be created inside Generator functions',
);
}
$cppClass = $this->getNativeObjectCppName($className);
$descriptor = $this->getNativeObjectDescriptorName($className);
if ($constructor === null) {
@ -3909,9 +3928,16 @@ class CompilerBase implements PropertyAccessContext
// Although C++17 orders the braced-list elements, materializing an
// expression prevents captured statements from a later part from
// being hoisted ahead of an earlier Call.
$list[] = $part instanceof Node\InterpolatedStringPart
? $this->parseExpr($part)
: $this->parseOrderedOperand($part, false);
if ($part instanceof Node\InterpolatedStringPart) {
$list[] = $this->parseExpr($part);
} elseif ($this->isNativeObjectClass($this->detectClassOfExpr($part))) {
$list[] = $this->parseOrderedOperand(
new Expr\MethodCall($part, new Node\Identifier('toString')),
false,
);
} else {
$list[] = $this->parseOrderedOperand($part, false);
}
}
return 'php::concat({' . implode(', ', $list) . '})';
@ -3957,7 +3983,9 @@ class CompilerBase implements PropertyAccessContext
if (!$node->expr) {
return 'php::aotExit()';
}
$status = $this->parseExprAsValue($node->expr);
$status = $this->isNativeObjectClass($this->detectClassOfExpr($node->expr))
? $this->parseExprToString($node->expr)
: $this->parseExprAsValue($node->expr);
return 'php::aotExit(' . $status . ')';
}
@ -4063,6 +4091,13 @@ class CompilerBase implements PropertyAccessContext
if ($name === 'this_' || str_starts_with($name, 'tmp_var_')) {
continue;
}
// Native pointers have no zval representation. Boxing one into
// the include symbol table would silently select a bool overload
// and expose `true` instead of the object. Dynamic PHP is not
// allowed to observe Native locals, so leave these names absent.
if ($this->isNativeObjectVar($name)) {
continue;
}
$phpName = $this->unescapeVarName($name);
$scope[] = '{ ' . $this->getLiteralString($phpName) . '.str(), php::Var(' . $name . ') }';
}
@ -4879,10 +4914,11 @@ class CompilerBase implements PropertyAccessContext
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<void **>(&_native_this_root)';
} elseif ($this->hasLocalVar($name)) {
// `this_` and Native parameters are borrowed from a generated
// caller which already owns a root slot (nativeConstruct/
// nativeClone do the same for lifecycle callbacks). Only
// function-owned pointer slots must be registered here.
if ($name !== 'this_' && !$this->hasArgument($name) && $this->hasLocalVar($name)) {
$rootSlots[] = 'reinterpret_cast<void **>(&' . $name . ')';
}
}

@ -185,6 +185,11 @@ trait FiberGenerator
private function materializeYieldOperand(Node $expr, bool $force = false): string
{
if ($this->isNativeObjectClass($this->detectClassOfExpr($expr))) {
// Yield payloads are stored in a Zend array and cross the Fiber /
// Generator object boundary. A Native pointer has no zval form.
$this->fatalError($expr, 'Native objects cannot be yielded through a Zend Generator');
}
[$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr);
foreach ($beforeStmts as $stmt) {
$this->context->beforeStmtLines[] = $stmt;
@ -286,6 +291,15 @@ trait FiberGenerator
if ($v->stmts) {
$body .= $this->parseStmts($v->stmts);
}
if ($this->context->nativeObjects !== []) {
// TypePHP Generators are represented by Zend Closure/Fiber state.
// Keep Native values out of that generated state even though the
// runtime root registry itself now tolerates Fiber suspension.
$this->fatalError(
$v,
'Generator functions cannot retain Native objects across suspension',
);
}
$body .= $this->getIndent() . 'return ' . self::VALUE_NULL . ';' . PHP_EOL;
if ($this->context->needsUserCodeCallableScope) {
$body = $this->genUserCodeCallableScopeGuard() . $body;

@ -315,6 +315,50 @@ trait NativeClassSupportTrait
$this->fatalError($expr, "Native objects do not support the `{$operator}` operator{$suffix}");
}
/**
* A Native object expression is a raw C++ pointer. Applying arithmetic
* unary or update operators to it could otherwise become pointer
* arithmetic, which is valid C++ but has no PHP object semantics.
*/
protected function assertNativeObjectOperatorOperandSupported(
NodeAbstract $operand,
NodeAbstract $errorNode,
string $operator,
bool $unary = false,
): void {
if (!$this->isNativeObjectClass($this->detectClassOfExpr($operand))) {
return;
}
$prefix = $unary ? 'unary ' : '';
$this->fatalError($errorNode, "Native objects do not support the {$prefix}`{$operator}` operator");
}
protected function assertNotNativeObjectArrayKey(NodeAbstract $key): void
{
if ($this->isNativeObjectClass($this->detectClassOfExpr($key))) {
$this->fatalError($key, 'Native objects cannot be used as PHP array keys');
}
}
protected function assertNotNativeObjectArrayDimensionReceiver(
NodeAbstract $receiver,
NodeAbstract $errorNode,
): void {
if ($this->isNativeObjectClass($this->detectClassOfExpr($receiver))) {
$this->fatalError($errorNode, 'Native objects do not support array dimension access');
}
}
protected function assertNotNativeObjectDynamicClassTarget(
NodeAbstract $target,
NodeAbstract $errorNode,
): void {
if ($this->isNativeObjectClass($this->detectClassOfExpr($target))) {
$this->fatalError($errorNode, 'Native objects cannot be used as dynamic class targets');
}
}
protected function getNativeObjectCppName(string|ClassDef $class): string
{
$classDef = $class instanceof ClassDef ? $class : $this->getClass(ltrim($class, '\\'));
@ -539,6 +583,68 @@ trait NativeClassSupportTrait
}
}
/**
* Reflection constructors accept a class name string, so the ordinary
* Native-pointer Zend boundary check cannot see this escape. Reject known
* Native class literals before generating a lookup for a class which is
* intentionally absent from the Zend class table.
*/
protected function assertNativeClassNotUsedWithReflection(
Node\Expr\New_ $expr,
string $constructedClass,
): void {
$reflectionClass = strtolower(ltrim($constructedClass, '\\'));
if (!in_array($reflectionClass, [
'reflectionclass',
'reflectionmethod',
'reflectionproperty',
'reflectionclassconstant',
'reflectionenum',
'reflectionenumunitcase',
'reflectionenumbackedcase',
], true) || $expr->args === []) {
return;
}
$target = $expr->args[0]->value;
$nativeClass = '';
if ($this->isScalarString($target)) {
// Reflection string arguments are runtime names, not names relative
// to the current PHP namespace.
$candidate = ltrim($target->value, '\\');
if ($this->isNativeObjectClass($candidate)) {
$nativeClass = $candidate;
}
} elseif ($this->isClassConstFetch($target)
&& $this->isNameExpr($target->class)
&& $this->isIdExpr($target->name)
&& strtolower($this->parseIdentifier($target->name)) === 'class'
) {
$candidate = $this->parseIdentifier($target->class);
if ($candidate === 'self' || $candidate === 'static') {
$candidate = $this->getFullClassName();
} elseif ($candidate === 'parent') {
$candidate = $this->classDef?->extends ?? '';
} else {
$candidate = $this->getNamespacedClassName($candidate);
}
if ($this->isNativeObjectClass($candidate)) {
$nativeClass = $candidate;
}
}
if ($nativeClass !== '') {
$reflectionName = strrchr($constructedClass, '\\');
$reflectionName = $reflectionName === false
? $constructedClass
: substr($reflectionName, 1);
$this->fatalError(
$target,
"Native class `{$nativeClass}` cannot be used with {$reflectionName}",
);
}
}
protected function getNativeObjectReturnType(FunctionDef $function): ?string
{
if ($function->returnType !== Type::OBJECT || !$this->isNativeObjectClass($function->returnClass)) {
@ -614,6 +720,50 @@ trait NativeClassSupportTrait
return isset($this->context->nonNullNativeObjects[$name]);
}
/**
* Conservative straight-line non-null proof for a Native pointer value.
* This deliberately excludes properties (a non-nullable Native field has
* a nullptr zero value) and control-flow expressions. The caller may only
* retain the proof at function top level, where no branch merge is needed.
*/
protected function isNativeObjectExpressionKnownNonNull(NodeAbstract $expr): bool
{
if ($expr instanceof Node\Expr\ErrorSuppress) {
return $this->isNativeObjectExpressionKnownNonNull($expr->expr);
}
if ($expr instanceof Node\Expr\New_ || $expr instanceof Node\Expr\Clone_) {
return $this->isNativeObjectClass($this->detectClassOfExpr($expr));
}
if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) {
return $this->isNativeObjectKnownNonNull($this->parseIdentifier($expr));
}
if ($expr instanceof Node\Expr\FuncCall
&& ($this->isNameExpr($expr->name) || $this->isFullNameExpr($expr->name))
) {
$native = $this->findNativeFunction($this->parseIdentifier($expr->name));
if ($native !== false) {
$function = $this->getFunction($native);
return $this->isNativeObjectClass($function->returnClass)
&& !$function->returnNullable;
}
}
if ($expr instanceof Node\Expr\MethodCall
&& $this->isIdExpr($expr->name)
) {
$receiverClass = $this->detectClassOfExpr($expr->var);
if ($this->isNativeObjectClass($receiverClass)) {
$method = $this->findNativeObjectMethod(
$receiverClass,
$this->parseIdentifier($expr->name),
);
return $method !== null
&& $this->isNativeObjectClass($method->functionDef->returnClass)
&& !$method->functionDef->returnNullable;
}
}
return false;
}
protected function getNativeObjectReceiver(string $name): string
{
if ($name === 'this_') {

@ -170,6 +170,10 @@ trait ArrayExpressionTrait
protected function parseArrayDimFetch(Expr\ArrayDimFetch $node): string
{
$this->assertNotNativeObjectArrayDimensionReceiver($node->var, $node);
if ($node->dim !== null) {
$this->assertNotNativeObjectArrayKey($node->dim);
}
$write = $this->isArrayDimFetchUpdate($node);
if ($this->isStdContainerExpr($node)) {
if ($write && $node->dim === null) {

@ -21,6 +21,12 @@ trait AssignOpTrait
{
protected function parseAssignArrayDim(NodeAbstract $left, NodeAbstract $right): string
{
if ($left instanceof Expr\ArrayDimFetch && $left->dim !== null) {
$this->assertNotNativeObjectArrayKey($left->dim);
}
if ($left instanceof Expr\ArrayDimFetch) {
$this->assertNotNativeObjectArrayDimensionReceiver($left->var, $left);
}
if ($this->isPropertyFetch($left)) {
return $this->parseAssignPropertyArrayDim($left, $right);
}
@ -288,6 +294,7 @@ trait AssignOpTrait
$finalVarType = $this->getNormalAssignType($type);
$runtimeObjectAssignClass = '';
$assigningNullToTypedObject = false;
$markNativeObjectNonNull = false;
if ($type === Type::VOID) {
$type = Type::VAR;
}
@ -358,6 +365,11 @@ trait AssignOpTrait
}
// 类型推断,获取对象的类名,如果不是对象则返回空字符串
$rightClass = $this->detectClassOfExpr($right);
$markNativeObjectNonNull = $this->context->scopeLevel <= 1
&& !$this->hasScopeGlobalVar($var)
&& !$this->hasStaticVar($var)
&& $this->isNativeObjectClass($rightClass)
&& $this->isNativeObjectExpressionKnownNonNull($right);
if ($this->isNativeObjectVar($var)) {
// Assignment rebinds the local pointer slot. Even an
// assignment nested in a conditional invalidates the simple
@ -520,6 +532,12 @@ trait AssignOpTrait
. ' return php::toObject(' . $checkedValue . ', ' . $this->getClassEntryPtr($runtimeObjectAssignClass) . ');'
. ' })(' . $rightExpr . ')';
}
if ($markNativeObjectNonNull) {
// Parsing is sequential: this proof affects only subsequent source
// expressions. Nested control flow is excluded above because its
// assignment may not execute on every path.
$this->markNativeObjectNonNull($var);
}
$leftExprType = $this->detectTypeOfExpr($left);
$rightExprType = $this->detectTypeOfExpr($right);
if ($propertyWriteTarget !== null && ($propertyDef = $this->getNativePropertyDef($left)) !== null) {
@ -611,6 +629,7 @@ trait AssignOpTrait
protected function parseAssignOp(Expr\AssignOp $node, string $op): string
{
$this->assertNativeObjectOperatorOperandSupported($node->var, $node, $op);
$this->assertNotNullsafeWriteContext($node->var);
$this->assertNativePropertyHookDirectWriteTarget($node->var);
$pythonOperator = $this->parsePythonAssignOperator($node);
@ -1077,11 +1096,34 @@ trait AssignOpTrait
{
$this->checkLeftValue($expr->var);
$rightClass = $this->detectClassOfExpr($expr->expr);
$nativeRight = $this->isNativeObjectClass($rightClass);
// An undefined variable must exist before generating its isset check.
// Keep it as Variant so NULL remains distinguishable from native defaults.
// A Native RHS establishes a typed nullptr slot; it must never be
// declared as Variant because boxing the raw pointer would coerce it
// to bool. Other values retain the normal nullable Variant behavior.
$var = $this->isVarExpr($expr->var) ? $this->parseIdentifier($expr->var) : null;
if ($var !== null && !$this->hasVar($var)) {
$this->addLocalVar($var, Type::VAR);
if ($nativeRight) {
$this->addLocalVar($var, $this->getNativeObjectPointerType($rightClass));
$this->addNativeObject($var, $rightClass);
} else {
$this->addLocalVar($var, Type::VAR);
}
}
if ($var !== null && $this->isNativeObjectVar($var)) {
$leftClass = $this->getNativeObjectVarClass($var);
if ($this->isNull($expr->expr)) {
// nullptr remains a valid nullable slot value.
} elseif (!$nativeRight) {
$this->fatalError($expr->expr, "Native object `\${$var}` cannot be converted to var/object");
} elseif (!$this->isObjectClassStaticallyAssignableTo($rightClass, $leftClass)) {
$this->fatalError(
$expr->expr,
"Cannot assign native object `{$rightClass}` to `{$leftClass}`",
);
}
}
$isset = $this->parseChainedExpr($expr->var, self::OP_ISSET);
@ -1093,13 +1135,49 @@ trait AssignOpTrait
$this->assertCanAssignPropertyWrite($propertyWriteTarget, $expr->expr);
}
$rightBeforeCount = count($this->context->beforeStmtLines);
$rightAfterCount = count($this->context->afterStmtLines);
$right = $this->parseExpr($expr->expr);
$rightBefore = array_slice($this->context->beforeStmtLines, $rightBeforeCount);
$rightAfter = array_slice($this->context->afterStmtLines, $rightAfterCount);
$this->context->beforeStmtLines = array_slice(
$this->context->beforeStmtLines,
0,
$rightBeforeCount,
);
$this->context->afterStmtLines = array_slice(
$this->context->afterStmtLines,
0,
$rightAfterCount,
);
if ($propertyWriteTarget !== null) {
$right = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $expr->expr, $right);
}
if ($this->isVarExpr($expr->expr) and !$this->hasVar($right)) {
$this->errorUndefinedVariable($expr->expr);
}
$targetClass = $var !== null && $this->isNativeObjectVar($var)
? $this->getNativeObjectVarClass($var)
: $this->detectClassOfExpr($expr->var);
if (($rightBefore !== [] || $rightAfter !== []) && $this->isNativeObjectClass($targetClass)) {
$tmp = $this->genTmpVarName();
$pointerType = $this->getNativeObjectPointerType($targetClass);
$this->addLocalVar($tmp, $pointerType);
$this->addNativeObject($tmp, $targetClass);
$code = '[&]() -> ' . $pointerType . ' {' . PHP_EOL;
$code .= $this->getIndent() . 'if (' . $isset . ') { return ' . $var . '; }' . PHP_EOL;
$code .= $this->formatCapturedStmtLines($rightBefore);
$code .= $this->getIndent() . $tmp . ' = ' . $right . ';' . PHP_EOL;
$code .= $this->formatCapturedStmtLines($rightAfter);
$code .= $this->getIndent() . $var . ' = ' . $tmp . ';' . PHP_EOL;
$code .= $this->getIndent() . 'return ' . $var . ';' . PHP_EOL;
$code .= $this->getIndent() . '}()';
return $code;
}
$this->appendCapturedStmtLinesToContext($rightBefore);
foreach ($rightAfter as $stmt) {
$this->context->afterStmtLines[] = $stmt;
}
return '(' . $isset . '?' . $var . ':(' . $var . ' = ' . $right . '))';
}

@ -810,6 +810,13 @@ trait BinaryOpTrait
if ($leftIsNative && $rightIsNative) {
return 'static_cast<const void *>(' . $left . ') == static_cast<const void *>(' . $right . ')';
}
if ($leftIsNative || $rightIsNative) {
// A Native pointer has no zval representation and can never be
// strictly identical to a Zend value. Explicit void casts retain
// both PHP side effects in left-to-right order without allowing
// C++ to coerce the pointer to bool.
return '(static_cast<void>(' . $left . '), static_cast<void>(' . $right . '), false)';
}
if ($right === 'nullptr') {
// The left operand may itself be an assignment or another compound
// expression. Parenthesize it before invoking Variant::isNull(), or

@ -17,6 +17,9 @@ trait ClassConstantFetchTrait
{
protected function parseClassConstFetch(Expr\ClassConstFetch $expr): string
{
if (!$this->isNameExpr($expr->class)) {
$this->assertNotNativeObjectDynamicClassTarget($expr->class, $expr);
}
$this->rejectPythonModuleClassConstantFetch($expr);
if (!$this->isIdExpr($expr->name)) {

@ -22,6 +22,9 @@ trait ExceptionControlFlowTrait
if ($this->method === '__destruct') {
$this->warning($expr, "Throwing exception in {$this->getFullClassName()}::__destruct() may cause memory leak");
}
if ($this->isNativeObjectClass($this->detectClassOfExpr($expr->expr))) {
$this->fatalError($expr, 'Native objects cannot be thrown as Zend exceptions');
}
$type = $this->detectTypeOfExpr($expr->expr);
if ($this->isNewExpr($expr->expr)) {
$ex = $this->parseExpr($expr->expr);
@ -237,4 +240,3 @@ trait ExceptionControlFlowTrait
}
}

@ -171,6 +171,12 @@ trait ForeachTrait
protected function parseForeach(Foreach_ $node): string
{
if ($this->isNativeObjectClass($this->detectClassOfExpr($node->expr))) {
// Native classes have no Zend iterator handlers and cannot carry
// Traversable as a runtime interface value. Expose iteration via
// an explicitly returned PHP array or a typed std container.
$this->fatalError($node->expr, 'Native objects cannot be iterated by PHP foreach');
}
if ($this->isVarExpr($node->expr)) {
$name = $this->parseIdentifier($node->expr);
if ($this->hasVar($name)) {

@ -749,6 +749,9 @@ trait MethodCallTrait
protected function parseStaticCall(Expr\StaticCall $expr): string
{
if (!$this->isNameExpr($expr->class)) {
$this->assertNotNativeObjectDynamicClassTarget($expr->class, $expr);
}
$pythonCall = $this->parsePythonModuleStaticCall($expr);
if ($pythonCall !== null) {
return $pythonCall;

@ -782,6 +782,7 @@ trait PropertyAccessTrait
$this->assertNotNullsafeWriteContext($var);
$this->assertNativePropertyHookDirectWriteTarget($var);
if ($this->isArrayDimFetch($var)) {
$this->assertNotNativeObjectArrayDimensionReceiver($var->var, $var);
if ($var->dim === null) {
$this->fatalError($var, 'Cannot use [] for array unset');
}

@ -124,6 +124,8 @@ trait SelectionExpressionTrait
{
$nativeClass = $this->detectClassOfExpr($expr);
$nativeSelection = $this->isNativeObjectClass($nativeClass);
$conditionNativeClass = $this->detectClassOfExpr($expr->cond);
$nativeCondition = $this->isNativeObjectClass($conditionNativeClass);
$returnType = $nativeSelection
? $this->getNativeObjectPointerType($nativeClass)
: Type::VAR;
@ -134,7 +136,13 @@ trait SelectionExpressionTrait
$this->errorUndefinedVariable($expr->cond);
}
} else {
$tmpVar = $this->addTmpVar(Type::VAR);
if ($nativeCondition) {
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, $this->getNativeObjectPointerType($conditionNativeClass));
$this->addNativeObject($tmpVar, $conditionNativeClass);
} else {
$tmpVar = $this->addTmpVar(Type::VAR);
}
$this->context->beforeStmtLines[] = $tmpVar . ' = ' . $var . ';';
$var = $tmpVar;
}
@ -157,12 +165,37 @@ trait SelectionExpressionTrait
$code .= $this->getIndent() . 'if (!' . $matched . ') {';
$code .= $this->formatCapturedStmtLines($beforeStmts);
if ($afterStmts) {
$condTmpVar = $this->addTmpVar(Type::VAR);
$condClass = $this->detectClassOfExpr($cond);
if ($nativeCondition && $this->isNativeObjectClass($condClass)) {
$condTmpVar = $this->genTmpVarName();
$this->addLocalVar($condTmpVar, $this->getNativeObjectPointerType($condClass));
$this->addNativeObject($condTmpVar, $condClass);
} else {
$condTmpVar = $this->addTmpVar(Type::VAR);
}
$code .= $this->getIndent() . "{$condTmpVar} = {$condValue};";
$code .= $this->formatCapturedStmtLines($afterStmts);
$condValue = $condTmpVar;
}
$code .= $this->getIndent() . $matched . ' = php::same(' . $var . ', ' . $condValue . ');';
if ($nativeCondition) {
$condClass = $this->detectClassOfExpr($cond);
if ($this->isNull($cond)) {
$comparison = $var . ' == nullptr';
} elseif ($this->isNativeObjectClass($condClass)
&& ($this->isObjectClassStaticallyAssignableTo($condClass, $conditionNativeClass)
|| $this->isObjectClassStaticallyAssignableTo($conditionNativeClass, $condClass))
) {
$comparison = $var . ' == ' . $condValue;
} else {
// PHP match uses strict identity. An unrelated Native
// pointer or a Zend value can never be identical, but
// its expression must still be evaluated for effects.
$comparison = '(static_cast<void>(' . $condValue . '), false)';
}
$code .= $this->getIndent() . $matched . ' = (' . $comparison . ');';
} else {
$code .= $this->getIndent() . $matched . ' = php::same(' . $var . ', ' . $condValue . ');';
}
$code .= $this->getIndent() . '}';
}
$code .= $this->getIndent() . 'if (' . $matched . ') {';

@ -16,6 +16,16 @@ trait SwitchTrait
protected function parseSwitch(Node\Stmt\Switch_ $v): string
{
$cond = $v->cond;
if ($this->isNativeObjectClass($this->detectClassOfExpr($cond))) {
$this->fatalError($cond, 'Native objects cannot be used as switch values');
}
foreach ($v->cases as $case) {
if ($case->cond !== null
&& $this->isNativeObjectClass($this->detectClassOfExpr($case->cond))
) {
$this->fatalError($case->cond, 'Native objects cannot be used as switch values');
}
}
$tmp_var = $this->genTmpVarName();
$type = $this->detectTypeOfExpr($cond);
$this->assertExprCanBeUsedAsValue($cond, 'switch condition');

@ -20,6 +20,7 @@ trait UnaryExpressionTrait
if ($pythonOperator !== null) {
return $pythonOperator;
}
$this->assertNativeObjectOperatorOperandSupported($expr->expr, $expr, '~', true);
$type = $this->detectTypeOfExpr($expr->expr);
$this->assertExprCanBeUsedAsValue($expr->expr, 'bitwise operand');
if ($type === Type::BIGINT) {
@ -89,6 +90,7 @@ trait UnaryExpressionTrait
if ($pythonOperator !== null) {
return $pythonOperator;
}
$this->assertNativeObjectOperatorOperandSupported($expr->expr, $expr, '-', true);
$type = $this->detectTypeOfExpr($expr->expr);
$this->assertExprCanBeUsedAsValue($expr->expr, 'unary operand');
if ($type === Type::BIGFLOAT) {
@ -124,6 +126,7 @@ trait UnaryExpressionTrait
if ($pythonOperator !== null) {
return $pythonOperator;
}
$this->assertNativeObjectOperatorOperandSupported($expr->expr, $expr, '+', true);
$this->assertExprCanBeUsedAsValue($expr->expr, 'unary operand');
return $this->parseExprAsValue($expr->expr);
}

@ -663,6 +663,18 @@ class Preprocessor extends CompilerBase
$this->parseParams($v->params, $functionDef);
$this->assertNativeObjectFunctionSignature($v, $functionDef);
if ($functionDef->generator
&& ($this->classDef?->nativeObject || $this->functionUsesNativeObject($functionDef))
) {
// FiberGenerator lowers the body to a Zend Closure and captures all
// parameters through php::Args. Native pointers deliberately have
// no zval representation, and a Native method's `this` cannot be
// bound to that Closure either.
$this->fatalError(
$v,
'Generator functions cannot accept, capture, or return Native objects',
);
}
if ($this->classDef !== null
&& !$this->classDef->nativeObject

@ -1188,8 +1188,8 @@ CODE;
$code .= <<<CODE
PHP_RSHUTDOWN_FUNCTION({$moduleName}) {
php_app_clean();
php::request_shutdown();
php_app_clean();
return SUCCESS;
}

@ -6,6 +6,15 @@ Native class: object pointers use PHP object truthiness in conditions
#[Native]
class NativeConditionValue {}
#[Native]
class NativeConditionWithConversion
{
public function toBool(): bool
{
return false;
}
}
function maybeNative(bool $present): ?NativeConditionValue
{
return $present ? new NativeConditionValue() : null;
@ -23,6 +32,12 @@ function main(): void
if ($present) {
echo "present\n";
}
$converted = new NativeConditionWithConversion();
var_dump((bool) $converted);
if ($converted) {
echo "object truthiness\n";
}
}
?>
--EXPECT--
@ -31,3 +46,5 @@ bool(false)
bool(false)
bool(true)
present
bool(false)
object truthiness

@ -0,0 +1,22 @@
--TEST--
Native class: exit converts through the native string method
--FILE--
<?php
#[Native]
class NativeExitValue
{
public function toString(): string
{
return 'native exit';
}
}
function main(): void
{
exit(new NativeExitValue());
}
?>
--EXPECT--
native exit

@ -0,0 +1,43 @@
--TEST--
Native class: root frames remain valid across non-LIFO Fiber suspension
--FILE--
<?php
#[Native]
class NativeFiberValue
{
public string $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
function suspendedNativeFrame(): void
{
$value = new NativeFiberValue('suspended');
Fiber::suspend();
echo $value->name, "\n";
}
function resumeFromNewerNativeFrame(Fiber $fiber): void
{
$value = new NativeFiberValue('newer');
$fiber->resume();
echo $value->name, "\n";
}
function main(): void
{
$fiber = new Fiber(static function (): void {
suspendedNativeFrame();
});
$fiber->start();
resumeFromNewerNativeFrame($fiber);
}
?>
--EXPECT--
suspended
newer

@ -0,0 +1,40 @@
--TEST--
Native class: request shutdown safely detaches roots owned by suspended Fibers
--FILE--
<?php
#[Native]
class NativeFiberShutdownValue
{
public string $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
function suspendUntilRequestShutdown(): void
{
$value = new NativeFiberShutdownValue('alive');
Fiber::suspend();
// This frame intentionally remains suspended until request shutdown.
echo $value->name, "\n";
}
function main(): void
{
global $suspendedFiber;
$suspendedFiber = new Fiber(static function (): void {
suspendUntilRequestShutdown();
});
$suspendedFiber->start();
echo "suspended\n";
}
?>
--EXPECT--
suspended

@ -0,0 +1,28 @@
--TEST--
Native class: long-running allocation triggers automatic tracing collection
--FILE--
<?php
#[Native]
class NativeGcThresholdValue
{
public function __destruct()
{
global $nativeGcFinalized;
$nativeGcFinalized++;
}
}
function main(): void
{
global $nativeGcFinalized;
$nativeGcFinalized = 0;
for ($i = 0; $i < 300000; $i++) {
$value = new NativeGcThresholdValue();
}
var_dump($nativeGcFinalized > 0);
}
?>
--EXPECT--
bool(true)

@ -0,0 +1,4 @@
<?php
var_dump(isset($nativeValue));

@ -0,0 +1,19 @@
--TEST--
Native class: include scope does not expose raw native pointers to ZendVM
--FILE--
<?php
#[Native]
class NativeIncludeScopeValue {}
function main(): void
{
$nativeValue = new NativeIncludeScopeValue();
include __DIR__ . '/include-native-scope.inc';
var_dump($nativeValue instanceof NativeIncludeScopeValue);
}
?>
--EXPECT--
bool(false)
bool(true)

@ -57,6 +57,9 @@ function main(): void
var_dump((bool) $value);
var_dump((string) $value);
var_dump(strval($value));
print $value;
echo "\n";
echo "interpolated={$value}\n";
$magic = new NativeMagicString();
var_dump($magic->toString());
@ -81,5 +84,7 @@ float(7.5)
bool(true)
string(7) "value=7"
string(7) "value=7"
value=7
interpolated=value=7
string(5) "magic"
string(5) "magic"

@ -0,0 +1,60 @@
--TEST--
Native class: match compares object identity without entering ZendVM
--FILE--
<?php
#[Native]
class NativeMatchValue {}
#[Native]
class NativeMatchUnrelated {}
function nativeMatchIdentity(NativeMatchValue $value): NativeMatchValue
{
echo "subject\n";
return $value;
}
function unrelatedMatchValue(): NativeMatchUnrelated
{
echo "unrelated\n";
return new NativeMatchUnrelated();
}
function scalarMatchValue(): int
{
echo "scalar\n";
return 42;
}
function choose(?NativeMatchValue $subject, NativeMatchValue $same, NativeMatchValue $other): string
{
return match ($subject) {
$other => 'other',
$same => 'same',
null => 'null',
};
}
function main(): void
{
$value = new NativeMatchValue();
$other = new NativeMatchValue();
var_dump(choose($value, $value, $other));
var_dump(choose(null, $value, $other));
var_dump(match (nativeMatchIdentity($value)) {
unrelatedMatchValue() => 'unrelated',
scalarMatchValue() => 'scalar',
$value => 'identity',
});
}
?>
--EXPECT--
string(4) "same"
string(4) "null"
subject
unrelated
scalar
string(8) "identity"

@ -0,0 +1,46 @@
--TEST--
Native class: strict identity never coerces raw pointers to PHP values
--FILE--
<?php
#[Native]
class NativeStrictIdentity {}
function nativeIdentityOperand(NativeStrictIdentity $value): NativeStrictIdentity
{
echo "native\n";
return $value;
}
function zendIdentityOperand(): bool
{
echo "zend\n";
return true;
}
function main(): void
{
$value = new NativeStrictIdentity();
$alias = $value;
$other = new NativeStrictIdentity();
var_dump($value === $alias);
var_dump($value === $other);
var_dump($value === true);
var_dump($value !== true);
var_dump(nativeIdentityOperand($value) === zendIdentityOperand());
var_dump(zendIdentityOperand() === nativeIdentityOperand($value));
}
?>
--EXPECT--
bool(true)
bool(false)
bool(false)
bool(true)
native
zend
bool(false)
zend
native
bool(false)

@ -7,6 +7,7 @@ Native class: ternary, match and coalesce preserve native pointer types
class NativeSelectedValue
{
public int $value;
public ?NativeSelectedValue $child;
public function __construct(int $value)
{
@ -27,6 +28,17 @@ function selectWithCoalesce(?NativeSelectedValue $value): NativeSelectedValue
return $value ?? new NativeSelectedValue(30);
}
function makeSelectedValue(): NativeSelectedValue
{
echo "made\n";
return new NativeSelectedValue(70);
}
function identitySelectedValue(NativeSelectedValue $value): NativeSelectedValue
{
return $value;
}
function main(): void
{
$first = true ? new NativeSelectedValue(1) : new NativeSelectedValue(2);
@ -34,6 +46,18 @@ function main(): void
var_dump(selectWithMatch(1)->value, selectWithMatch(2)->value);
var_dump(selectWithCoalesce(null)->value);
var_dump(selectWithCoalesce($first)->value);
$created ??= new NativeSelectedValue(40);
$created ??= new NativeSelectedValue(41);
var_dump($created->value);
$holder = new NativeSelectedValue(50);
$holder->child ??= new NativeSelectedValue(60);
var_dump($holder->child->value);
$existing = new NativeSelectedValue(80);
$existing ??= identitySelectedValue(makeSelectedValue());
var_dump($existing->value);
}
?>
--EXPECT--
@ -42,3 +66,6 @@ int(10)
int(20)
int(30)
int(1)
int(40)
int(60)
int(80)

Loading…
Cancel
Save