Native Class 支持

master
韩天峰 2 weeks ago
parent af0b6f9582
commit 981b8f54d7
  1. 1232
      docs/NATIVE_CLASS_OBJECT.md
  2. 15
      phpunit/code/native-class-closure-capture.php
  3. 14
      phpunit/code/native-class-closure-parameter.php
  4. 14
      phpunit/code/native-class-closure-return.php
  5. 10
      phpunit/code/native-class-dynamic-magic-method.php
  6. 10
      phpunit/code/native-class-dynamic-static-magic-method.php
  7. 13
      phpunit/code/native-class-instanceof.php
  8. 25
      phpunit/code/native-class-interface-argument.php
  9. 22
      phpunit/code/native-class-interface-assignment.php
  10. 27
      phpunit/code/native-class-interface-property.php
  11. 20
      phpunit/code/native-class-interface-return.php
  12. 6
      phpunit/code/native-class-internal-interface-missing-method.php
  13. 23
      phpunit/code/native-class-internal-interface-parameter.php
  14. 10
      phpunit/code/native-class-internal-interface-visibility.php
  15. 16
      phpunit/code/native-class-json-encode.php
  16. 13
      phpunit/code/native-class-keyword-missing.php
  17. 16
      phpunit/code/native-class-keyword-return-type.php
  18. 11
      phpunit/code/native-class-mixed-return.php
  19. 12
      phpunit/code/native-class-php-array.php
  20. 13
      phpunit/code/native-class-php-property.php
  21. 7
      phpunit/code/native-class-readonly-property.php
  22. 9
      phpunit/code/native-class-static-method.php
  23. 7
      phpunit/code/native-class-static-property.php
  24. 7
      phpunit/code/native-class-std-container-property.php
  25. 15
      phpunit/code/native-class-trait-dynamic-magic-method.php
  26. 15
      phpunit/code/native-class-trait-static-method.php
  27. 11
      phpunit/code/native-class-union-signature.php
  28. 7
      phpunit/code/native-class-untyped-property.php
  29. 11
      phpunit/code/native-class-untyped-return.php
  30. 14
      phpunit/code/native-class-zend-constructor.php
  31. 6
      phpunit/code/native-class-zend-inheritance.php
  32. 12
      phpunit/code/native-class-zend-native-property.php
  33. 3
      phpunit/src/CompileTimeAttributeRegistryTest.php
  34. 3
      phpunit/src/CompilerBaseApiTest.php
  35. 1
      phpunit/src/Entity/ClassDefTest.php
  36. 38
      phpunit/src/NativeClass/NativeClassAttributeLoweringTest.php
  37. 246
      phpunit/src/NativeClass/NativeClassValidationTest.php
  38. 213
      src/CompilerBase.php
  39. 21
      src/Context/CompilationStateTrait.php
  40. 12
      src/Context/FunctionContext.php
  41. 2
      src/Entity/ClassDef.php
  42. 4
      src/Entity/FunctionDef.php
  43. 7
      src/Generator/CallArgumentGenerator.php
  44. 26
      src/Generator/ClosureGenerator.php
  45. 8
      src/Generator/PropertyPromotion.php
  46. 866
      src/NativeClass/NativeClassSupportTrait.php
  47. 38
      src/Optimizer/FuncCallOptimizer.php
  48. 4
      src/Parser/ArrayExpressionTrait.php
  49. 122
      src/Parser/AssignOpTrait.php
  50. 34
      src/Parser/BinaryOpTrait.php
  51. 12
      src/Parser/FunctionCallTrait.php
  52. 88
      src/Parser/MethodCallTrait.php
  53. 47
      src/Parser/PropertyAccessTrait.php
  54. 98
      src/Parser/SelectionExpressionTrait.php
  55. 15
      src/Parser/TypeConversionTrait.php
  56. 5
      src/Parser/UnaryExpressionTrait.php
  57. 85
      src/Preprocessor.php
  58. 1
      src/Transform/CompileTimeAttributeRegistry.php
  59. 39
      src/Transform/NativeClassAttributeLowering.php
  60. 1
      src/Transform/Visitor.php
  61. 134
      src/Translator.php
  62. 45
      src/TypeSystem/NativeTypeCompatibilityTrait.php
  63. 16
      src/gen_stub.php
  64. 5
      src/polyfills.php
  65. 41
      tests/compiler/native-class/basic.phpt
  66. 30
      tests/compiler/native-class/by-reference.phpt
  67. 36
      tests/compiler/native-class/call-argument-roots.phpt
  68. 22
      tests/compiler/native-class/chained-assignment.phpt
  69. 32
      tests/compiler/native-class/chained-call.phpt
  70. 29
      tests/compiler/native-class/clone-and-zend-invisible.phpt
  71. 71
      tests/compiler/native-class/composite-property-types.phpt
  72. 25
      tests/compiler/native-class/declaration-order.phpt
  73. 34
      tests/compiler/native-class/destructor-inheritance.phpt
  74. 37
      tests/compiler/native-class/gc-cycle.phpt
  75. 33
      tests/compiler/native-class/generators.phpt
  76. 42
      tests/compiler/native-class/global-and-static.phpt
  77. 38
      tests/compiler/native-class/inherited-property-slot.phpt
  78. 41
      tests/compiler/native-class/instanceof.phpt
  79. 24
      tests/compiler/native-class/internal-interface.phpt
  80. 74
      tests/compiler/native-class/keyword-conversions.phpt
  81. 35
      tests/compiler/native-class/magic-methods.phpt
  82. 28
      tests/compiler/native-class/non-null-parameter.phpt
  83. 50
      tests/compiler/native-class/nullable-signatures.phpt
  84. 43
      tests/compiler/native-class/phpx-properties.phpt
  85. 51
      tests/compiler/native-class/private-property-slots.phpt
  86. 32
      tests/compiler/native-class/property-hooks.phpt
  87. 73
      tests/compiler/native-class/trait-inheritance-interface.phpt
  88. 28
      tests/compiler/native-class/unset-alias.phpt
  89. 44
      tests/compiler/native-class/value-selection.phpt
  90. 30
      tests/compiler/native-class/zend-hidden-method.phpt
  91. 43
      tests/compiler/native-class/zero-values.phpt

File diff suppressed because it is too large Load Diff

@ -0,0 +1,15 @@
<?php
#[Native]
class NativeClosureCapture
{
public int $value = 1;
}
function makeNativeCapturingClosure(): Closure
{
$native = new NativeClosureCapture();
return static function () use ($native): int {
return 1;
};
}

@ -0,0 +1,14 @@
<?php
#[Native]
class NativeClosureParameter
{
public int $value = 1;
}
function makeNativeParameterClosure(): Closure
{
return static function (NativeClosureParameter $native): int {
return $native->value;
};
}

@ -0,0 +1,14 @@
<?php
#[Native]
class NativeClosureReturn
{
public int $value = 1;
}
function makeNativeReturningClosure(): Closure
{
return static function () {
return new NativeClosureReturn();
};
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeDynamicMagicMethod
{
public function __get(string $name): mixed
{
return null;
}
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeDynamicStaticMagicMethod
{
public static function __callStatic(string $name, array $arguments): mixed
{
return null;
}
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativeInstanceofTarget
{
public int $value = 0;
}
function testNativeInstanceof(string $class): bool
{
$value = new NativeInstanceofTarget();
return $value instanceof $class;
}

@ -0,0 +1,25 @@
<?php
interface NativeInterfaceArgumentContract
{
public function value(): int;
}
#[Native]
class NativeInterfaceArgumentValue implements NativeInterfaceArgumentContract
{
public function value(): int
{
return 1;
}
}
function consumeNativeInterfaceArgument(NativeInterfaceArgumentContract $value): int
{
return $value->value();
}
function main(): void
{
consumeNativeInterfaceArgument(new NativeInterfaceArgumentValue());
}

@ -0,0 +1,22 @@
<?php
interface NativeInterfaceAssignmentContract
{
public function value(): int;
}
#[Native]
class NativeInterfaceAssignmentValue implements NativeInterfaceAssignmentContract
{
public function value(): int
{
return 1;
}
}
function replaceNativeInterfaceAssignment(
NativeInterfaceAssignmentContract $target,
NativeInterfaceAssignmentValue $value,
): void {
$target = $value;
}

@ -0,0 +1,27 @@
<?php
interface NativeInterfacePropertyContract
{
public function value(): int;
}
#[Native]
class NativeInterfacePropertyValue implements NativeInterfacePropertyContract
{
public function value(): int
{
return 1;
}
}
#[Native]
class NativeInterfacePropertyHolder
{
public NativeInterfacePropertyContract $value;
}
function main(): void
{
$holder = new NativeInterfacePropertyHolder();
$holder->value = new NativeInterfacePropertyValue();
}

@ -0,0 +1,20 @@
<?php
interface NativeInterfaceReturnContract
{
public function value(): int;
}
#[Native]
class NativeInterfaceReturnValue implements NativeInterfaceReturnContract
{
public function value(): int
{
return 1;
}
}
function makeNativeInterfaceReturn(): NativeInterfaceReturnContract
{
return new NativeInterfaceReturnValue();
}

@ -0,0 +1,6 @@
<?php
#[Native]
class NativeMissingCountable implements Countable
{
}

@ -0,0 +1,23 @@
<?php
#[Native]
class NativeNarrowArrayAccess implements ArrayAccess
{
public function offsetExists(string $offset): bool
{
return false;
}
public function offsetGet(mixed $offset): mixed
{
return null;
}
public function offsetSet(mixed $offset, mixed $value): void
{
}
public function offsetUnset(mixed $offset): void
{
}
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeProtectedCountable implements Countable
{
protected function count(): int
{
return 1;
}
}

@ -0,0 +1,16 @@
<?php
#[Native]
class NativeJsonValue
{
public function toArray(): array
{
return [];
}
}
function main(): void
{
$value = new NativeJsonValue();
json_encode($value);
}

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

@ -0,0 +1,16 @@
<?php
#[Native]
class InvalidNativeConversion
{
public function toArray(): string
{
return '';
}
}
function main(): void
{
$value = new InvalidNativeConversion();
$value->toArray();
}

@ -0,0 +1,11 @@
<?php
#[Native]
class NativeMixedReturn {}
function makeNativeMixed(): mixed
{
return new NativeMixedReturn();
}
function main(): void {}

@ -0,0 +1,12 @@
<?php
#[Native]
class NativeArrayElement
{
public int $value = 1;
}
function storeNativeInArray(): array
{
return [new NativeArrayElement()];
}

@ -0,0 +1,13 @@
<?php
#[Native]
class NativePropertyValue
{
public int $value = 1;
}
function storeNativeInPhpProperty(): void
{
$phpObject = new stdClass();
$phpObject->value = new NativePropertyValue();
}

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

@ -0,0 +1,9 @@
<?php
#[Native]
class NativeWithStaticMethod
{
public static function create(): void
{
}
}

@ -0,0 +1,7 @@
<?php
#[Native]
class NativeStaticProperty
{
public static int $value = 1;
}

@ -0,0 +1,7 @@
<?php
#[Native]
class NativeWithStdContainer
{
public std\vector $values;
}

@ -0,0 +1,15 @@
<?php
trait NativeDynamicMagicTrait
{
public function __serialize(): array
{
return [];
}
}
#[Native]
class NativeTraitDynamicMagicMethod
{
use NativeDynamicMagicTrait;
}

@ -0,0 +1,15 @@
<?php
trait NativeStaticMethodTrait
{
public static function value(): int
{
return 1;
}
}
#[Native]
class NativeTraitStaticMethod
{
use NativeStaticMethodTrait;
}

@ -0,0 +1,11 @@
<?php
#[Native]
class NativeUnionMember
{
public int $value = 1;
}
function invalidNativeUnion(NativeUnionMember|string $value): void
{
}

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

@ -0,0 +1,11 @@
<?php
#[Native]
class NativeUntypedReturn {}
function makeNativeUntyped()
{
return new NativeUntypedReturn();
}
function main(): void {}

@ -0,0 +1,14 @@
<?php
#[Native]
class NativeConstructorArgument
{
public int $value = 1;
}
class ZendBackedConstructor
{
public function __construct(NativeConstructorArgument $native)
{
}
}

@ -0,0 +1,6 @@
<?php
class ZendBackedParent {}
#[Native]
class NativeInvalidChild extends ZendBackedParent {}

@ -0,0 +1,12 @@
<?php
#[Native]
class NativePropertyType
{
public int $value = 1;
}
class ZendObjectWithNativeProperty
{
public NativePropertyType $value;
}

@ -8,7 +8,7 @@ final class CompileTimeAttributeRegistryTest extends TestCase
public function testEveryBuiltInCompileTimeAttributeHasCompleteMetadata(): void
{
$expected = [
'MethodsFor', 'NoExport', 'WasmExport', 'Getter', 'Setter', 'With', 'Printer', 'Arrayable',
'Native', 'MethodsFor', 'NoExport', 'WasmExport', 'Getter', 'Setter', 'With', 'Printer', 'Arrayable',
'NotNull', 'NotEmpty', 'Validate', 'Override', 'MustUse', 'Hot', 'Cold', 'Constructor',
];
$this->assertSame($expected, CompileTimeAttributeRegistry::names());
@ -22,6 +22,7 @@ final class CompileTimeAttributeRegistryTest extends TestCase
}
$this->assertNotContains('NoExport', CompileTimeAttributeRegistry::names(true));
$this->assertNotContains('WasmExport', CompileTimeAttributeRegistry::names(true));
$this->assertNotContains('Native', CompileTimeAttributeRegistry::names(true));
$this->assertContains('Getter', CompileTimeAttributeRegistry::names(true));
$this->assertContains('Override', CompileTimeAttributeRegistry::names(true));
$this->assertSame(

@ -1141,8 +1141,9 @@ YAML);
file_get_contents($this->compiler->getArgInfoHeaderFile($file)),
);
foreach (\TypePhp\Transform\CompileTimeAttributeRegistry::names() as $attribute) {
$needle = $attribute === 'Native' ? '#[Native' : $attribute;
$this->assertStringNotContainsString(
$attribute,
$needle,
file_get_contents($this->compiler->getArgInfoHeaderFile($file)),
);
}

@ -26,6 +26,7 @@ class ClassDefTest extends TestCase
$this->assertFalse($class->enum);
$this->assertFalse($class->requireCtor);
$this->assertFalse($class->inheritedFromInternalClass);
$this->assertFalse($class->nativeObject);
$this->assertNull($class->trait);
}

@ -0,0 +1,38 @@
<?php
namespace TypePhp\Tests\NativeClass;
use PhpParser\Node\Stmt\Class_;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\ParserFactory;
use PHPUnit\Framework\TestCase;
use TypePhp\Transform\NativeClassAttributeLowering;
final class NativeClassAttributeLoweringTest extends TestCase
{
public function testConsumesNativeAttributeAndMarksNamedClass(): void
{
$parser = (new ParserFactory())->createForNewestSupportedVersion();
$nodes = $parser->parse('<?php #[Native] class Point {}');
$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
$traverser->addVisitor(new NativeClassAttributeLowering());
$nodes = $traverser->traverse($nodes);
$this->assertInstanceOf(Class_::class, $nodes[0]);
$this->assertTrue(NativeClassAttributeLowering::isNative($nodes[0]));
$this->assertSame([], $nodes[0]->attrGroups);
}
public function testLeavesOrdinaryClassUnmarked(): void
{
$parser = (new ParserFactory())->createForNewestSupportedVersion();
$nodes = $parser->parse('<?php class Point {}');
$traverser = new NodeTraverser();
$traverser->addVisitor(new NativeClassAttributeLowering());
$nodes = $traverser->traverse($nodes);
$this->assertFalse(NativeClassAttributeLowering::isNative($nodes[0]));
}
}

@ -0,0 +1,246 @@
<?php
namespace TypePhp\Tests\NativeClass;
use TypePhp\Exception\TestError;
final class NativeClassValidationTest extends \BaseTest
{
public function testRejectsUntypedProperty(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native class properties must declare a type');
$this->compile('native-class-untyped-property.php');
}
public function testRejectsStaticProperty(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native class static properties are not supported');
$this->compile('native-class-static-property.php');
}
public function testRejectsInheritanceAcrossObjectModels(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native and ZendVM-backed classes cannot inherit from each other');
$this->compile('native-class-zend-inheritance.php');
}
public function testRejectsStaticMethod(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native class static methods are not supported');
$this->compile('native-class-static-method.php');
}
public function testRejectsReadonlyPropertyUntilNativeWriteStateIsImplemented(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native class readonly properties are not supported');
$this->compile('native-class-readonly-property.php');
}
public function testRejectsStdContainerProperty(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native class properties cannot use Std Container types');
$this->compile('native-class-std-container-property.php');
}
public function testRejectsDynamicInstanceofBecauseNativeClassesHaveNoRuntimeTypeLookup(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Dynamic instanceof is not supported for native objects');
$this->compile('native-class-instanceof.php');
}
public function testRejectsNativeObjectStoredInPhpArray(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be stored in PHP arrays');
$this->compile('native-class-php-array.php');
}
public function testRejectsNativeObjectStoredInPhpObjectProperty(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be stored in PHP arrays, PHP object properties');
$this->compile('native-class-php-property.php');
}
public function testRejectsNativeTypedPropertyOnZendObject(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native object types can only be used as properties of native classes');
$this->compile('native-class-zend-native-property.php');
}
public function testRejectsNativeObjectCapturedByClosure(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be captured by Zend closures');
$this->compile('native-class-closure-capture.php');
}
public function testRejectsNativeObjectClosureParameter(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Zend closures cannot declare native object parameters or return types');
$this->compile('native-class-closure-parameter.php');
}
public function testRejectsNativeObjectReturnedByUntypedClosure(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Zend closures cannot return native objects');
$this->compile('native-class-closure-return.php');
}
public function testRejectsNativeObjectParameterOnZendConstructor(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Zend-backed constructors cannot accept or return native objects');
$this->compile('native-class-zend-constructor.php');
}
public function testRejectsUnsupportedNativeObjectUnion(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native object types cannot be combined with other union or intersection members');
$this->compile('native-class-union-signature.php');
}
public function testRejectsIncorrectNativeKeywordReturnType(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('must return exactly `array`');
$this->compile('native-class-keyword-return-type.php');
}
public function testRejectsMissingNativeKeywordMethod(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('must define `toInt()`');
$this->compile('native-class-keyword-missing.php');
}
public function testRejectsNativeObjectPassedToJsonEncode(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot cross a dynamic PHP/ZendVM call boundary');
$this->compile('native-class-json-encode.php');
}
public function testRejectsNativeObjectFromUntypedReturn(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native object return values require an explicit native class return type');
$this->compile('native-class-untyped-return.php');
}
public function testRejectsNativeObjectFromMixedReturn(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native object return values require an explicit native class return type');
$this->compile('native-class-mixed-return.php');
}
public function testRejectsNativeObjectPassedToInterfaceParameter(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be converted to interface `NativeInterfaceArgumentContract`');
$this->compile('native-class-interface-argument.php');
}
public function testRejectsNativeObjectAssignedToInterfaceVariable(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be assigned to interface-typed variables');
$this->compile('native-class-interface-assignment.php');
}
public function testRejectsNativeObjectAssignedToInterfaceProperty(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be assigned to interface-typed properties');
$this->compile('native-class-interface-property.php');
}
public function testRejectsNativeObjectReturnedAsInterface(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native objects cannot be returned as interface `NativeInterfaceReturnContract`');
$this->compile('native-class-interface-return.php');
}
public function testRejectsDynamicMagicMethod(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native classes do not support dynamic magic method `__get()`');
$this->compile('native-class-dynamic-magic-method.php');
}
public function testRejectsDynamicStaticMagicMethodBeforeGenericStaticDiagnostic(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native classes do not support dynamic magic method `__callStatic()`');
$this->compile('native-class-dynamic-static-magic-method.php');
}
public function testRejectsDynamicMagicMethodInjectedByTrait(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native classes do not support dynamic magic method `__serialize()`');
$this->compile('native-class-trait-dynamic-magic-method.php');
}
public function testDynamicMagicMethodDenyListIsComplete(): void
{
$trait = new \ReflectionClass(\TypePhp\NativeClass\NativeClassSupportTrait::class);
$constant = $trait->getReflectionConstant('UNSUPPORTED_NATIVE_MAGIC_METHODS');
$this->assertNotFalse($constant);
$this->assertSame([
'__call',
'__callstatic',
'__get',
'__set',
'__isset',
'__unset',
'__sleep',
'__wakeup',
'__serialize',
'__unserialize',
'__set_state',
'__debuginfo',
], array_keys($constant->getValue()));
}
public function testRejectsStaticMethodInjectedByTrait(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Native class static methods are not supported');
$this->compile('native-class-trait-static-method.php');
}
public function testRejectsMissingInternalInterfaceMethod(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('must implement method `Countable::count()`');
$this->compile('native-class-internal-interface-missing-method.php');
}
public function testRejectsNonPublicInternalInterfaceMethod(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('must be compatible with `Countable::count()`');
$this->compile('native-class-internal-interface-visibility.php');
}
public function testRejectsNarrowedInternalInterfaceParameter(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('must be compatible with `ArrayAccess::offsetExists()`');
$this->compile('native-class-internal-interface-parameter.php');
}
}

@ -82,6 +82,7 @@ use TypePhp\Resolver\Reflection;
use TypePhp\Symbol\SymbolRepository;
use TypePhp\TypeSystem\CompositeTypeCheckerTrait;
use TypePhp\TypeSystem\NativeTypeCompatibilityTrait;
use TypePhp\NativeClass\NativeClassSupportTrait;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\Node\ArrayItem;
@ -102,6 +103,7 @@ class CompilerBase implements PropertyAccessContext
use CompilerDiagnosticTrait;
use CompilationStateTrait;
use NativeTypeCompatibilityTrait;
use NativeClassSupportTrait;
use NativeBuildConfigurationTrait;
use PythonModuleTrait;
use DeclarationSymbolTrait;
@ -433,6 +435,10 @@ class CompilerBase implements PropertyAccessContext
'GLOBALS' => Type::ARRAY,
];
protected array $globalVars = [];
/** @var array<string, string> Global/static Native pointer slot => class name. */
protected array $nativeGlobalObjects = [];
/** @var array<string, true> Request-reset initialization flags for Native static locals. */
protected array $nativeStaticInitializers = [];
protected bool $nativeTypes = false;
protected bool $decimalTypes = false;
protected bool $bigintTypes = false;
@ -1845,9 +1851,7 @@ class CompilerBase implements PropertyAccessContext
{
$lines = [];
foreach ($v->exprs as $expr) {
$type = $this->detectTypeOfExpr($expr);
$parsed = $this->convertExprToStringByType($this->parseExprAsValue($expr), $type);
$lines[] = 'php::echo(' . $parsed . ');';
$lines[] = 'php::echo(' . $this->parseExprToString($expr) . ');';
}
return implode("\n" . $this->getIndent(), $lines);
@ -1875,9 +1879,27 @@ class CompilerBase implements PropertyAccessContext
protected function detectClassOfExpr(NodeAbstract $expr): string
{
if ($expr instanceof Expr\Clone_) {
return $this->detectClassOfExpr($expr->expr);
}
if ($expr instanceof Expr\Closure || $expr instanceof Expr\ArrowFunction) {
return 'Closure';
}
if ($expr instanceof Expr\Ternary) {
return $this->getCommonNativeObjectExpressionClass([
$expr->if ?? $expr->cond,
$expr->else,
]);
}
if ($expr instanceof Expr\Match_) {
return $this->getCommonNativeObjectExpressionClass(array_map(
static fn (Node\MatchArm $arm): Expr => $arm->body,
$expr->arms,
));
}
if ($expr instanceof Expr\BinaryOp\Coalesce) {
return $this->getCommonNativeObjectExpressionClass([$expr->left, $expr->right]);
}
if ($expr instanceof Expr\MethodCall && $this->isNamedMethod($expr->name)) {
$keywordType = $this->findKeywordMethod($this->parseIdentifier($expr->name));
if ($keywordType !== null && $keywordType !== Type::OBJECT) {
@ -1914,6 +1936,18 @@ class CompilerBase implements PropertyAccessContext
return $this->getObjectType($object);
}
}
if ($expr instanceof Expr\PropertyFetch && $this->isIdExpr($expr->name)) {
$receiverClass = $this->detectClassOfExpr($expr->var);
if ($this->isNativeObjectClass($receiverClass)) {
$property = $this->findNativeObjectProperty($receiverClass, $expr->name->toString());
if ($property !== null
&& $property->type === Type::OBJECT
&& $this->isNativeObjectClass($property->class)
) {
return $property->class;
}
}
}
if ($this->isArrayDimFetch($expr) and $this->isStdContainerExpr($expr)) {
if ($this->isStdArrayExpr($expr)) {
if (!$expr->hasAttribute('stdArrayDimFetch')) {
@ -2256,6 +2290,25 @@ class CompilerBase implements PropertyAccessContext
}
// 实际函数的返回值
$type = $this->detectTypeOfExpr($v->expr);
$nativeExpressionClass = $this->detectClassOfExpr($v->expr);
if ($this->context->inClosure && $this->isNativeObjectClass($nativeExpressionClass)) {
$this->fatalError($v, 'Zend closures cannot return native objects');
}
if (!$this->context->inClosure && $this->isNativeObjectClass($nativeExpressionClass)) {
$declaredReturnClass = $this->getReturnClass();
if (!$this->isNativeObjectClass($declaredReturnClass)) {
if ($declaredReturnClass !== '' && $this->isInterface($declaredReturnClass)) {
$this->fatalError(
$v,
"Native objects cannot be returned as interface `{$declaredReturnClass}`",
);
}
$this->fatalError(
$v,
'Native object return values require an explicit native class return type',
);
}
}
if ($this->isCurrentConstructor() && !$this->context->inClosure) {
$this->fatalError($v, 'Method `' . $this->getCurrentMethodDisplayName() . '()` cannot return a value');
}
@ -2276,6 +2329,27 @@ class CompilerBase implements PropertyAccessContext
'return value'
);
}
if (!$this->context->inClosure
&& ($nativeReturnClass = $this->getReturnClass()) !== ''
&& $this->isNativeObjectClass($nativeReturnClass)
) {
if ($this->isNull($v->expr)) {
if (!$this->functionDef->returnNullable) {
$this->fatalError($v, "The return type is non-nullable native object `{$nativeReturnClass}`");
}
return 'return nullptr;';
}
$objectClass = $this->detectClassOfExpr($v->expr);
if ($objectClass === '' || !$this->isNativeObjectClass($objectClass)
|| !$this->isObjectClassStaticallyAssignableTo($objectClass, $nativeReturnClass)
) {
$this->fatalError(
$v,
"The return type is native object `{$nativeReturnClass}`, `{$objectClass}` given"
);
}
return 'return ' . $this->parseExprAsValue($v->expr) . ';';
}
$expr = $this->parseExprAsValue($v->expr);
$returnType = $this->getReturnType();
@ -3582,6 +3656,27 @@ class CompilerBase implements PropertyAccessContext
. $constructor['className'] . '::__construct()'
);
}
if ($this->isNativeObjectClass($className)) {
$cppClass = $this->getNativeObjectCppName($className);
$descriptor = $this->getNativeObjectDescriptorName($className);
if ($constructor === null) {
if ($expr->args !== []) {
$this->fatalError($expr, "Native class `{$className}` does not have a constructor");
}
return 'php::nativeConstruct<' . $cppClass . '>(' . $descriptor
. ', [&](auto &this_) { ' . $cppClass . '__initialize(this_); })';
}
$nativeCtor = $this->getNativeMethod($expr, $className, '__construct');
if ($nativeCtor === false) {
$this->fatalError($expr, "Native constructor `{$className}::__construct()` cannot be resolved");
}
$args = $expr->args === []
? ''
: ', ' . $this->parseNativeCallArgs($expr->args, $nativeCtor);
return 'php::nativeConstruct<' . $cppClass . '>(' . $descriptor
. ', [&](auto &this_) { ' . $cppClass . '__initialize(this_); '
. self::PREFIX . $nativeCtor . '(this_' . $args . '); })';
}
$cePtr = $this->getClassEntryPtr($className);
}
} else {
@ -3599,12 +3694,61 @@ class CompilerBase implements PropertyAccessContext
protected function parseClone(Expr\Clone_ $expr): string
{
$this->assertExprCanBeUsedAsValue($expr->expr, 'clone operand');
$class = $this->detectClassOfExpr($expr->expr);
if ($this->isNativeObjectClass($class)) {
if (!$this->isVarExpr($expr->expr)) {
$this->fatalError($expr, 'Native object clone currently requires a typed variable');
}
$source = $this->parseIdentifier($expr->expr);
$cpp = $this->getNativeObjectCppName($class);
$descriptor = $this->getNativeObjectDescriptorName($class);
$classDef = $this->getClass($class);
$initializer = '';
if ($classDef->hasMethod('__clone')) {
$clone = self::PREFIX . $this->getNativeName('__clone', $classDef->namespace, $classDef->name);
$initializer = $clone . '(this_); ';
}
return 'php::nativeClone<' . $cpp . '>(' . $descriptor . ', '
. $this->getNativeObjectReceiver($source)
. ', [&](auto &this_) { ' . $initializer . '})';
}
return 'php::clone(' . $this->parseExprAsValue($expr->expr) . ')';
}
protected function parseInstanceof(Expr\Instanceof_ $expr): string
{
$this->assertExprCanBeUsedAsValue($expr->expr, 'instanceof operand');
$valueClass = $this->detectClassOfExpr($expr->expr);
$targetClass = $this->resolveCompileTimeInstanceofClass($expr->class);
$valueIsNative = $this->isNativeObjectClass($valueClass);
$targetIsNative = $targetClass !== null && $this->isNativeObjectClass($targetClass);
$targetIsInterface = $targetClass !== null && $this->isInterface($targetClass);
if ($valueIsNative && $targetClass === null) {
$this->fatalError($expr, 'Dynamic instanceof is not supported for native objects');
}
if ($valueIsNative || $targetIsNative) {
$result = $valueIsNative
&& ($targetIsNative || $targetIsInterface)
&& $this->isObjectClassStaticallyAssignableTo($valueClass, $targetClass);
if (!$result
&& $valueIsNative
&& $targetIsNative
&& $this->isObjectClassStaticallyAssignableTo($targetClass, $valueClass)
) {
$this->fatalError(
$expr,
'Native instanceof cannot be resolved from the static base-class type'
);
}
// Folding must not discard constructor/function side effects from
// a non-variable left operand. Native objects cannot cross into a
// Variant, so sequence the original pointer expression directly.
$value = $this->parseExprAsValue($expr->expr);
return '(static_cast<void>(' . $value . '), ' . ($result ? 'true' : 'false') . ')';
}
if ($this->isNameExpr($expr->class)) {
$value = $this->parseExprAsValue($expr->expr);
$classPtr = $this->resolveInstanceofClassPtr($expr->class);
@ -3619,6 +3763,21 @@ class CompilerBase implements PropertyAccessContext
}
}
protected function resolveCompileTimeInstanceofClass(NodeAbstract $class): ?string
{
if (!$this->isNameExpr($class)) {
return null;
}
$name = $this->parseIdentifier($class);
if ($name === 'self' || $name === 'static') {
return $this->getFullClassName();
}
if ($name === 'parent') {
return $this->classDef?->extends ?? '';
}
return $this->getNamespacedClassName($name);
}
protected function resolveInstanceofClassPtr(NodeAbstract $class): string
{
$className = $this->parseIdentifier($class);
@ -3672,7 +3831,10 @@ class CompilerBase implements PropertyAccessContext
$this->addGlobalVar($name, Type::VAR);
}
if (!$this->hasScopeGlobalVar($name)) {
$this->addScopeGlobalVar($name, Type::VAR);
$this->addScopeGlobalVar($name, $this->globalVars[$name]);
}
if (isset($this->nativeGlobalObjects[$name])) {
$this->addNativeObject($name, $this->nativeGlobalObjects[$name]);
}
}
return '';
@ -3710,11 +3872,24 @@ class CompilerBase implements PropertyAccessContext
$this->assertExprCanBeUsedAsValue($var->default, 'static variable default value');
}
$globalVar = $this->addStaticVar($var->var, $varName, $type);
if ($var->default) {
$class = $this->detectClassOfExpr($var->default);
if ($this->isNativeObjectClass($class)) {
$this->promoteGlobalOrStaticToNativeObject($varName, $class);
}
}
$list[] = Type::VAR . ' &' . $varName . ' = ' . $this->escapeGlobalVar($globalVar) . ';';
$list[] = 'auto &' . $varName . ' = ' . $this->escapeGlobalVar($globalVar) . ';';
if ($var->default) {
$initState = self::STATIC_VAR . $varName . '_initialized';
$initCode = $this->getIndent() . 'static bool ' . $initState . ' = false;';
if (isset($this->nativeGlobalObjects[$globalVar])) {
$flag = $globalVar . '__initialized';
$this->nativeStaticInitializers[$flag] = true;
$initState = $this->escapeGlobalVar($flag);
$initCode = '';
} else {
$initCode = $this->getIndent() . 'static THREAD_LOCAL bool ' . $initState . ' = false;';
}
$initCode .= $this->getIndent() . "if (!{$initState}) { \n";
$this->indentLevel++;
$initCode .= $this->getIndent() . "{$initState} = true;\n";
@ -4450,7 +4625,9 @@ class CompilerBase implements PropertyAccessContext
$code .= Type::VAR . ' ' . $name . ';';
} else {
$code .= $type . ' ' . $name;
if ($type === Type::INT or $type === Type::FLOAT or $type === Type::BOOL) {
if ($this->isNativeObjectVar($name)) {
$code .= ' = nullptr';
} elseif ($type === Type::INT or $type === Type::FLOAT or $type === Type::BOOL) {
$code .= ' = 0';
}
$code .= ';';
@ -4476,6 +4653,23 @@ class CompilerBase implements PropertyAccessContext
. ', this_);' . PHP_EOL;
}
$code .= $this->genLocalVarDecl($this->context->localVars);
if ($this->context->nativeObjects !== []) {
$rootSlots = [];
foreach ($this->context->nativeObjects as $name => $_class) {
if ($name === 'this_') {
$code .= $this->getIndent() . 'auto *_native_this_root = &this_;' . PHP_EOL;
$rootSlots[] = 'reinterpret_cast<void **>(&_native_this_root)';
} elseif ($this->hasLocalVar($name)) {
$rootSlots[] = 'reinterpret_cast<void **>(&' . $name . ')';
}
}
if ($rootSlots !== []) {
$code .= $this->getIndent() . 'php::NativeRootSlot _native_root_slots[] = {'
. implode(', ', $rootSlots) . '};' . PHP_EOL;
$code .= $this->getIndent() . 'php::NativeRootFrame _native_root_frame('
. '_native_root_slots, ' . count($rootSlots) . ');' . PHP_EOL;
}
}
// Native static calls pass a lightweight Object containing the called
// class entry. A wrapper can be shared by all calls to the same class,
// but its initialization must dominate every control-flow branch that
@ -4490,7 +4684,7 @@ class CompilerBase implements PropertyAccessContext
if ($name === 'GLOBALS') {
continue;
}
$code .= $this->getIndent() . Type::VAR . ' &' . $name . ' = ' . $this->escapeGlobalVar($name) . ';' . PHP_EOL;
$code .= $this->getIndent() . 'auto &' . $name . ' = ' . $this->escapeGlobalVar($name) . ';' . PHP_EOL;
}
foreach ($this->context->objectProps as $name => $info) {
if (($info['kind'] ?? 'zval') === 'var') {
@ -4522,6 +4716,9 @@ class CompilerBase implements PropertyAccessContext
if ($this->functionDef->returnType === Type::VOID) {
return '';
}
if ($this->getNativeObjectReturnType($this->functionDef) !== null) {
return $this->getIndent() . 'return nullptr;';
}
if ($this->functionDef->returnTypeCheck && !$this->context->inClosure) {
return $this->genUnionCheckedReturn(self::VALUE_NULL);
}

@ -74,6 +74,23 @@ trait CompilationStateTrait
$this->globalVars[$name] = $type;
}
protected function promoteGlobalOrStaticToNativeObject(string $name, string $class): void
{
$class = ltrim($class, '\\');
if ($this->hasStaticVar($name)) {
$slot = $this->escapeStaticVar($name);
$this->context->staticVars[$name] = $this->getNativeObjectPointerType($class);
} elseif ($this->hasScopeGlobalVar($name)) {
$slot = $name;
$this->context->globalVars[$name] = $this->getNativeObjectPointerType($class);
} else {
return;
}
$this->globalVars[$slot] = $this->getNativeObjectPointerType($class);
$this->nativeGlobalObjects[$slot] = $class;
$this->addNativeObject($name, $class);
}
protected function addScopeGlobalVar(string $name, string $type): void
{
$this->context->globalVars[$name] = $type;
@ -81,6 +98,10 @@ trait CompilationStateTrait
protected function addObject(string $name, string $class): void
{
if ($this->isNativeObjectClass($class)) {
$this->addNativeObject($name, $class);
return;
}
// Interfaces have no concrete method body for native calls. Abstract classes may have concrete methods.
if ($this->isInterface($class)) {
$this->context->declaredObjects[$name] = $class;

@ -29,6 +29,9 @@ class FunctionContext
*/
public array $objects = [];
/** @var array<string, string> Native Object pointer variable => fully-qualified class name. */
public array $nativeObjects = [];
/**
* Declared object constraints that are not used for native-call dispatch.
*
@ -90,6 +93,7 @@ class FunctionContext
$this->staticVars = [];
$this->arguments = [];
$this->objects = [];
$this->nativeObjects = [];
$this->declaredObjects = [];
$this->stdArrays = [];
$this->stdContainers = [];
@ -124,11 +128,17 @@ class FunctionContext
unset($this->scopeLayouts[$this->scopeLevel]);
}
public function resetAnalysisTemporaries(array $localVars, int $tmpVarIndex, array $declaredObjects): void
public function resetAnalysisTemporaries(
array $localVars,
int $tmpVarIndex,
array $declaredObjects,
array $nativeObjects = [],
): void
{
$this->localVars = $localVars;
$this->tmpVarIndex = $tmpVarIndex;
$this->declaredObjects = $declaredObjects;
$this->nativeObjects = $nativeObjects;
$this->beforeStmtLines = [];
$this->afterStmtLines = [];
$this->objectProps = [];

@ -32,6 +32,8 @@ class ClassDef extends ClassLikeDef
public string $extends = '';
public bool $requireCtor = false;
public bool $enum = false;
/** Compile-time-only class using the Native Object layout instead of zend_object. */
public bool $nativeObject = false;
/** Whether this class and its methods are part of the public ABI of a library build. */
public bool $exported = true;
public ?string $methodsForTarget = null;

@ -24,6 +24,8 @@ class FunctionDef
public string $params = '';
public string $namespace;
public bool $method = false;
/** Fully-qualified declaring class for methods; empty for free functions. */
public string $declaringClass = '';
public bool $stub = false;
/** Whether this function is part of the public ABI of a library build. */
public bool $exported = true;
@ -61,6 +63,8 @@ class FunctionDef
* @var string 必须是带有命名空间的完整类名
*/
public string $returnClass = '';
/** Whether a Native object return may be represented by nullptr. */
public bool $returnNullable = false;
/**
* Late-bound return type keyword: 'self', 'static' or 'parent'.

@ -606,6 +606,13 @@ trait CallArgumentGenerator
protected function parseCallArgValue(Node\Arg $arg): string
{
$this->assertExprCanBeUsedAsValue($arg->value, 'function argument');
$class = $this->detectClassOfExpr($arg->value);
if ($class !== '' && $this->isNativeObjectClass($class)) {
$this->fatalError(
$arg,
'Native objects cannot cross a dynamic PHP/ZendVM call boundary'
);
}
// C++17 evaluates php::ArgList{...} elements from left to right, but a
// later argument may emit captured beforeStmtLines while being lowered.
// Those statements are placed before the whole outer call and would

@ -114,6 +114,32 @@ trait ClosureGenerator
private function doGenClosure(Expr\ArrowFunction|Expr\Closure $expr, array $params, array $uses = []): string
{
if ($this->classDef?->nativeObject && !$expr->static) {
$this->fatalError($expr, 'Native objects cannot be bound as $this to Zend closures');
}
foreach ($params as $param) {
if ($this->getNativeObjectClassesFromTypeNode($param->type, self::DECL_TYPE_OF_PARAM) !== []) {
$this->fatalError($param, 'Zend closures cannot declare native object parameters or return types');
}
}
if ($this->getNativeObjectClassesFromTypeNode($expr->returnType, self::DECL_TYPE_OF_RETURN) !== []) {
$this->fatalError($expr, 'Zend closures cannot declare native object parameters or return types');
}
foreach ($uses as $useItem) {
if (!$this->isVarExpr($useItem->var)) {
continue;
}
$name = $this->parseIdentifier($useItem->var);
if ($this->isNativeObjectVar($name)) {
$this->fatalError($useItem, 'Native objects cannot be captured by Zend closures');
}
}
if ($expr instanceof Expr\ArrowFunction
&& $this->isNativeObjectClass($this->detectClassOfExpr($expr->expr))
) {
$this->fatalError($expr->expr, 'Zend closures cannot return native objects');
}
$isGenerator = $this->closureContainsYield($expr);
if ($isGenerator) {
$this->validateGeneratorClosure($expr, $params);

@ -16,6 +16,14 @@ trait PropertyPromotion
{
$code = '';
$propertyName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name);
if ($this->classDef?->nativeObject) {
$value = $this->getNativeObjectArgumentType($argInfo) !== null
? $argInfo->name
: $this->convertExprFromType($argInfo->type, $argInfo->name);
$property = $this->classDef->getProperty($propertyName);
return 'this_.' . $this->getNativeObjectPropertyCppName($property, $this->classDef)
. ' = ' . $value . ';' . PHP_EOL;
}
$code .= 'this_.setProperty(' . $this->genCharPtr($propertyName) . ', ' . $argInfo->name . ')';
$code .= ";\n";
return $code;

@ -0,0 +1,866 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\NativeClass;
use TypePhp\Entity\ArgInfo;
use TypePhp\Entity\ClassDef;
use TypePhp\Entity\FunctionDef;
use TypePhp\Entity\MethodDef;
use TypePhp\Entity\PropertyDef;
use TypePhp\Resolver\Reflection;
use PhpParser\Modifiers;
use TypePhp\Type;
use PhpParser\NodeAbstract;
use PhpParser\Node;
trait NativeClassSupportTrait
{
/**
* Magic methods whose semantics require Zend object handlers, runtime
* method resolution, dynamic properties, or Zend serialization state.
* Native classes deliberately have none of those runtime facilities.
*/
private const UNSUPPORTED_NATIVE_MAGIC_METHODS = [
'__call' => true,
'__callstatic' => true,
'__get' => true,
'__set' => true,
'__isset' => true,
'__unset' => true,
'__sleep' => true,
'__wakeup' => true,
'__serialize' => true,
'__unserialize' => true,
'__set_state' => true,
'__debuginfo' => true,
];
protected function assertNativeMagicMethodSupported(NodeAbstract $node, string $method): void
{
if (!$this->classDef?->nativeObject
|| !isset(self::UNSUPPORTED_NATIVE_MAGIC_METHODS[strtolower($method)])
) {
return;
}
$this->fatalError(
$node,
"Native classes do not support dynamic magic method `{$method}()`",
);
}
/**
* Native classes have no zend_class_entry, so Zend cannot perform the
* normal MINIT-time interface verification for them. Convert an internal
* reflection signature into the compiler's existing MethodDef model and
* run the same compatibility checker used for project interfaces.
*/
protected function checkInternalInterfaceImplementation(
NodeAbstract $node,
ClassDef $classDef,
string $interfaceName,
): void {
$interface = Reflection::getClass($interfaceName);
if ($interface === null) {
$this->fatalError($node, "Internal interface `{$interfaceName}` is not available");
}
foreach ($interface->getMethods() as $method) {
$childMethodDef = $this->findClassMethodDef($classDef, $method->getName(), $classDef->isAbstract());
if ($childMethodDef === null) {
if ($classDef->isAbstract()) {
continue;
}
$this->fatalError(
$node,
"Class `{$classDef->getNamespacedName(false)}` must implement method " .
"`{$interfaceName}::{$method->getName()}()`",
);
}
$this->validateMethodOverrideSignature(
$childMethodDef->node ?? $node,
$method->getName(),
$childMethodDef,
$this->createInternalInterfaceMethodDef($method),
$interfaceName,
);
}
}
private function createInternalInterfaceMethodDef(\ReflectionMethod $method): MethodDef
{
$flags = Modifiers::PUBLIC | Modifiers::ABSTRACT;
if ($method->isStatic()) {
$flags |= Modifiers::STATIC;
}
$methodDef = new MethodDef($flags, $method->getName());
$functionDef = new FunctionDef($method->getName(), Type::VAR, '');
$functionDef->method = true;
$functionDef->declaringClass = $method->getDeclaringClass()->getName();
$functionDef->returnsByRef = $method->returnsReference();
$functionDef->returnTypeUndeclared = $method->getReturnType() === null;
$this->applyReflectedReturnType($functionDef, $method->getReturnType(), $method->getDeclaringClass());
foreach ($method->getParameters() as $parameter) {
$argument = new ArgInfo();
$argument->name = $parameter->getName();
$argument->phpName = $parameter->getName();
$argument->byRef = $parameter->isPassedByReference();
$argument->variadic = $parameter->isVariadic();
$argument->undeclared = $parameter->getType() === null;
$argument->nullable = $parameter->allowsNull();
$this->applyReflectedParameterType($argument, $parameter->getType(), $method->getDeclaringClass());
if ($parameter->isOptional() || $parameter->isVariadic()) {
// Compatibility only needs to distinguish required from
// optional parameters; the concrete default is irrelevant.
$argument->defaultValue = new Node\Expr\ConstFetch(new Node\Name('null'));
}
$functionDef->argInfoList[] = $argument;
}
$functionDef->argCountRequired = $method->getNumberOfRequiredParameters();
$methodDef->functionDef = $functionDef;
return $methodDef;
}
private function applyReflectedReturnType(
FunctionDef $function,
?\ReflectionType $type,
\ReflectionClass $declaringClass,
): void {
if ($type === null) {
$function->returnTypeStr = '';
return;
}
$node = $this->reflectionTypeToNode($type, $declaringClass);
$function->returnTypeStr = $this->typeCheckNodeToString($node);
if ($node instanceof Node\NullableType
|| $node instanceof Node\UnionType
|| $node instanceof Node\IntersectionType
) {
$typeInfo = $this->buildTypeCheckFromNode($node);
$function->returnType = Type::VAR;
$function->returnTypeCheck = $typeInfo['check'];
$function->returnTypeNode = $node;
return;
}
[$function->returnType, $function->returnClass] = $this->resolveReflectedNamedType($node);
}
private function applyReflectedParameterType(
ArgInfo $argument,
?\ReflectionType $type,
\ReflectionClass $declaringClass,
): void {
if ($type === null) {
$argument->type = Type::VAR;
return;
}
$node = $this->reflectionTypeToNode($type, $declaringClass);
$argument->typeStr = $this->typeCheckNodeToString($node);
if ($node instanceof Node\NullableType
|| $node instanceof Node\UnionType
|| $node instanceof Node\IntersectionType
) {
$typeInfo = $this->buildTypeCheckFromNode($node);
$argument->type = Type::VAR;
$argument->typeCheck = $typeInfo['check'];
$argument->typeNode = $node;
return;
}
[$argument->type, $argument->declaredClass] = $this->resolveReflectedNamedType($node);
if ($argument->declaredClass !== '' && !$this->isInterface($argument->declaredClass)) {
$argument->class = $argument->declaredClass;
}
$argument->explicitMixed = strtolower($argument->typeStr) === 'mixed';
}
/** @return array{string, string} */
private function resolveReflectedNamedType(NodeAbstract $node): array
{
$name = $this->parseIdentifier($node);
$lower = strtolower(ltrim($name, '\\'));
if (isset($this->zendTypeMap[$lower])) {
return [$this->getTypeFromZendType($lower), ''];
}
return [Type::OBJECT, ltrim($name, '\\')];
}
private function reflectionTypeToNode(
\ReflectionType $type,
\ReflectionClass $declaringClass,
bool $allowNullableWrapper = true,
): NodeAbstract {
if ($type instanceof \ReflectionUnionType) {
return new Node\UnionType(array_map(
fn (\ReflectionType $member): NodeAbstract =>
$this->reflectionTypeToNode($member, $declaringClass, false),
$type->getTypes(),
));
}
if ($type instanceof \ReflectionIntersectionType) {
return new Node\IntersectionType(array_map(
fn (\ReflectionType $member): NodeAbstract =>
$this->reflectionTypeToNode($member, $declaringClass, false),
$type->getTypes(),
));
}
/** @var \ReflectionNamedType $type */
$name = $type->getName();
$lower = strtolower($name);
if ($lower === 'self') {
$node = new Node\Name\FullyQualified($declaringClass->getName());
} elseif ($lower === 'parent') {
$parent = $declaringClass->getParentClass();
$node = $parent === false
? new Node\Name('parent')
: new Node\Name\FullyQualified($parent->getName());
} elseif ($lower === 'static') {
$node = new Node\Name('static');
} elseif ($type->isBuiltin()) {
$node = new Node\Identifier($name);
} else {
$node = new Node\Name\FullyQualified($name);
}
if ($allowNullableWrapper
&& $type->allowsNull()
&& !in_array($lower, ['mixed', 'null'], true)
) {
return new Node\NullableType($node);
}
return $node;
}
protected function isNativeObjectClass(string $class): bool
{
$class = ltrim($class, '\\');
return $class !== '' && $this->hasClass($class) && $this->getClass($class)->nativeObject;
}
protected function getNativeObjectCppName(string|ClassDef $class): string
{
$classDef = $class instanceof ClassDef ? $class : $this->getClass(ltrim($class, '\\'));
return self::PREFIX . $this->getNativeName('', $classDef->namespace, $classDef->name);
}
protected function getNativeObjectDescriptorName(string|ClassDef $class): string
{
return $this->getNativeObjectCppName($class) . '__type';
}
protected function getNativeObjectPointerType(string|ClassDef $class): string
{
return $this->getNativeObjectCppName($class) . ' *';
}
protected function getNativeObjectArgumentType(ArgInfo $argument): ?string
{
$class = $argument->declaredClass ?: $argument->class;
if ((!$argument->byRef && $argument->type !== Type::OBJECT)
|| !$this->isNativeObjectClass($class)
) {
return null;
}
return $this->getNativeObjectPointerType($class) . ($argument->byRef ? '&' : '');
}
protected function resolveNullableNativeObjectType(?NodeAbstract $type, int $declarationKind): ?array
{
$inner = null;
if ($type instanceof Node\NullableType) {
$inner = $type->type;
} elseif ($type instanceof Node\UnionType && count($type->types) === 2) {
foreach ($type->types as $member) {
if ($member instanceof Node\Identifier && strtolower($member->toString()) === 'null') {
continue;
}
if ($inner !== null) {
return null;
}
$inner = $member;
}
}
if (!$inner instanceof Node\Name) {
return null;
}
[$innerType, $class] = $this->resolveTypeDecl($inner, $declarationKind);
if ($innerType !== Type::OBJECT || !$this->isNativeObjectClass($class)) {
return null;
}
return [Type::OBJECT, $class];
}
/** @return list<string> */
protected function getNativeObjectClassesFromTypeNode(?NodeAbstract $type, int $declarationKind): array
{
if ($type === null || $type instanceof Node\Identifier) {
return [];
}
if ($type instanceof Node\NullableType) {
return $this->getNativeObjectClassesFromTypeNode($type->type, $declarationKind);
}
if ($type instanceof Node\UnionType || $type instanceof Node\IntersectionType) {
$classes = [];
foreach ($type->types as $member) {
foreach ($this->getNativeObjectClassesFromTypeNode($member, $declarationKind) as $class) {
$classes[strtolower($class)] = $class;
}
}
return array_values($classes);
}
if (!$type instanceof Node\Name) {
return [];
}
[$resolvedType, $class] = $this->resolveTypeDecl($type, $declarationKind);
return $resolvedType === Type::OBJECT && $this->isNativeObjectClass($class) ? [$class] : [];
}
/**
* Resolve a common Native pointer type for value-selection branches.
* Null is accepted as the empty state; any Zend/non-object branch makes
* the expression unsuitable for the Native object model.
*
* @param list<NodeAbstract> $expressions
*/
protected function getCommonNativeObjectExpressionClass(array $expressions): string
{
$common = '';
foreach ($expressions as $expression) {
if ($this->isNull($expression)) {
continue;
}
$class = $this->detectClassOfExpr($expression);
if (!$this->isNativeObjectClass($class)) {
return '';
}
if ($common === '') {
$common = $class;
continue;
}
if ($this->isObjectClassStaticallyAssignableTo($class, $common)) {
continue;
}
if ($this->isObjectClassStaticallyAssignableTo($common, $class)) {
$common = $class;
continue;
}
return '';
}
return $common;
}
protected function assertSupportedNativeObjectTypeNode(
?NodeAbstract $type,
int $declarationKind,
NodeAbstract $errorNode,
): void {
if (!$type instanceof Node\UnionType && !$type instanceof Node\IntersectionType) {
return;
}
$nativeClasses = $this->getNativeObjectClassesFromTypeNode($type, $declarationKind);
if ($nativeClasses !== [] && $this->resolveNullableNativeObjectType($type, $declarationKind) === null) {
$this->fatalError(
$errorNode,
'Native object types cannot be combined with other union or intersection members',
);
}
}
protected function getNativeObjectReturnType(FunctionDef $function): ?string
{
if ($function->returnType !== Type::OBJECT || !$this->isNativeObjectClass($function->returnClass)) {
return null;
}
return $this->getNativeObjectPointerType($function->returnClass);
}
protected function getNativeObjectMethodThisType(FunctionDef $function): ?string
{
if (!$function->method || !$this->isNativeObjectClass($function->declaringClass)) {
return null;
}
return $this->getNativeObjectCppName($function->declaringClass) . ' &';
}
protected function functionUsesNativeObject(FunctionDef $function): bool
{
if ($this->getNativeObjectReturnType($function) !== null
|| $this->getNativeObjectMethodThisType($function) !== null
) {
return true;
}
foreach ($function->argInfoList as $argument) {
if ($this->getNativeObjectArgumentType($argument) !== null) {
return true;
}
}
return false;
}
protected function genNativeObjectParameterChecks(FunctionDef $function): string
{
$code = '';
foreach ($function->argInfoList as $argument) {
$class = $argument->declaredClass ?: $argument->class;
if (!$argument->nullable && $this->isNativeObjectClass($class)) {
$code .= $this->getIndent() . 'php::nativeGcRequireObject('
. $argument->name . ', "' . addslashes($class) . '");' . PHP_EOL;
}
}
return $code;
}
protected function addNativeObject(string $name, string $class): void
{
$this->context->nativeObjects[$name] = ltrim($class, '\\');
$this->context->objects[$name] = ltrim($class, '\\');
}
protected function isNativeObjectVar(string $name): bool
{
return isset($this->context->nativeObjects[$name]);
}
protected function getNativeObjectVarClass(string $name): string
{
return $this->context->nativeObjects[$name] ?? '';
}
protected function getNativeObjectReceiver(string $name): string
{
if ($name === 'this_') {
return 'this_';
}
$class = $this->getNativeObjectVarClass($name);
return 'php::nativeDeref(' . $name . ', "' . addslashes($class) . '")';
}
protected function getNativeObjectMemberReceiver(string $name): string
{
return $this->getNativeObjectReceiver($name) . '.';
}
/**
* Materialize a Native-producing expression as a precisely rooted local.
* This is shared by chained method and property access so neither path can
* accidentally pass a Native pointer through php::Variant.
*/
protected function materializeNativeObjectReceiver(NodeAbstract $expr, string $class): string
{
[$receiver, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr);
$this->appendCapturedStmtLinesToContext($beforeStmts);
$object = $this->genTmpVarName();
$this->addLocalVar($object, $this->getNativeObjectPointerType($class));
$this->addNativeObject($object, $class);
$this->context->beforeStmtLines[] = $object . ' = ' . $receiver . ';';
$this->appendCapturedStmtLinesToContext($afterStmts);
// Keep the temporary in the precise root frame while the complete PHP
// statement executes, then release that root at statement end.
$this->context->afterStmtLines[] = $object . ' = nullptr;';
return $object;
}
protected function findNativeObjectProperty(string $class, string $property): ?PropertyDef
{
while ($class !== '' && $this->hasClass($class)) {
$classDef = $this->getClass($class);
if ($classDef->hasProperty($property)) {
return $classDef->getProperty($property);
}
$class = $classDef->extends;
}
return null;
}
protected function findNativeObjectMethod(string $class, string $method): ?MethodDef
{
while ($class !== '' && $this->isNativeObjectClass($class)) {
$classDef = $this->getClass($class);
if ($classDef->hasMethod($method)) {
return $classDef->getMethod($method);
}
$class = $classDef->extends;
}
return null;
}
/**
* Native objects cannot fall back to PHPX/Zend conversion helpers. A
* keyword conversion is therefore a statically checked ordinary Native
* method call. __toString() is accepted as the PHP-compatible spelling of
* toString().
*/
protected function resolveNativeObjectKeywordMethod(
NodeAbstract $node,
string $class,
string $method,
): string {
$expectedType = self::KEYWORD_METHOD_MAP[$method] ?? null;
if ($expectedType === null) {
return $method;
}
$resolvedMethod = $method;
$methodDef = $this->findNativeObjectMethod($class, $resolvedMethod);
if ($method === 'toString' && $methodDef === null) {
$resolvedMethod = '__toString';
$methodDef = $this->findNativeObjectMethod($class, $resolvedMethod);
}
if ($methodDef === null) {
$this->fatalError($node, "Native class `{$class}` must define `{$method}()` for this conversion");
}
$function = $methodDef->functionDef;
if ($function->argInfoList !== []) {
$this->fatalError($node, "Native conversion method `{$class}::{$resolvedMethod}()` must not accept arguments");
}
if ($function->returnsByRef || $function->returnNullable || $function->returnType !== $expectedType) {
$expectedTypeName = match ($expectedType) {
Type::INT => 'int',
Type::FLOAT => 'float',
Type::STR => 'string',
Type::BOOL => 'bool',
Type::ARRAY => 'array',
Type::STREAM => 'Stream',
Type::BIGINT => 'BigInt',
Type::BIGFLOAT => 'BigFloat',
Type::DECIMAL => 'Decimal',
Type::OBJECT => 'object',
Type::VAR => 'mixed',
default => $expectedType,
};
$this->fatalError(
$node,
"Native conversion method `{$class}::{$resolvedMethod}()` must return exactly `{$expectedTypeName}`",
);
}
return $resolvedMethod;
}
protected function getNativeVirtualMethodName(string $method): string
{
return '__typephp_virtual_' . strtolower($method);
}
protected function isNativeVirtualMethod(ClassDef $class, MethodDef $method): bool
{
if ($method->flags & (Modifiers::STATIC | Modifiers::PRIVATE | Modifiers::FINAL | Modifiers::ABSTRACT)) {
return false;
}
if (in_array(strtolower($method->name), ['__construct', '__destruct', '__clone'], true)) {
return false;
}
if ($this->isOverrideMethod($class->getNamespacedName(false) . '::' . $method->name)) {
return true;
}
$parent = $class->extends;
while ($parent !== '' && $this->hasClass($parent)) {
$parentDef = $this->getClass($parent);
if ($parentDef->hasMethod($method->name)) {
return true;
}
$parent = $parentDef->extends;
}
return false;
}
protected function getNativeMethodReturnCppType(FunctionDef $function): string
{
return $function->returnsByRef
? Type::REF
: ($this->getNativeObjectReturnType($function) ?? $function->returnType);
}
protected function getNativeMethodParameterDeclarations(FunctionDef $function): string
{
$args = [];
foreach ($function->argInfoList as $argument) {
if ($argument->variadic) {
$args[] = Type::ARRAY . ' ' . $argument->name;
} else {
$args[] = $this->genArgumentDeclaration($argument);
}
}
return implode(', ', $args);
}
protected function getNativeObjectPropertyType(PropertyDef $property): string
{
if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) {
return $this->getNativeObjectPointerType($property->class);
}
return match ($property->type) {
Type::STREAM, Type::BOX => Type::VAR,
default => $property->type,
};
}
protected function getNativeObjectPropertyCppName(
string|PropertyDef $property,
string|ClassDef|null $declaringClass = null,
): string
{
$name = $property instanceof PropertyDef ? $property->name : $property;
if (!$property instanceof PropertyDef || !$property->isPrivate()) {
return $this->escapeVarName($name);
}
if ($declaringClass === null) {
throw new \LogicException('Native private property field requires its declaring class');
}
return '__private_' . $this->getNativeObjectCppName($declaringClass)
. '__' . $this->escapeVarName($name);
}
protected function isNativeObjectForbiddenPropertyType(PropertyDef $property): bool
{
if (in_array($property->type, [
Type::BOX,
Type::STD_ARRAY,
Type::STD_VECTOR,
Type::STD_MAP,
Type::STD_ORDERED_MAP,
], true)) {
return true;
}
return in_array(strtolower(ltrim($property->class, '\\')), [
'std\\array',
'std\\vector',
'std\\map',
'std\\ordered_map',
], true);
}
protected function isNativeObjectInheritedPropertyRedeclaration(
ClassDef $class,
PropertyDef $property,
): bool {
$parent = $class->extends;
while ($parent !== '' && $this->isNativeObjectClass($parent)) {
$parentDef = $this->getClass($parent);
if ($parentDef->hasProperty($property->name)) {
$parentProperty = $parentDef->getProperty($property->name);
if ($property->isPrivate() || $parentProperty->isPrivate()) {
return false;
}
// Property compatibility is validated separately. TypePHP
// treats a compatible public/protected redeclaration as the
// same inherited slot, so a Native child must not emit a
// second C++ field with the same PHP property name.
return true;
}
$parent = $parentDef->extends;
}
return false;
}
/**
* C++ requires a base struct to be complete before defining a derived
* struct. PHP source order has no such restriction, so emit Native class
* definitions in inheritance order while retaining source order between
* unrelated classes.
*
* @return list<ClassDef>
*/
protected function getNativeObjectClassesInDeclarationOrder(): array
{
$classes = array_values(array_filter(
$this->symbols->classes(),
static fn (ClassDef $class): bool => $class->nativeObject,
));
$byName = [];
foreach ($classes as $class) {
$byName[strtolower(ltrim($class->getNamespacedName(false), '\\'))] = $class;
}
$ordered = [];
$visited = [];
$visit = function (ClassDef $class) use (&$visit, &$ordered, &$visited, $byName): void {
$key = strtolower(ltrim($class->getNamespacedName(false), '\\'));
if (isset($visited[$key])) {
return;
}
$visited[$key] = true;
$parent = strtolower(ltrim($class->extends, '\\'));
if ($parent !== '' && isset($byName[$parent])) {
$visit($byName[$parent]);
}
$ordered[] = $class;
};
foreach ($classes as $class) {
$visit($class);
}
return $ordered;
}
protected function genNativeObjectDeclarations(): string
{
$classes = $this->getNativeObjectClassesInDeclarationOrder();
if ($classes === []) {
return '';
}
$code = '// TypePHP Native Object declarations' . PHP_EOL;
foreach ($classes as $class) {
$code .= 'struct ' . $this->getNativeObjectCppName($class) . ';' . PHP_EOL;
}
$code .= PHP_EOL;
foreach ($classes as $class) {
$name = $this->getNativeObjectCppName($class);
$parent = $class->extends !== '' && $this->isNativeObjectClass($class->extends)
? ' : public ' . $this->getNativeObjectCppName($class->extends)
: '';
$code .= 'struct ' . $name . $parent . ' {' . PHP_EOL;
foreach ($class->properties as $property) {
if ($property->flags & Modifiers::STATIC
|| $this->isNativeObjectInheritedPropertyRedeclaration($class, $property)
) {
continue;
}
$type = $this->getNativeObjectPropertyType($property);
// PHPX value types own their storage and must be initialized by
// their C++ default constructor. PHP-level defaults are applied
// by the generated allocation/constructor path, not in this
// shared declaration header (which cannot reference file-local
// literal tables).
$default = null;
if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) {
$default = 'nullptr';
} elseif (in_array($property->type, [Type::INT, Type::FLOAT, Type::BOOL], true)) {
$default = '0';
}
$code .= ' ' . $type . ' ' . $this->getNativeObjectPropertyCppName($property, $class);
if ($default !== null) {
$code .= ' = ' . $default;
}
$code .= ';' . PHP_EOL;
}
foreach ($class->methods as $method) {
if (!$this->isNativeVirtualMethod($class, $method)) {
continue;
}
$parentHasMethod = $class->extends !== ''
&& $this->hasClass($class->extends)
&& $this->getClass($class->extends)->hasMethod($method->name);
$code .= ' virtual ' . $this->getNativeMethodReturnCppType($method->functionDef)
. ' ' . $this->getNativeVirtualMethodName($method->name) . '('
. $this->getNativeMethodParameterDeclarations($method->functionDef) . ')'
. ($parentHasMethod ? ' override' : '') . ';' . PHP_EOL;
}
$code .= '};' . PHP_EOL;
$code .= 'void ' . $name . '__initialize(' . $name . ' &object);' . PHP_EOL;
$code .= 'void ' . $name . '__gc_trace(void *object, php::NativeMarker &marker);' . PHP_EOL;
$code .= 'extern const php::NativeTypeDescriptor '
. $this->getNativeObjectDescriptorName($class) . ';' . PHP_EOL . PHP_EOL;
}
return $code;
}
protected function genNativeObjectRuntimeDefinition(ClassDef $class): string
{
$cpp = $this->getNativeObjectCppName($class);
$prefix = $cpp . '__gc';
$code = '';
$code .= 'void ' . $cpp . '__initialize(' . $cpp . ' &this_) {' . PHP_EOL;
if ($class->extends !== '' && $this->isNativeObjectClass($class->extends)) {
$code .= ' ' . $this->getNativeObjectCppName($class->extends) . '__initialize(this_);' . PHP_EOL;
}
foreach ($class->properties as $property) {
if ($property->isStatic() || $property->default === null) {
continue;
}
if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) {
$value = 'nullptr';
} else {
$value = $property->default;
}
$code .= ' this_.' . $this->getNativeObjectPropertyCppName($property, $class) . ' = ' . $value . ';' . PHP_EOL;
}
$code .= '}' . PHP_EOL . PHP_EOL;
foreach ($class->methods as $method) {
if (!$this->isNativeVirtualMethod($class, $method)) {
continue;
}
$function = $method->functionDef;
$returnType = $this->getNativeMethodReturnCppType($function);
$args = array_map(static fn (ArgInfo $arg): string => $arg->name, $function->argInfoList);
$nativeFunction = self::PREFIX . $this->getNativeName(
$method->name,
$class->namespace,
$class->name,
);
$code .= $returnType . ' ' . $cpp . '::' . $this->getNativeVirtualMethodName($method->name)
. '(' . $this->getNativeMethodParameterDeclarations($function) . ') {' . PHP_EOL;
$call = $nativeFunction . '(*this' . ($args === [] ? '' : ', ' . implode(', ', $args)) . ')';
$code .= ' ' . ($returnType === Type::VOID ? '' : 'return ') . $call . ';' . PHP_EOL;
$code .= '}' . PHP_EOL . PHP_EOL;
}
$code .= 'void ' . $prefix . '_trace(void *object, php::NativeMarker &marker) {' . PHP_EOL;
$hasParentTrace = $class->extends !== '' && $this->isNativeObjectClass($class->extends);
$nativeProperties = array_filter(
$class->properties,
fn (PropertyDef $property): bool => !$property->isStatic()
&& !$this->isNativeObjectInheritedPropertyRedeclaration($class, $property)
&& $property->type === Type::OBJECT
&& $this->isNativeObjectClass($property->class),
);
if ($hasParentTrace || $nativeProperties !== []) {
$code .= ' auto &this_ = *static_cast<' . $cpp . ' *>(object);' . PHP_EOL;
} else {
$code .= ' (void) object;' . PHP_EOL;
$code .= ' (void) marker;' . PHP_EOL;
}
if ($hasParentTrace) {
$code .= ' ' . $this->getNativeObjectCppName($class->extends)
. '__gc_trace(static_cast<' . $this->getNativeObjectCppName($class->extends) . ' *>(&this_), marker);'
. PHP_EOL;
}
foreach ($nativeProperties as $property) {
$code .= ' marker.mark(this_.' . $this->getNativeObjectPropertyCppName($property, $class) . ');' . PHP_EOL;
}
$code .= '}' . PHP_EOL;
$destructors = [];
$destructorClass = $class;
while (true) {
if ($destructorClass->hasMethod('__destruct')) {
$destructors[] = [
self::PREFIX . $this->getNativeName(
'__destruct',
$destructorClass->namespace,
$destructorClass->name,
),
$this->getNativeObjectCppName($destructorClass),
];
}
if ($destructorClass->extends === '' || !$this->isNativeObjectClass($destructorClass->extends)) {
break;
}
$destructorClass = $this->getClass($destructorClass->extends);
}
if ($destructors !== []) {
$code .= 'static void ' . $prefix . '_finalize(void *object) {' . PHP_EOL;
foreach ($destructors as [$destructor, $destructorCpp]) {
$code .= ' ' . $destructor . '(*static_cast<' . $destructorCpp . ' *>(object));' . PHP_EOL;
}
$code .= '}' . PHP_EOL;
}
$code .= 'static void ' . $prefix . '_destroy(void *object) noexcept {' . PHP_EOL;
$code .= ' static_cast<' . $cpp . ' *>(object)->~' . $cpp . '();' . PHP_EOL;
$code .= '}' . PHP_EOL;
$code .= 'const php::NativeTypeDescriptor ' . $this->getNativeObjectDescriptorName($class) . ' = {' . PHP_EOL;
$code .= ' "' . addslashes($class->getNamespacedName(false)) . '",' . PHP_EOL;
$code .= ' sizeof(' . $cpp . '),' . PHP_EOL;
$code .= ' alignof(' . $cpp . '),' . PHP_EOL;
$code .= ' ' . $prefix . '_trace,' . PHP_EOL;
$code .= ' ' . ($destructors !== [] ? $prefix . '_finalize' : 'nullptr') . ',' . PHP_EOL;
$code .= ' ' . $prefix . '_destroy,' . PHP_EOL;
$code .= '};' . PHP_EOL . PHP_EOL;
return $code;
}
}

@ -211,6 +211,23 @@ trait FuncCallOptimizer
return false;
}
// Optimized php::fn::* calls must obey the same ZendVM escape boundary
// as the generic call generator. The four scalar conversions are
// language-level Native keyword aliases and are lowered to an exact
// Native method; every other PHP function rejects Native pointers.
if (!isset($config['conversion'])) {
foreach ($expr->args as $arg) {
if ($arg instanceof Node\Arg
&& $this->isNativeObjectClass($this->detectClassOfExpr($arg->value))
) {
$this->fatalError(
$arg,
'Native objects cannot cross a dynamic PHP/ZendVM call boundary',
);
}
}
}
// 检测参数中使用的变量是否已定义,若变量不存在则回退到动态调用路径
// 动态路径中的 parseCallArgs() 会给出明确的错误信息
foreach ($expr->args as $arg) {
@ -472,6 +489,22 @@ trait FuncCallOptimizer
{
$arg = $expr->args[0]->value;
$type = $this->detectTypeOfExpr($arg);
$nativeClass = $this->detectClassOfExpr($arg);
if ($this->isNativeObjectClass($nativeClass)) {
$method = match ($convType) {
self::ARG_TYPE_STR => 'toString',
self::ARG_TYPE_INT => 'toInt',
self::ARG_TYPE_FLOAT => 'toFloat',
self::ARG_TYPE_BOOL => 'toBool',
default => null,
};
if ($method !== null) {
return $this->parseMethodCall(new Node\Expr\MethodCall(
$arg,
new Node\Identifier($method),
));
}
}
$parsed = $this->parseExpr($arg);
if ($convType === self::ARG_TYPE_STR) {
@ -589,7 +622,10 @@ trait FuncCallOptimizer
protected function doFoldKnownClass(Node\Expr\FuncCall $expr): string|false
{
$cn = $expr->args[0]->value;
return ($this->isScalarString($cn) && $this->hasClass($cn->value)) ? 'true' : false;
if (!$this->isScalarString($cn) || !$this->hasClass($cn->value)) {
return false;
}
return $this->isNativeObjectClass($cn->value) ? 'false' : 'true';
}
protected function doFoldKnownConstant(Node\Expr\FuncCall $expr): string|false

@ -31,6 +31,10 @@ trait ArrayExpressionTrait
$hasNextInsert = false;
$hasReference = false;
foreach ($items as $item) {
$valueClass = $this->detectClassOfExpr($item->value);
if ($this->isNativeObjectClass($valueClass)) {
$this->fatalError($item->value, 'Native objects cannot be stored in PHP arrays');
}
if ($item->unpack) {
$hasUnpack = true;
}

@ -95,7 +95,13 @@ trait AssignOpTrait
$next = $next->expr;
}
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, Type::VAR);
$nativeClass = $this->detectClassOfExpr($next);
if ($this->isNativeObjectClass($nativeClass)) {
$this->addLocalVar($tmpVar, $this->getNativeObjectPointerType($nativeClass));
$this->addNativeObject($tmpVar, $nativeClass);
} else {
$this->addLocalVar($tmpVar, Type::VAR);
}
// 翻转赋值链
$chain = array_reverse($chain);
@ -208,10 +214,64 @@ trait AssignOpTrait
protected function parseAssignFinally(Expr $left, Expr $right): string
{
$this->assertNotNullsafeWriteContext($left);
$rightClass = $this->detectClassOfExpr($right);
if ($left instanceof Expr\List_) {
if ($this->isNativeObjectClass($rightClass)) {
$this->fatalError($right, 'Native objects cannot be destructured into PHP values');
}
return $this->parseAssignToList($left, $right);
}
if ($this->isNativeObjectClass($rightClass)) {
$allowed = false;
if ($this->isVarExpr($left)) {
$leftName = $this->parseVariable($left);
$declaredClass = $this->getDeclaredObjectType($leftName);
if ($declaredClass !== '' && $this->isInterface($declaredClass)) {
$this->fatalError(
$left,
'Native objects cannot be assigned to interface-typed variables',
);
}
$allowed = !$this->hasVar($leftName)
|| $this->isNativeObjectVar($leftName)
|| $this->hasScopeGlobalVar($leftName)
|| $this->hasStaticVar($leftName);
if ($allowed && ($this->hasScopeGlobalVar($leftName) || $this->hasStaticVar($leftName))) {
$this->promoteGlobalOrStaticToNativeObject($leftName, $rightClass);
}
} elseif ($left instanceof Expr\PropertyFetch
&& $this->isVarExpr($left->var)
&& $this->isIdExpr($left->name)
) {
$receiver = $this->parseVariable($left->var);
if ($this->isNativeObjectVar($receiver)) {
$property = $this->findNativeObjectProperty(
$this->getNativeObjectVarClass($receiver),
$left->name->toString(),
);
if ($property !== null
&& $property->class !== ''
&& $this->isInterface($property->class)
) {
$this->fatalError(
$left,
'Native objects cannot be assigned to interface-typed properties',
);
}
$allowed = $property !== null
&& $property->type === Type::OBJECT
&& $this->isNativeObjectClass($property->class);
}
}
if (!$allowed) {
$this->fatalError(
$left,
'Native objects cannot be stored in PHP arrays, PHP object properties, static properties, or mixed variables',
);
}
}
// A direct assignment to readonly must go through write_property so
// Zend can enforce scope, initialization state, type and clone rules.
$propertyWriteTarget = $this->preparePropertyWriteTarget($left, true);
@ -230,6 +290,45 @@ trait AssignOpTrait
$this->fatalError($left, 'Cannot write to read-only hooked property');
}
if ($left instanceof Expr\PropertyFetch
&& $this->isVarExpr($left->var)
&& $this->isIdExpr($left->name)
) {
$object = $this->parseIdentifier($left->var);
if ($this->isNativeObjectVar($object)) {
$property = $this->parseIdentifier($left->name);
$class = $this->getNativeObjectVarClass($object);
$access = $this->getNativePropertyAccess($left);
if ($access === null) {
$this->fatalError($left, "Native class `{$class}` has no property `\${$property}`");
}
$def = $access->getPropertyDef();
$field = $this->getNativeObjectPropertyCppName($def, $access->getClassDef());
$rightExpr = $this->parseExprAsValue($right);
if ($def->type === Type::OBJECT && $this->isNativeObjectClass($def->class)) {
if ($this->isNull($right)) {
if (!$def->nullable) {
$this->fatalError($right, "Cannot assign null to native property `{$class}::\${$property}`");
}
return $this->getNativeObjectMemberReceiver($object)
. $field . ' = nullptr';
}
$rightClass = $this->detectClassOfExpr($right);
if ($rightClass === '' || !$this->isObjectClassStaticallyAssignableTo($rightClass, $def->class)) {
$this->fatalError($right, "Cannot assign value to native property `{$class}::\${$property}`");
}
} else {
$this->assertCanAssignPropertyWrite($propertyWriteTarget, $right);
$rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr);
if ($def->type !== Type::VAR) {
$rightExpr = $this->convertExprFromType($def->type, $rightExpr);
}
}
return $this->getNativeObjectMemberReceiver($object)
. $field . ' = ' . $rightExpr;
}
}
if ($propertyWriteTarget !== null && $this->shouldUseDynamicNativePropertyWrite($left, $type)) {
return $this->parseAssignPropertyFetch($left, $right, $propertyWriteTarget);
}
@ -250,11 +349,28 @@ trait AssignOpTrait
}
// 类型推断,获取对象的类名,如果不是对象则返回空字符串
$rightClass = $this->detectClassOfExpr($right);
if ($this->isNativeObjectVar($var)) {
$leftClass = $this->getNativeObjectVarClass($var);
if ($this->isNull($right)) {
return $var . ' = nullptr';
}
if ($rightClass === '' || !$this->isNativeObjectClass($rightClass)) {
$this->fatalError($right, "Native object `\${$var}` cannot be converted to var/object");
}
if (!$this->isObjectClassStaticallyAssignableTo($rightClass, $leftClass)) {
$this->fatalError($right, "Cannot assign native object `{$rightClass}` to `{$leftClass}`");
}
}
// 右值是一个对象,已获得类的名称,左值必须与右值的类一致
if ($rightClass) {
if (!$this->hasVar($var)) {
$this->addLocalVar($var, Type::OBJECT);
$this->addObject($var, $rightClass);
if ($this->isNativeObjectClass($rightClass)) {
$this->addLocalVar($var, $this->getNativeObjectPointerType($rightClass));
$this->addNativeObject($var, $rightClass);
} else {
$this->addLocalVar($var, Type::OBJECT);
$this->addObject($var, $rightClass);
}
} elseif (($leftClass = $this->getDeclaredObjectType($var)) !== '') {
if ($this->isObjectClassStaticallyAssignableTo($rightClass, $leftClass)) {
// A child object can be assigned to a parent typed object.

@ -531,11 +531,21 @@ trait BinaryOpTrait
[$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr);
$this->appendCapturedStmtLinesToContext($beforeStmts);
$type = $this->getOrderedOperandTmpType($expr, (string) $value);
$tmpVar = $this->addTmpVar($type);
$nativeClass = $this->detectClassOfExpr($expr);
if ($this->isNativeObjectClass($nativeClass)) {
$type = $this->getNativeObjectPointerType($nativeClass);
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, $type);
$this->addNativeObject($tmpVar, $nativeClass);
} else {
$type = $this->getOrderedOperandTmpType($expr, (string) $value);
$tmpVar = $this->addTmpVar($type);
}
$this->context->beforeStmtLines[] = $tmpVar . ' = ' . $value . ';';
$this->appendCapturedStmtLinesToContext($afterStmts);
if (in_array($type, [Type::VAR, Type::STR, Type::ARRAY, Type::OBJECT], true)) {
if ($this->isNativeObjectClass($nativeClass)) {
$this->context->afterStmtLines[] = $tmpVar . ' = nullptr;';
} elseif (in_array($type, [Type::VAR, Type::STR, Type::ARRAY, Type::OBJECT], true)) {
// The declaration is function-scoped, but PHP releases an owned
// expression temporary after the statement that consumes it.
// All zval-owning PHPX wrappers must be cleared here: an Object is
@ -632,6 +642,13 @@ trait BinaryOpTrait
continue;
}
$itemClass = $this->detectClassOfExpr($item);
if ($this->isNativeObjectClass($itemClass)) {
$toString = new Expr\MethodCall($item, new Node\Identifier('toString'));
$argList[] = $this->parseOrderedOperand($toString, false);
continue;
}
$type = $this->detectTypeOfExpr($item);
// C++17 evaluates the braced-list elements in order. The temporary
// is still required because lowering a later operand may append
@ -782,6 +799,17 @@ trait BinaryOpTrait
}
$left = $this->parseCompareExpr($expr->left);
$right = $this->parseCompareExpr($expr->right);
$leftIsNative = $this->isNativeObjectClass($this->detectClassOfExpr($expr->left));
$rightIsNative = $this->isNativeObjectClass($this->detectClassOfExpr($expr->right));
if ($leftIsNative && $this->isNull($expr->right)) {
return '(' . $left . ') == nullptr';
}
if ($rightIsNative && $this->isNull($expr->left)) {
return '(' . $right . ') == nullptr';
}
if ($leftIsNative && $rightIsNative) {
return 'static_cast<const void *>(' . $left . ') == static_cast<const void *>(' . $right . ')';
}
if ($right === 'nullptr') {
// The left operand may itself be an assignment or another compound
// expression. Parenthesize it before invoking Variant::isNull(), or

@ -80,6 +80,18 @@ trait FunctionCallTrait
return $pythonObjectCall;
}
$callableClass = $this->detectClassOfExpr($expr->name);
if ($this->isNativeObjectClass($callableClass)) {
if ($expr->isFirstClassCallable()) {
$this->fatalError($expr, 'Native object callables cannot be converted to Zend closures');
}
return $this->parseMethodCall(new Expr\MethodCall(
$expr->name,
new Node\Identifier('__invoke'),
$expr->args,
));
}
if ($this->isVarExpr($expr->name)) {
$fn = $this->parseIdentifier($expr->name);
$placeHolder = $fn;

@ -287,6 +287,24 @@ trait MethodCallTrait
$this->fatalError($expr, 'Cannot call parent method because class `' . $this->classDef->name . '` does not extend any class');
}
$parentClass = $this->classDef->extends;
if ($this->classDef->nativeObject) {
if (!$this->isIdExpr($expr->name)) {
$this->fatalError($expr, 'Dynamic parent method calls are not supported for native objects');
}
$method = $this->parseIdentifier($expr->name);
if (strtolower($method) === '__destruct') {
$this->fatalError($expr, 'Explicit calls to native object destructors are not supported');
}
$nativeFunc = $this->getNativeMethod($expr, $parentClass, $method);
if ($nativeFunc === false) {
$this->fatalError($expr, "Native parent class `{$parentClass}` has no method `{$method}()`");
}
if ($expr->args === []) {
return self::PREFIX . $nativeFunc . '(this_)';
}
return self::PREFIX . $nativeFunc . '(this_, '
. $this->parseNativeCallArgs($expr->args, $nativeFunc) . ')';
}
$staticCall = false;
if ($this->isIdExpr($expr->name)) {
$method = $this->parseIdentifier($expr->name);
@ -348,12 +366,20 @@ trait MethodCallTrait
}
$class = '';
$materializedNativeReceiver = false;
// C++17 sequences a member-call receiver before its arguments, but
// lowering an argument may hoist captured beforeStmtLines ahead of the
// whole call. Materialize an effectful receiver before parsing args.
$object = empty($expr->args)
? $this->parseIdentifier($expr->var)
: $this->parseOrderedOperand($expr->var, false);
$receiverClass = !$this->isVarExpr($expr->var) ? $this->detectClassOfExpr($expr->var) : '';
if ($this->isNativeObjectClass($receiverClass)) {
$object = $this->materializeNativeObjectReceiver($expr->var, $receiverClass);
$class = $receiverClass;
$materializedNativeReceiver = true;
} else {
$object = empty($expr->args)
? $this->parseIdentifier($expr->var)
: $this->parseOrderedOperand($expr->var, false);
}
if ($this->isVarExpr($expr->var)) {
if (!$this->hasVar($object)) {
$this->errorUndefinedVariable($expr->var);
@ -379,6 +405,27 @@ trait MethodCallTrait
);
}
if ($class !== '' && $this->isNativeObjectClass($class)) {
if (!$this->isNamedMethod($expr->name)) {
$this->fatalError($expr, 'Dynamic native object method calls are not supported');
}
$nativeMethodName = strtolower($expr->name->toString());
if ($nativeMethodName === '__destruct') {
$this->fatalError($expr, 'Explicit calls to native object destructors are not supported');
}
$nativeKeyword = $expr->name->toString();
if (isset(self::KEYWORD_METHOD_MAP[$nativeKeyword])) {
if (!isset(self::KEYWORD_METHOD_WITH_ARGUMENTS[$nativeKeyword]) && $expr->args !== []) {
$this->fatalError($expr, "The {$nativeKeyword} method does not accept parameters");
}
$resolvedKeyword = $this->resolveNativeObjectKeywordMethod($expr, $class, $nativeKeyword);
if ($resolvedKeyword !== $nativeKeyword) {
$expr->name = new Node\Identifier($resolvedKeyword, $expr->name->getAttributes());
}
$expr->setAttribute('nativeKeywordCall', true);
}
}
$magicMethod = false;
$method = $this->identifierToStr($expr->name, literal: true);
@ -395,7 +442,10 @@ trait MethodCallTrait
$receiverType = Type::VAR;
}
$keywordType = $this->findKeywordMethod($methodName);
if ($keywordType !== null && isset(self::KEYWORD_METHOD_MAP[$methodName])) {
if ($keywordType !== null
&& isset(self::KEYWORD_METHOD_MAP[$methodName])
&& !$expr->getAttribute('nativeKeywordCall', false)
) {
if (!isset(self::KEYWORD_METHOD_WITH_ARGUMENTS[$methodName]) && $expr->args !== []) {
$this->fatalError($expr, "The {$methodName} method does not accept parameters");
}
@ -448,8 +498,36 @@ trait MethodCallTrait
}
// 可转为原生调用的 MethodCall
if ($this->isVarExpr($expr->var) and $this->isNamedMethod($expr->name)) {
if (($this->isVarExpr($expr->var) || $materializedNativeReceiver) and $this->isNamedMethod($expr->name)) {
$type = $this->getVarType($object);
if ($class !== '' && $this->isNativeObjectClass($class)) {
$methodName = $expr->name->toString();
// Native objects have their own C++ virtual thunk for an
// overridden family; do not let the Zend-object devirtualizer
// downgrade this call to the dynamic path.
$nativeFunc = $this->getNativeMethod($expr, $class, $methodName);
if ($nativeFunc === false) {
$this->fatalError($expr, "Native class `{$class}` has no method `{$methodName}()`");
}
$expr->setAttribute('nativeCall', $nativeFunc);
$nativeFunctionDef = $this->getFunction($nativeFunc);
$declaringClass = $this->getClass($nativeFunctionDef->declaringClass);
$declaringMethod = $declaringClass->getMethod($methodName);
if ($this->isNativeVirtualMethod($declaringClass, $declaringMethod)) {
$call = $this->getNativeObjectMemberReceiver($object)
. $this->getNativeVirtualMethodName($methodName);
if ($expr->args === []) {
return $call . '()';
}
return $call . '(' . $this->parseNativeCallArgs($expr->args, $nativeFunc) . ')';
}
$receiver = $this->getNativeObjectReceiver($object);
if ($expr->args === []) {
return self::PREFIX . $nativeFunc . '(' . $receiver . ')';
}
return self::PREFIX . $nativeFunc . '(' . $receiver . ', '
. $this->parseNativeCallArgs($expr->args, $nativeFunc) . ')';
}
// 引用参数允许方法调用:有class信息走原生调用,无class信息走动态调用
if (!$this->checkArgType($type, Type::OBJECT) and $type !== Type::REF) {
$methodName = $expr->name->toString();

@ -780,6 +780,12 @@ trait PropertyAccessTrait
$lines[] = $array . '.offsetUnset(' . $dim . ');';
}
} elseif ($this->isPropertyFetch($var)) {
if ($this->isVarExpr($var->var)) {
$nativeObject = $this->parseIdentifier($var->var);
if ($this->isNativeObjectVar($nativeObject)) {
$this->fatalError($var, 'Native object properties cannot be unset');
}
}
// unset has its own unconditional readonly diagnostic below;
// it is forbidden even while __construct is running.
$propertyWriteTarget = $this->preparePropertyWriteTarget($var, true);
@ -828,7 +834,9 @@ trait PropertyAccessTrait
$this->errorUndefinedVariable($var);
}
$type = $this->getVarType($name);
if ($this->isNativeType($type)) {
if ($this->isNativeObjectVar($name)) {
$lines[] = "{$name} = nullptr;";
} elseif ($this->isNativeType($type)) {
$this->warning($var, "Variable of native type `\${$name}` cannot be unset");
} elseif ($type === Type::OBJECT) {
// A PHP local read after unset() evaluates to null (and may
@ -925,17 +933,52 @@ trait PropertyAccessTrait
$object = $expr->var;
$property = $expr->name;
$id = $this->getPropertyIdentifier($expr, $object, $property);
$nativeExpressionClass = !$this->isVarExpr($object) ? $this->detectClassOfExpr($object) : '';
if ($this->isNativeObjectClass($nativeExpressionClass)) {
if (!$property instanceof Node\Identifier) {
$this->fatalError($expr, 'Dynamic native object property access is not supported');
}
$propertyName = $property->toString();
$resolution = $this->resolveNativeInstanceProperty($expr, $propertyName, $nativeExpressionClass);
if ($resolution === null) {
$this->fatalError(
$expr,
"Native class `{$nativeExpressionClass}` has no property `\${$propertyName}`"
);
}
$id = $this->applyNativePropertyAccessResult($expr, $resolution);
} else {
$id = $this->getPropertyIdentifier($expr, $object, $property);
}
$hook = $this->getPropertyHookGetter($expr);
if ($hook !== null) {
return $this->emitPropertyHookGetterCall($expr, $hook);
}
if ($this->isNativeObjectClass($nativeExpressionClass)) {
$objectName = $this->materializeNativeObjectReceiver($object, $nativeExpressionClass);
return $this->getNativeObjectMemberReceiver($objectName)
. $this->getNativeObjectPropertyCppName($resolution->propertyDef, $resolution->classDef);
}
$update = $this->isPropertyFetchUpdate($expr);
$objectName = $update ? $this->parseWritableIdentifier($object) : $this->parseIdentifier($object);
if ($this->isVarExpr($object) and !$this->hasVar($objectName)) {
$this->errorUndefinedVariable($object);
}
if ($this->isVarExpr($object) && $this->isNativeObjectVar($objectName)) {
if (!$property instanceof Node\Identifier) {
$this->fatalError($expr, 'Dynamic native object property access is not supported');
}
$propertyName = $property->toString();
$class = $this->getNativeObjectVarClass($objectName);
$resolution = $this->resolveNativeInstanceProperty($expr, $propertyName, $class);
if ($resolution === null) {
$this->fatalError($expr, "Native class `{$class}` has no property `\${$propertyName}`");
}
return $this->getNativeObjectMemberReceiver($objectName)
. $this->getNativeObjectPropertyCppName($resolution->propertyDef, $resolution->classDef);
}
$objectVar = $objectName;
if ($this->usesTraitPropertyScope($objectVar)) {
$getProperty = 'typephp_read_property_scoped('

@ -24,7 +24,9 @@ trait SelectionExpressionTrait
$this->assertExprCanBeUsedAsValue($expr->else, 'ternary branch');
$ifType = $this->detectTypeOfExpr($expr->if);
$elseType = $this->detectTypeOfExpr($expr->else);
$typeChanged = $ifType !== $elseType;
$nativeClass = $this->detectClassOfExpr($expr);
$nativeSelection = $this->isNativeObjectClass($nativeClass);
$typeChanged = !$nativeSelection && $ifType !== $elseType;
[$cond, $condBeforeStmts, $condAfterStmts] = $this->parseExprWithCapturedStmts($expr->cond);
$ifBeforeStmtCount = count($this->context->beforeStmtLines);
$ifAfterStmtCount = count($this->context->afterStmtLines);
@ -42,6 +44,15 @@ trait SelectionExpressionTrait
$this->context->beforeStmtLines = array_slice($this->context->beforeStmtLines, 0, $elseBeforeStmtCount);
$this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $elseAfterStmtCount);
if ($nativeSelection) {
if ($this->isNull($expr->if)) {
$if = 'nullptr';
}
if ($this->isNull($expr->else)) {
$else = 'nullptr';
}
}
$hasBranchStmts = $condBeforeStmts || $condAfterStmts || $ifBeforeStmts || $ifAfterStmts || $elseBeforeStmts || $elseAfterStmts;
if (!$hasBranchStmts && $typeChanged) {
$if = 'php::Var(' . $if . ')';
@ -50,7 +61,9 @@ trait SelectionExpressionTrait
if ($hasBranchStmts) {
// REF and VOID are expression implementation types, not valid
// by-value result types for the materializing lambda.
$ternaryType = $this->getNormalAssignType($typeChanged ? Type::VAR : $ifType);
$ternaryType = $nativeSelection
? $this->getNativeObjectPointerType($nativeClass)
: $this->getNormalAssignType($typeChanged ? Type::VAR : $ifType);
$code = '[&]() -> ' . $ternaryType . '{';
$code .= $this->formatCapturedStmtLines($condBeforeStmts);
if ($condAfterStmts) {
@ -61,9 +74,9 @@ trait SelectionExpressionTrait
}
$cond = $this->convertConditionExpr($expr->cond, $cond);
$code .= $this->getIndent() . 'if (' . $cond . ') {';
$code .= $this->formatTernaryReturn($expr->if, $if, $ifBeforeStmts, $ifAfterStmts, $ternaryType, $ifType);
$code .= $this->formatTernaryReturn($expr->if, $if, $ifBeforeStmts, $ifAfterStmts, $ternaryType, $ifType, $nativeClass);
$code .= $this->getIndent() . '} else {';
$code .= $this->formatTernaryReturn($expr->else, $else, $elseBeforeStmts, $elseAfterStmts, $ternaryType, $elseType);
$code .= $this->formatTernaryReturn($expr->else, $else, $elseBeforeStmts, $elseAfterStmts, $ternaryType, $elseType, $nativeClass);
$code .= $this->getIndent() . '}';
$code .= $this->getIndent() . '}()';
return $code;
@ -79,16 +92,23 @@ trait SelectionExpressionTrait
array $afterStmts,
string $returnType,
string $valueType,
string $nativeClass = '',
): string
{
$code = $this->formatCapturedStmtLines($beforeStmts);
$returnsReference = $valueType === Type::REF
|| ($valueExpr instanceof Expr\CallLike && $this->resolveRefReturningCall($valueExpr) !== false);
if ($returnType !== Type::VAR && ($beforeStmts || $afterStmts)) {
if ($nativeClass === '' && $returnType !== Type::VAR && ($beforeStmts || $afterStmts)) {
$value = $this->convertExprFromType($returnType, $value);
}
if ($afterStmts || $returnsReference) {
$tmpVar = $this->addTmpVar($returnType);
if ($nativeClass !== '') {
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, $returnType);
$this->addNativeObject($tmpVar, $nativeClass);
} else {
$tmpVar = $this->addTmpVar($returnType);
}
$code .= $this->getIndent() . "{$tmpVar} = {$value};";
$code .= $this->formatCapturedStmtLines($afterStmts);
$code .= $this->getIndent() . 'return ' . $tmpVar . ';';
@ -102,6 +122,11 @@ trait SelectionExpressionTrait
protected function parseMatch(Expr\Match_ $expr): string
{
$nativeClass = $this->detectClassOfExpr($expr);
$nativeSelection = $this->isNativeObjectClass($nativeClass);
$returnType = $nativeSelection
? $this->getNativeObjectPointerType($nativeClass)
: Type::VAR;
$this->assertExprCanBeUsedAsValue($expr->cond, 'match condition');
$var = $this->parseIdentifier($expr->cond);
if ($this->isVarExpr($expr->cond)) {
@ -114,7 +139,7 @@ trait SelectionExpressionTrait
$var = $tmpVar;
}
$code = '[&]() -> ' . Type::VAR . '{';
$code = '[&]() -> ' . $returnType . '{';
$default = null;
foreach ($expr->arms as $arm) {
if ($arm->conds === null) {
@ -141,29 +166,40 @@ trait SelectionExpressionTrait
$code .= $this->getIndent() . '}';
}
$code .= $this->getIndent() . 'if (' . $matched . ') {';
$code .= $this->formatMatchReturn($arm->body);
$code .= $this->formatMatchReturn($arm->body, $nativeClass);
$code .= $this->getIndent() . '}';
}
if ($default) {
$code .= $this->getIndent() . '{';
$code .= $this->formatMatchReturn($default);
$code .= $this->formatMatchReturn($default, $nativeClass);
$code .= $this->getIndent() . '}';
} else {
$code .= $this->getIndent() . '{ return php::throwException("UnhandledMatchError", "Unhandled match case"); }';
$code .= $nativeSelection
? $this->getIndent() . '{ php::throwException("UnhandledMatchError", "Unhandled match case"); return nullptr; }'
: $this->getIndent() . '{ return php::throwException("UnhandledMatchError", "Unhandled match case"); }';
}
$code .= '}()';
return $code;
}
protected function formatMatchReturn(NodeAbstract $body): string
protected function formatMatchReturn(NodeAbstract $body, string $nativeClass = ''): string
{
$this->assertExprCanBeUsedAsValue($body, 'match arm');
[$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($body);
if ($nativeClass !== '' && $this->isNull($body)) {
$value = 'nullptr';
}
$code = $this->formatCapturedStmtLines($beforeStmts);
if ($afterStmts) {
$tmpVar = $this->addTmpVar(Type::VAR);
if ($nativeClass !== '') {
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, $this->getNativeObjectPointerType($nativeClass));
$this->addNativeObject($tmpVar, $nativeClass);
} else {
$tmpVar = $this->addTmpVar(Type::VAR);
}
$code .= $this->getIndent() . "{$tmpVar} = {$value};";
$code .= $this->formatCapturedStmtLines($afterStmts);
$code .= $this->getIndent() . 'return ' . $tmpVar . ';';
@ -178,6 +214,10 @@ trait SelectionExpressionTrait
{
$this->assertExprCanBeUsedAsValue($left, 'selection value');
$this->assertExprCanBeUsedAsValue($right, 'selection value');
$nativeClass = $this->detectClassOfExpr($expr);
if ($this->isNativeObjectClass($nativeClass)) {
return $this->parseNativeValueSelection($left, $right, $nativeClass);
}
$leftExpr = $this->parseIdentifier($left);
if ($this->isVarExpr($left)) {
$this->checkVarMustExist($left, $leftExpr);
@ -231,4 +271,38 @@ trait SelectionExpressionTrait
return $tmpVar;
}
protected function parseNativeValueSelection(Expr $left, Expr $right, string $nativeClass): string
{
$pointerType = $this->getNativeObjectPointerType($nativeClass);
[$leftValue, $leftBefore, $leftAfter] = $this->parseExprWithCapturedStmts($left);
[$rightValue, $rightBefore, $rightAfter] = $this->parseExprWithCapturedStmts($right);
if ($this->isNull($left)) {
$leftValue = 'nullptr';
}
if ($this->isNull($right)) {
$rightValue = 'nullptr';
}
$leftTmp = $this->genTmpVarName();
$this->addLocalVar($leftTmp, $pointerType);
$this->addNativeObject($leftTmp, $nativeClass);
$code = '[&]() -> ' . $pointerType . '{';
$code .= $this->formatCapturedStmtLines($leftBefore);
$code .= $this->getIndent() . $leftTmp . ' = ' . $leftValue . ';';
$code .= $this->formatCapturedStmtLines($leftAfter);
$code .= $this->getIndent() . 'if (' . $leftTmp . ' != nullptr) { return ' . $leftTmp . '; }';
$code .= $this->formatCapturedStmtLines($rightBefore);
if ($rightAfter) {
$rightTmp = $this->genTmpVarName();
$this->addLocalVar($rightTmp, $pointerType);
$this->addNativeObject($rightTmp, $nativeClass);
$code .= $this->getIndent() . $rightTmp . ' = ' . $rightValue . ';';
$code .= $this->formatCapturedStmtLines($rightAfter);
$rightValue = $rightTmp;
}
$code .= $this->getIndent() . 'return ' . $rightValue . ';';
return $code . $this->getIndent() . '}()';
}
}

@ -15,6 +15,21 @@ use PhpParser\NodeAbstract;
trait TypeConversionTrait
{
protected function parseExprToString(NodeAbstract $node): string
{
$class = $this->detectClassOfExpr($node);
if ($this->isNativeObjectClass($class)) {
return $this->parseMethodCall(new Node\Expr\MethodCall(
$node,
new Node\Identifier('toString'),
));
}
return $this->convertExprToStringByType(
$this->parseExprAsValue($node),
$this->detectTypeOfExpr($node),
);
}
protected function convertExprToStringByType(string $expr, $type): string
{
if ($type === Type::STR) {

@ -54,10 +54,7 @@ trait UnaryExpressionTrait
protected function parseCastString(Expr\Cast\String_ $node): string
{
$this->assertExprCanBeUsedAsValue($node->expr, 'cast operand');
return $this->convertExprToStringByType(
$this->parseExprAsValue($node->expr),
$this->detectTypeOfExpr($node->expr)
);
return $this->parseExprToString($node->expr);
}
protected function parseCastBool(Expr\Cast\Bool_ $node): string

@ -21,6 +21,7 @@ use TypePhp\Entity\PropertyDef;
use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
use TypePhp\Exception\SyntaxError;
use TypePhp\Transform\PropertyHookLowering;
use TypePhp\Transform\NativeClassAttributeLowering;
use TypePhp\Transform\PrinterLowering;
use TypePhp\Transform\ArrayableLowering;
use TypePhp\Transform\ClassFieldSelection;
@ -77,6 +78,10 @@ class Preprocessor extends CompilerBase
protected function genArgumentDeclaration(ArgInfo $argInfo): string
{
$nativeObjectType = $this->getNativeObjectArgumentType($argInfo);
if ($nativeObjectType !== null) {
return $nativeObjectType . $argInfo->name;
}
$type = $argInfo->type;
if ($type === Type::STREAM || $type === Type::BOX) {
$type = Type::VAR;
@ -392,9 +397,6 @@ class Preprocessor extends CompilerBase
protected function parseParameterType(Node\Param $param, ArgInfo $argInfo, string $var): string
{
if ($param->byRef) {
return Type::REF;
}
// Capture the late-bound parameter type keyword *before* resolveTypeDecl
// runs, because resolveTypeDecl mutates the `self`/`static`/`parent` node
// name to the declaring class when the method belongs to a trait.
@ -406,6 +408,12 @@ class Preprocessor extends CompilerBase
}
}
[$type, $class] = $this->resolveTypeDecl($param->type, self::DECL_TYPE_OF_PARAM);
$this->assertSupportedNativeObjectTypeNode($param->type, self::DECL_TYPE_OF_PARAM, $param);
$nullableNative = $this->resolveNullableNativeObjectType($param->type, self::DECL_TYPE_OF_PARAM);
if ($nullableNative !== null) {
[$type, $class] = $nullableNative;
$argInfo->nullable = true;
}
$argInfo->undeclared = $param->type === null;
if (
$param->type !== null
@ -424,7 +432,10 @@ class Preprocessor extends CompilerBase
// Record late-bound parameter type keywords so they can be re-resolved
// to the consuming class when a trait method is flattened into a class.
$argInfo->typeKeyword = $typeKeyword;
return $type;
// Ordinary PHP references use php::Ref at the native ABI. Keep the
// resolved declaration metadata above, however: Native object
// references need it to lower to `native_struct *&` instead.
return $param->byRef ? Type::REF : $type;
}
/**
@ -499,7 +510,7 @@ class Preprocessor extends CompilerBase
}
if ($param->type instanceof NullableType || $param->type instanceof UnionType || $param->type instanceof IntersectionType) {
$typeInfo = $this->buildTypeCheckFromNode($param->type);
if (!empty($typeInfo['check'])) {
if (!empty($typeInfo['check']) && !$this->isNativeObjectClass($argInfo->declaredClass)) {
$argInfo->typeCheck = $typeInfo['check'];
$argInfo->typeStr = $typeInfo['typeStr'];
$argInfo->typeNode = $param->type;
@ -585,6 +596,14 @@ class Preprocessor extends CompilerBase
}
}
[$returnType, $class] = $this->resolveTypeDecl($v->returnType, self::DECL_TYPE_OF_RETURN);
$this->assertSupportedNativeObjectTypeNode($v->returnType, self::DECL_TYPE_OF_RETURN, $v);
$nullableNativeReturn = $this->resolveNullableNativeObjectType(
$v->returnType,
self::DECL_TYPE_OF_RETURN,
);
if ($nullableNativeReturn !== null) {
[$returnType, $class] = $nullableNativeReturn;
}
// 构造、析构、克隆方法不能有返回值
if ($this->method and in_array($this->method, ['__construct', '__destruct', '__clone'])) {
$returnType = Type::VOID;
@ -607,6 +626,7 @@ class Preprocessor extends CompilerBase
}
$functionDef->exported = !($this->classDef?->exported === false || $this->hasNoExportAttribute($v));
$functionDef->returnClass = $class;
$functionDef->returnNullable = $nullableNativeReturn !== null;
$functionDef->returnTypeStr = $v->returnType === null
? ''
: $this->typeCheckNodeToString($v->returnType);
@ -622,6 +642,7 @@ class Preprocessor extends CompilerBase
}
if (!$functionDef->generator
&& !$this->isNativeObjectClass($functionDef->returnClass)
&& ($v->returnType instanceof NullableType
|| $v->returnType instanceof UnionType
|| $v->returnType instanceof IntersectionType)) {
@ -641,6 +662,14 @@ class Preprocessor extends CompilerBase
$this->parseParams($v->params, $functionDef);
if ($this->classDef !== null
&& !$this->classDef->nativeObject
&& strtolower($fnName) === '__construct'
&& $this->functionUsesNativeObject($functionDef)
) {
$this->fatalError($v, 'Zend-backed constructors cannot accept or return native objects');
}
// main 函数,返回值必须为 void 类型,参数必须为空或者 argc, argv 两个参数
if (!$this->class and !$this->namespace and $fnName === self::ENTRY_FUNCTION) {
if (count($v->params) > 0) {
@ -741,6 +770,9 @@ class Preprocessor extends CompilerBase
$functionDef->sourceFile = $this->file;
$functionDef->startLine = $v->getStartLine();
$functionDef->method = $this->methodDef !== null;
$functionDef->declaringClass = $functionDef->method
? $this->classDef->getNamespacedName(false)
: '';
$functionDef->displayName = $functionDef->method
? $this->classDef->getNamespacedName(false) . '::' . $functionDef->name
: $functionDef->getNamespacedName();
@ -767,6 +799,7 @@ class Preprocessor extends CompilerBase
}
$this->classDef = new ClassDef($this->class, $flags, $this->namespace);
$this->classDef->nativeObject = NativeClassAttributeLowering::isNative($class);
$this->classDef->exported = !$this->hasNoExportAttribute($class);
$this->classDef->methodsForTarget = $this->parseMethodsForTarget($class);
$this->addClass($fullClassName, $this->classDef);
@ -1200,6 +1233,21 @@ class Preprocessor extends CompilerBase
{
$flags = $this->parseModifiers($flags);
[$type, $class] = $this->resolveTypeDecl($typeNode, self::DECL_TYPE_OF_PROPERTY);
$this->assertSupportedNativeObjectTypeNode($typeNode, self::DECL_TYPE_OF_PROPERTY, $errorNode);
$nullableNative = $this->resolveNullableNativeObjectType(
$typeNode,
self::DECL_TYPE_OF_PROPERTY,
);
if ($nullableNative !== null) {
[$type, $class] = $nullableNative;
$nullable = true;
}
if ($this->isNativeObjectClass($class) && !$this->classDef->nativeObject) {
$this->fatalError(
$errorNode,
'Native object types can only be used as properties of native classes',
);
}
$default = null;
$arrayInitPlan = null;
@ -1440,6 +1488,23 @@ class Preprocessor extends CompilerBase
protected function parseClassPropertyDef(Node\Stmt\Property $v): void
{
if ($this->classDef->nativeObject) {
if ($v->type === null) {
$this->fatalError($v, 'Native class properties must declare a type');
}
if ($v->isStatic()) {
$this->fatalError($v, 'Native class static properties are not supported');
}
// A Zend readonly property carries runtime initialization state.
// Native fields deliberately have no Zend property slot, so a raw
// C++ assignment would silently bypass the readonly contract.
// Reject it until Native objects have an equally explicit state
// representation instead of emitting behavior that only appears
// readonly at compile time.
if ($v->isReadonly() || ($this->classDef->flags & Modifiers::READONLY)) {
$this->fatalError($v, 'Native class readonly properties are not supported');
}
}
$oriCtx = $this->context;
$this->context = $this->classDef->propertyContext;
$nullable = $v->type instanceof NullableType;
@ -1447,6 +1512,12 @@ class Preprocessor extends CompilerBase
foreach ($v->props as $prop) {
$propName = $this->parseIdentifier($prop->name);
$propDef = $this->addClassProperty($propName, $v->flags, $v->type, $prop->default, $nullable, $v);
if ($this->classDef->nativeObject && $this->isNativeObjectForbiddenPropertyType($propDef)) {
$message = $propDef->type === Type::BOX
? 'Native class properties cannot use Box types'
: 'Native class properties cannot use Std Container types';
$this->fatalError($v, $message);
}
$hookMetadata = $v->getAttribute(PropertyHookLowering::PROPERTY_ATTRIBUTE, []);
$propDef->virtual = (bool) ($hookMetadata['virtual'] ?? false);
foreach ($v->hooks as $hook) {
@ -1467,8 +1538,12 @@ class Preprocessor extends CompilerBase
$this->resetMethod();
$name = $this->getMethodName($v);
$this->method = $name;
$this->assertNativeMagicMethodSupported($v, $name);
$flags = $this->parseModifiers($v->flags);
$abstract = $flags & Modifiers::ABSTRACT;
if ($this->classDef->nativeObject && ($flags & Modifiers::STATIC)) {
$this->fatalError($v, 'Native class static methods are not supported');
}
if (!$abstract) {
$this->methodDef = new MethodDef($flags, $name);

@ -72,6 +72,7 @@ final class CompileTimeAttributeRegistry
];
};
$add('Native', [self::TARGET_NAMED_CLASS], 'Native can only be applied to named classes', self::ARGUMENTS_NONE, self::PHASE_PREPROCESS, false);
$add('MethodsFor', [self::TARGET_NAMED_CLASS], 'MethodsFor can only be applied to classes', self::ARGUMENTS_METHODS_FOR, self::PHASE_PREPROCESS);
$add('NoExport', [self::TARGET_CLASS_LIKE, self::TARGET_FUNCTION, self::TARGET_METHOD], 'NoExport can only be applied to classes, functions, or methods', self::ARGUMENTS_NONE, self::PHASE_PREPROCESS, false);
$add('WasmExport', [self::TARGET_FUNCTION], 'WasmExport can only be applied to named functions', self::ARGUMENTS_WASM_EXPORT, self::PHASE_PREPROCESS, false);

@ -0,0 +1,39 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\Transform;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
final class NativeClassAttributeLowering extends NodeVisitorAbstract
{
public const string ATTRIBUTE = 'typephpNativeClass';
public function enterNode(Node $node): null
{
self::lower($node);
return null;
}
public static function lower(Node $node): void
{
if (!$node instanceof Node\Stmt\Class_) {
return;
}
if (CompileTimeAttribute::consume($node, 'Native')) {
$node->setAttribute(self::ATTRIBUTE, true);
}
}
public static function isNative(Node $node): bool
{
return $node instanceof Node\Stmt\Class_
&& $node->getAttribute(self::ATTRIBUTE, false) === true;
}
}

@ -28,6 +28,7 @@ class Visitor extends NodeVisitorAbstract
public function enterNode(Node $node): null
{
$this->guard($node, static fn () => CompileTimeAttribute::validateNode($node));
$this->guard($node, static fn () => NativeClassAttributeLowering::lower($node), 'Native');
$this->guard($node, static fn () => FunctionAttributeLowering::lower($node));
$this->guard($node, static fn () => GetterLowering::validateTarget($node), 'Getter');
$this->guard($node, static fn () => PropertyMethodLowering::validateTarget($node));

@ -733,7 +733,13 @@ class Translator extends Preprocessor
}
foreach ($this->globalVars as $name => $type) {
$lines[] = 'extern THREAD_LOCAL ' . Type::VAR . ' ' . $this->escapeGlobalVar($name) . ';';
$cppType = isset($this->nativeGlobalObjects[$name])
? $this->getNativeObjectPointerType($this->nativeGlobalObjects[$name])
: Type::VAR;
$lines[] = 'extern THREAD_LOCAL ' . $cppType . ' ' . $this->escapeGlobalVar($name) . ';';
}
foreach ($this->nativeStaticInitializers as $name => $_) {
$lines[] = 'extern THREAD_LOCAL bool ' . $this->escapeGlobalVar($name) . ';';
}
if ($this->literalStrings) {
@ -811,7 +817,14 @@ class Translator extends Preprocessor
$code .= "// global vars \n";
foreach ($this->globalVars as $name => $type) {
$code .= 'THREAD_LOCAL ' . Type::VAR . ' ' . $this->escapeGlobalVar($name) . ';' . PHP_EOL;
$cppType = isset($this->nativeGlobalObjects[$name])
? $this->getNativeObjectPointerType($this->nativeGlobalObjects[$name])
: Type::VAR;
$code .= 'THREAD_LOCAL ' . $cppType . ' ' . $this->escapeGlobalVar($name)
. (isset($this->nativeGlobalObjects[$name]) ? ' = nullptr' : '') . ';' . PHP_EOL;
}
foreach ($this->nativeStaticInitializers as $name => $_) {
$code .= 'THREAD_LOCAL bool ' . $this->escapeGlobalVar($name) . ' = false;' . PHP_EOL;
}
$code .= "// class register functions \n";
@ -916,6 +929,9 @@ CODE;
$code .= "// class \n";
foreach ($this->getClassLikesWithConstants() as $classDef) {
if ($classDef instanceof ClassDef && $classDef->nativeObject) {
continue;
}
if ($classDef instanceof ClassDef && !$classDef->trait && !$classDef->enum) {
$code .= 'static zend_object* (*create_object_' . $classDef->getNamespacedName() . ")(zend_class_entry *class_type);\n";
$code .= 'static zend_object_handlers property_handlers_' . $classDef->getNamespacedName() . ";\n";
@ -945,6 +961,9 @@ CODE;
if ($functionDef->method) {
continue;
}
if ($this->functionUsesNativeObject($functionDef)) {
continue;
}
$fullName = $functionDef->getNamespacedName();
$zifName = $this->escapeZendFnName($fullName);
// TypePHP is always strict. Store the flag in the registered
@ -1006,6 +1025,11 @@ CODE;
if ($name == 'GLOBALS') {
continue;
}
if (isset($this->nativeGlobalObjects[$name])) {
$code .= 'php::nativeGcRegisterRequestRoot(reinterpret_cast<void **>(&'
. $this->escapeGlobalVar($name) . '));' . PHP_EOL;
continue;
}
$code .= 'php::initGlobal(' . $this->genCharPtr($name) . ', ' . $this->escapeGlobalVar($name) . ');' . PHP_EOL;
}
@ -1053,10 +1077,17 @@ CODE;
$code .= 'void php_app_clean() {' . PHP_EOL;
foreach ($this->globalVars as $name => $type) {
if ($name != 'GLOBALS') {
if (isset($this->nativeGlobalObjects[$name])) {
$code .= $this->escapeGlobalVar($name) . ' = nullptr;' . PHP_EOL;
continue;
}
$code .= $this->escapeGlobalVar($name) . '.unset();' . PHP_EOL;
$code .= 'php::unsetGlobal("' . $name . '");' . PHP_EOL;
}
}
foreach ($this->nativeStaticInitializers as $name => $_) {
$code .= $this->escapeGlobalVar($name) . ' = false;' . PHP_EOL;
}
foreach ($this->constants as $name => $const) {
if ($const->type !== Type::VAR) {
continue;
@ -1693,6 +1724,8 @@ CODE;
$code .= '#include <typephp_fiber_generator.h>' . PHP_EOL;
$code .= PHP_EOL;
$code .= $this->genNativeObjectDeclarations();
if ($this->isBuildModeLib()) {
$code .= $this->genLibraryApiMacro($this->targetName);
}
@ -1712,7 +1745,7 @@ CODE;
$functionDeclarationPrefix = $this->getFunctionDeclarationPrefix($func);
$list = [];
if ($func->method) {
$list[] = Type::OBJECT . ' &this_';
$list[] = ($this->getNativeObjectMethodThisType($func) ?? (Type::OBJECT . ' &')) . 'this_';
}
$argInfoList = $func->argInfoList;
if ($argInfoList) {
@ -1731,7 +1764,10 @@ CODE;
}
$params = implode(', ', $list);
$functionAttribute = $this->getFunctionOptimizationAttribute($func);
$code .= $functionDeclarationPrefix . $functionAttribute . ($func->returnsByRef ? Type::REF : $func->returnType) . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL;
$returnType = $func->returnsByRef
? Type::REF
: ($this->getNativeObjectReturnType($func) ?? $func->returnType);
$code .= $functionDeclarationPrefix . $functionAttribute . $returnType . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL;
if ($func->hasMultiReturn()) {
$code .= 'namespace ' . self::MULTI_RETURN_NAMESPACE . ' {' . PHP_EOL;
$code .= $functionDeclarationPrefix . $functionAttribute . $func->getMultiReturnCppType() . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL;
@ -1866,6 +1902,25 @@ CODE;
return str_replace('-', '_', $rs);
}
public function isNativeClassForStub(string $class): bool
{
return $this->isNativeObjectClass($class);
}
public function isNativeFunctionForStub(string $function): bool
{
return $this->hasFunction($function)
&& $this->functionUsesNativeObject($this->getFunction($function));
}
public function isNativeMethodForStub(string $class, string $method): bool
{
$class = ltrim($class, '\\');
return $this->hasClass($class)
&& $this->getClass($class)->hasMethod($method)
&& $this->functionUsesNativeObject($this->getClass($class)->getMethod($method)->functionDef);
}
public function getArgInfoHeaderFile(string $file, bool $relative = false): string
{
$filePath = $this->getRelativePath(str_replace(['.stub.php', '.php'], '', $file));
@ -2511,6 +2566,9 @@ CODE;
}
foreach ($this->classesDefineInFile as $classDef) {
if ($classDef->nativeObject) {
continue;
}
$cppCode .= $this->genClassWrapper($classDef);
}
@ -2519,7 +2577,7 @@ CODE;
}
foreach ($this->functionDefineInFile as $functionDef) {
if ($functionDef->attributeFactory) {
if ($functionDef->attributeFactory || $this->functionUsesNativeObject($functionDef)) {
continue;
}
$cppCode .= $this->genFunctionWrapper($functionDef);
@ -2565,7 +2623,7 @@ CODE;
}
foreach ($this->symbols->classes() as $classDef) {
if ($classDef->trait !== null) {
if ($classDef->trait !== null || $classDef->nativeObject) {
continue;
}
$ce = $this->getClassCe($classDef);
@ -3241,6 +3299,12 @@ CODE;
$parentClass = $this->getNamespacedClassName($this->parseIdentifier($class->extends));
if ($this->hasClass($parentClass)) {
$parent = $this->getClass($parentClass);
if ($this->classDef->nativeObject !== $parent->nativeObject) {
$this->fatalError(
$class,
'Native and ZendVM-backed classes cannot inherit from each other'
);
}
// 父类是 final 无法继承
if ($parent->flags & Modifiers::FINAL) {
$this->fatalError($class, "Class `{$this->class}` cannot extend final class `{$parentClass}`");
@ -3325,6 +3389,9 @@ CODE;
$this->checkInheritedAbstractMethodsAreImplemented($class);
}
$code = $this->genNativeMethod($methodCodes);
if ($this->classDef->nativeObject) {
$code .= $this->genNativeObjectRuntimeDefinition($this->classDef);
}
$oriCtx = $this->context;
$this->context = $this->classDef->propertyContext;
@ -3531,6 +3598,9 @@ CODE;
// 接口没有方法实体
if ($classDef instanceof ClassDef && $classDef->trait === null) {
if ($classDef->nativeObject) {
return '';
}
$defaultPropCount = 0;
foreach ($classDef->properties as $property) {
if (!$property->isStatic() && $property->default !== null) {
@ -3542,6 +3612,9 @@ CODE;
}
$methods = $classDef->methods;
foreach ($methods as $methodDef) {
if ($this->functionUsesNativeObject($methodDef->functionDef)) {
continue;
}
$cppCode .= $this->genMethodWrapper($classDef, $methodDef);
}
}
@ -3643,10 +3716,18 @@ CODE;
}
if ($this->class) {
$this->addArgument('this_', Type::OBJECT);
if ($this->classDef->nativeObject) {
$this->addArgument('this_', $this->getNativeObjectCppName($this->classDef) . ' &');
$this->addNativeObject('this_', $this->classDef->getNamespacedName(false));
} else {
$this->addArgument('this_', Type::OBJECT);
}
}
foreach ($this->functionDef->argInfoList as $argInfo) {
$this->addArgument($argInfo->name, $argInfo->variadic ? Type::ARRAY : $argInfo->type);
$argumentType = $argInfo->variadic
? Type::ARRAY
: ($this->getNativeObjectArgumentType($argInfo) ?? $argInfo->type);
$this->addArgument($argInfo->name, $argumentType);
if (!$argInfo->variadic and $argInfo->declaredClass) {
$this->addObject($argInfo->name, $argInfo->declaredClass);
}
@ -3665,6 +3746,7 @@ CODE;
$oriLocalVars = $this->context->localVars;
$oriTmpVarIndex = $this->context->tmpVarIndex;
$oriDeclaredObjects = $this->context->declaredObjects;
$oriNativeObjects = $this->context->nativeObjects;
/** SSA/e-SSA analysis for the current function. Built once per function, discarded with the context. */
$ssaBuilder = new SsaBuilder($v->stmts, $this->functionDef->argInfoList);
$ssaBuilder->build();
@ -3677,7 +3759,12 @@ CODE;
$this->optimizeLoopVars($ssaBuilder);
$this->optimizeObjectProps($ssaBuilder);
}
$this->context->resetAnalysisTemporaries($oriLocalVars, $oriTmpVarIndex, $oriDeclaredObjects);
$this->context->resetAnalysisTemporaries(
$oriLocalVars,
$oriTmpVarIndex,
$oriDeclaredObjects,
$oriNativeObjects,
);
}
$stmts = '';
@ -3699,21 +3786,29 @@ CODE;
$multiReturn = $this->functionDef->hasMultiReturn();
$cppReturnType = $multiReturn
? $this->functionDef->getMultiReturnCppType()
: ($this->functionDef->returnsByRef ? Type::REF : $this->getReturnType());
: ($this->functionDef->returnsByRef
? Type::REF
: ($this->getNativeObjectReturnType($this->functionDef) ?? $this->getReturnType()));
$nativeName = self::PREFIX . $name;
$functionAttribute = $this->getFunctionOptimizationAttribute($this->functionDef);
$functionDeclCode = $functionAttribute . $cppReturnType . ' ' . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '(';
if ($this->class) {
$functionDeclCode .= Type::OBJECT . ' &this_';
$functionDeclCode .= ($this->getNativeObjectMethodThisType($this->functionDef)
?? (Type::OBJECT . ' &')) . 'this_';
if ($this->functionDef->params) {
$functionDeclCode .= ', ';
}
}
$functionDeclCode .= $this->functionDef->params . ')';
// Rebuild parameter declarations from ArgInfo at code-generation time.
// Native classes may be discovered after an earlier declaration was
// normalized; the final ABI must use the precise native pointer type,
// not a stale php::Object spelling cached during preprocessing.
$functionDeclCode .= $this->getNativeMethodParameterDeclarations($this->functionDef) . ')';
$code = $functionDeclCode . ' {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->genScopeVarDecl();
$code .= $this->genNativeObjectParameterChecks($this->functionDef);
$code .= "\n";
// Runtime union/nullable parameter type checks
foreach ($this->functionDef->argInfoList as $i => $argInfo) {
@ -4375,6 +4470,7 @@ CODE;
private function checkInterfaceImplementation(NodeAbstract $node, ClassDef $classDef, string $interfaceName): void
{
if ($this->isInternalInterface($interfaceName)) {
$this->checkInternalInterfaceImplementation($node, $classDef, $interfaceName);
return;
}
if (!$this->hasInterface($interfaceName)) {
@ -4581,11 +4677,17 @@ CODE;
if ($chainNode->hasProperty($name)) {
$parentProp = $chainNode->getProperty($name);
// A parent private property would be a separate PHP slot
// hidden by the child declaration. TypePHP forbids that
// dual-slot model. Public/protected declarations instead
// hidden by the child declaration. Zend-backed TypePHP
// classes still forbid that dual-slot model, while Native
// classes have declaring-class-qualified C++ fields and
// can represent it without a runtime property table.
// Public/protected declarations instead
// describe the same inherited property slot and must obey
// PHP-compatible type, visibility and readonly rules.
if ($parentProp->flags & Modifiers::PRIVATE) {
if ($classDef->nativeObject) {
continue;
}
$this->fatalError($classStmt,
"Declaration of `{$className}::\${$name}` conflicts with private property " .
"`{$parentClass}::\${$name}`; property shadowing across inheritance is not allowed");
@ -4764,11 +4866,15 @@ CODE;
private function installComposedTraitMethod(Node\Stmt\ClassMethod $methodStmt): void
{
$name = $methodStmt->name->toString();
$this->assertNativeMagicMethodSupported($methodStmt, $name);
if ($this->classDef->hasMethod($name)) {
return;
}
$flags = $this->parseModifiers($methodStmt->flags);
if ($this->classDef->nativeObject && ($flags & Modifiers::STATIC)) {
$this->fatalError($methodStmt, 'Native class static methods are not supported');
}
$methodDef = new MethodDef($flags, $name);
$methodDef->node = $methodStmt;
$methodDef->traitOrigin = (string) $methodStmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE, '');

@ -159,6 +159,50 @@ trait NativeTypeCompatibilityTrait
);
}
$declaredClass = $argInfo->declaredClass ?: $argInfo->class;
$argumentClass = $this->detectClassOfExpr($arg->value);
if ($this->isNativeObjectClass($argumentClass)
&& !$this->isNativeObjectClass($declaredClass)
) {
if ($declaredClass !== '' && $this->isInterface($declaredClass)) {
$this->fatalError(
$arg,
"Native objects cannot be converted to interface `{$declaredClass}`",
);
}
$this->fatalError(
$arg,
'Native objects cannot cross a PHP/ZendVM argument boundary',
);
}
if ($this->isNativeObjectClass($declaredClass)) {
if ($argInfo->nullable && $this->isNull($arg->value)) {
return 'nullptr';
}
$class = $argumentClass;
if ($class === '' || !$this->isNativeObjectClass($class)
|| !$this->isObjectClassStaticallyAssignableTo($class, $declaredClass)
) {
$argName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name);
$this->fatalError(
$arg,
"Argument `{$argName}` must be a native object of type `{$declaredClass}`"
);
}
if ($argInfo->byRef) {
if (!$this->isVarExpr($arg->value)) {
$this->fatalError($arg, 'Native object reference arguments must be variables');
}
$var = $this->parseIdentifier($arg->value);
if (!$this->isNativeObjectVar($var)) {
$this->fatalError($arg, 'Native object reference arguments must be typed native variables');
}
return $var;
}
$expr = $this->parseOrderedArg($arg);
return $this->materializeCallArgValue($arg->value, $expr);
}
if ($argInfo->byRef) {
if ($this->isReferenceWrapperCall($arg->value)) {
$inner = $this->unwrapReferenceWrapperCall($arg->value, $arg);
@ -217,7 +261,6 @@ trait NativeTypeCompatibilityTrait
}
if ($argInfo->type === Type::OBJECT) {
$declaredClass = $argInfo->declaredClass ?: $argInfo->class;
if ($declaredClass !== '') {
$class = $this->detectDeclaredClassOfExpr($arg->value);
if ($class !== '') {

@ -4553,6 +4553,9 @@ class FileInfo {
}
if ($stmt instanceof Stmt\Function_) {
if (getTranslator()->isNativeFunctionForStub($stmt->namespacedName->toString())) {
continue;
}
$this->funcInfos[] = parseFunctionLike(
$prettyPrinter,
new FunctionName($stmt->namespacedName),
@ -4574,6 +4577,13 @@ class FileInfo {
if ($stmt instanceof Stmt\ClassLike) {
$className = $stmt->namespacedName;
// #[Native] classes are C++-only types. They intentionally have
// no zend_class_entry, arginfo method table or Zend wrappers.
if ($stmt instanceof Class_
&& getTranslator()->isNativeClassForStub($className->toString())
) {
continue;
}
$constInfos = [];
$propertyInfos = [];
$methodInfos = [];
@ -4631,6 +4641,12 @@ class FileInfo {
);
}
} else if ($classStmt instanceof Stmt\ClassMethod) {
if (getTranslator()->isNativeMethodForStub(
$className->toString(),
$classStmt->name->toString(),
)) {
continue;
}
if (!($classStmt->flags & Class_::VISIBILITY_MODIFIER_MASK)) {
$classStmt->flags |= Modifiers::PUBLIC;
}

@ -6,6 +6,11 @@
* @contact service@swoole.com
*/
#[Attribute(Attribute::TARGET_CLASS)]
final readonly class Native
{
}
#[Attribute(Attribute::TARGET_CLASS)]
final readonly class MethodsFor
{

@ -0,0 +1,41 @@
--TEST--
Native class: construction, typed properties and direct method calls
--FILE--
<?php
#[Native]
class Point
{
public int $x = 0;
public int $y = 0;
public function __construct(int $x, int $y)
{
$this->x = $x;
$this->y = $y;
}
public function sum(): int
{
return $this->x + $this->y;
}
}
function main(): void
{
$point = new Point(20, 22);
var_dump($point->x, $point->y, $point->sum());
unset($point);
try {
$point->sum();
} catch (Error $error) {
echo "null guarded\n";
}
}
?>
--EXPECT--
int(20)
int(22)
int(42)
null guarded

@ -0,0 +1,30 @@
--TEST--
Native class: by-reference parameters replace the caller pointer
--FILE--
<?php
#[Native]
class NativeReferenceValue
{
public int $value;
public function __construct(int $value)
{
$this->value = $value;
}
}
function replaceNativeReference(NativeReferenceValue &$value): void
{
$value = new NativeReferenceValue(42);
}
function main(): void
{
$value = new NativeReferenceValue(1);
replaceNativeReference($value);
var_dump($value->value);
}
?>
--EXPECT--
int(42)

@ -0,0 +1,36 @@
--TEST--
Native class: call result arguments are evaluated left-to-right and precisely rooted
--FILE--
<?php
#[Native]
class NativeArgument
{
public string $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
function makeNativeArgument(string $name): NativeArgument
{
echo 'make:', $name, PHP_EOL;
return new NativeArgument($name);
}
function consumeNativeArguments(NativeArgument $first, NativeArgument $second): void
{
echo $first->name, ':', $second->name, PHP_EOL;
}
function main(): void
{
consumeNativeArguments(makeNativeArgument('A'), makeNativeArgument('B'));
}
?>
--EXPECT--
make:A
make:B
A:B

@ -0,0 +1,22 @@
--TEST--
Native class: chained assignment remains in the native object model
--FILE--
<?php
#[Native]
class NativeChainedValue
{
public int $value = 42;
}
function main(): void
{
$first = $second = new NativeChainedValue();
var_dump($first->value, $second->value);
var_dump($first === $second);
}
?>
--EXPECT--
int(42)
int(42)
bool(true)

@ -0,0 +1,32 @@
--TEST--
Native class: method calls accept native object expressions as receivers
--FILE--
<?php
#[Native]
class NativeChainValue
{
public int $value = 42;
public function getValue(): int
{
return $this->value;
}
}
function makeNativeChainValue(): NativeChainValue
{
return new NativeChainValue();
}
function main(): void
{
echo (new NativeChainValue())->getValue(), PHP_EOL;
echo makeNativeChainValue()->getValue(), PHP_EOL;
echo makeNativeChainValue()->value, PHP_EOL;
}
?>
--EXPECT--
42
42
42

@ -0,0 +1,29 @@
--TEST--
Native class: clone is native and the class remains invisible to ZendVM
--FILE--
<?php
#[Native]
class NativeCloneValue
{
public int $value = 1;
public function __clone(): void
{
$this->value++;
}
}
function main(): void
{
$first = new NativeCloneValue();
$second = clone $first;
var_dump($first->value, $second->value);
var_dump(class_exists('NativeCloneValue', false));
}
?>
--EXPECT--
int(1)
int(2)
bool(false)

@ -0,0 +1,71 @@
--TEST--
Native class: nullable, union and intersection fields use Var with runtime type checks
--FILE--
<?php
interface NativeCompositeLeft {}
interface NativeCompositeRight {}
class NativeCompositeBoth implements NativeCompositeLeft, NativeCompositeRight {}
class NativeCompositeLeftOnly implements NativeCompositeLeft {}
#[Native]
class NativeCompositeProperties
{
public ?int $nullableInt;
public int|string|null $unionValue;
public NativeCompositeLeft&NativeCompositeRight $intersectionValue;
}
function writeNullable(NativeCompositeProperties $object, mixed $value): void
{
$object->nullableInt = $value;
}
function writeUnion(NativeCompositeProperties $object, mixed $value): void
{
$object->unionValue = $value;
}
function writeIntersection(NativeCompositeProperties $object, mixed $value): void
{
$object->intersectionValue = $value;
}
function main(): void
{
$object = new NativeCompositeProperties();
var_dump($object->nullableInt, $object->unionValue, $object->intersectionValue);
writeNullable($object, 42);
writeUnion($object, 'ok');
writeIntersection($object, new NativeCompositeBoth());
var_dump($object->nullableInt, $object->unionValue, $object->intersectionValue::class);
try {
writeNullable($object, 'bad');
} catch (TypeError $error) {
echo "type error\n";
}
try {
writeUnion($object, []);
} catch (TypeError $error) {
echo "type error\n";
}
try {
writeIntersection($object, new NativeCompositeLeftOnly());
} catch (TypeError $error) {
echo "type error\n";
}
}
?>
--EXPECT--
NULL
NULL
NULL
int(42)
string(2) "ok"
string(19) "NativeCompositeBoth"
type error
type error
type error

@ -0,0 +1,25 @@
--TEST--
Native class: declarations are emitted in inheritance order
--FILE--
<?php
#[Native]
class NativeOrderChild extends NativeOrderParent
{
public int $child = 2;
}
#[Native]
class NativeOrderParent
{
public int $parent = 1;
}
function main(): void
{
$value = new NativeOrderChild();
echo $value->parent, ':', $value->child, PHP_EOL;
}
?>
--EXPECT--
1:2

@ -0,0 +1,34 @@
--TEST--
Native class: GC finalization runs destructors from derived to base exactly once
--FILE--
<?php
#[Native]
class NativeDestructorBase
{
public function __destruct()
{
echo 'B';
}
}
#[Native]
class NativeDestructorChild extends NativeDestructorBase
{
public function __destruct()
{
echo 'C';
}
}
function main(): void
{
$value = new NativeDestructorChild();
$value = null;
echo "done\n";
}
?>
--EXPECT--
done
CB

@ -0,0 +1,37 @@
--TEST--
Native class: tracing GC collects cycles and runs destructors once
--FILE--
<?php
#[Native]
class NativeNode
{
public ?NativeNode $next = null;
public string $name = '';
public function __construct(string $name)
{
$this->name = $name;
}
public function __destruct()
{
echo $this->name;
}
}
function main(): void
{
$a = new NativeNode('A');
$b = new NativeNode('B');
$a->next = $b;
$b->next = $a;
$a = null;
$b = null;
echo "done\n";
}
?>
--EXPECT--
done
BA

@ -0,0 +1,33 @@
--TEST--
Native class: Getter and Setter generators lower to direct native methods
--FILE--
<?php
#[Native]
class NativeGeneratedAccessors
{
#[Getter]
#[Setter]
private int $value = 1;
public function __construct(
#[Getter]
private string $name = 'native',
) {
}
}
function main(): void
{
$object = new NativeGeneratedAccessors();
var_dump($object->getValue());
var_dump($object->getName());
$object->setValue(42);
var_dump($object->getValue());
}
?>
--EXPECT--
int(1)
string(6) "native"
int(42)

@ -0,0 +1,42 @@
--TEST--
Native class: TypePHP globals and static locals retain request-rooted native objects
--FILE--
<?php
#[Native]
class NativeCounter
{
public int $value;
}
function initializeGlobal(): void
{
global $nativeGlobal;
$nativeGlobal = new NativeCounter();
$nativeGlobal->value = 40;
}
function readGlobal(): int
{
global $nativeGlobal;
return $nativeGlobal->value;
}
function nextStatic(): int
{
static $counter = new NativeCounter();
return ++$counter->value;
}
function main(): void
{
initializeGlobal();
var_dump(readGlobal());
var_dump(nextStatic());
var_dump(nextStatic());
}
?>
--EXPECT--
int(40)
int(1)
int(2)

@ -0,0 +1,38 @@
--TEST--
Native class: compatible inherited property declarations reuse one C++ field
--FILE--
<?php
#[Native]
class NativePropertyBase
{
public int $value = 1;
public function writeFromBase(int $value): void
{
$this->value = $value;
}
}
#[Native]
class NativePropertyChild extends NativePropertyBase
{
public int $value = 2;
public function readFromChild(): int
{
return $this->value;
}
}
function main(): void
{
$value = new NativePropertyChild();
var_dump($value->readFromChild());
$value->writeFromBase(42);
var_dump($value->readFromChild());
}
?>
--EXPECT--
int(2)
int(42)

@ -0,0 +1,41 @@
--TEST--
Native class: statically resolved instanceof is folded at compile time
--FILE--
<?php
#[Native]
class NativeInstanceofBase
{
}
#[Native]
class NativeInstanceofChild extends NativeInstanceofBase
{
}
#[Native]
class NativeInstanceofOther
{
}
function makeInstanceofChild(): NativeInstanceofChild
{
echo "made\n";
return new NativeInstanceofChild();
}
function main(): void
{
$object = new NativeInstanceofChild();
var_dump($object instanceof NativeInstanceofChild);
var_dump($object instanceof NativeInstanceofBase);
var_dump($object instanceof NativeInstanceofOther);
var_dump(makeInstanceofChild() instanceof NativeInstanceofChild);
}
?>
--EXPECT--
bool(true)
bool(true)
bool(false)
made
bool(true)

@ -0,0 +1,24 @@
--TEST--
Native class: internal interfaces are compile-time contracts
--FILE--
<?php
#[Native]
class NativeCountableValue implements Countable
{
public function count(): int
{
return 3;
}
}
function main(): void
{
$value = new NativeCountableValue();
var_dump($value->count());
var_dump($value instanceof Countable);
}
?>
--EXPECT--
int(3)
bool(true)

@ -0,0 +1,74 @@
--TEST--
Native class: keyword conversions lower to exactly typed native methods
--FILE--
<?php
#[Native]
class NativeConversions
{
public int $value = 7;
public function toArray(): array
{
return [$this->value];
}
public function toInt(): int
{
return $this->value;
}
public function toFloat(): float
{
return $this->value + 0.5;
}
public function toBool(): bool
{
return $this->value !== 0;
}
public function toString(): string
{
return 'value=' . $this->value;
}
}
#[Native]
class NativeMagicString
{
public function __toString(): string
{
return 'magic';
}
}
function main(): void
{
$value = new NativeConversions();
var_dump($value->toArray());
var_dump($value->toInt());
var_dump($value->toFloat());
var_dump($value->toBool());
var_dump($value->toString());
var_dump((string) $value);
var_dump(strval($value));
$magic = new NativeMagicString();
var_dump($magic->toString());
var_dump((string) $magic);
}
?>
--EXPECT--
array(1) {
[0]=>
int(7)
}
int(7)
float(7.5)
bool(true)
string(7) "value=7"
string(7) "value=7"
string(7) "value=7"
string(5) "magic"
string(5) "magic"

@ -0,0 +1,35 @@
--TEST--
Native class: __toString and __invoke lower to direct native calls
--FILE--
<?php
#[Native]
class NativeCallableLabel
{
public string $label = 'native';
public function __toString(): string
{
return $this->label;
}
public function __invoke(int $number): string
{
return $this->label . ':' . $number;
}
}
function main(): void
{
$value = new NativeCallableLabel();
echo $value, "\n";
var_dump((string) $value);
var_dump('value=' . $value);
var_dump($value(42));
}
?>
--EXPECT--
native
string(6) "native"
string(12) "value=native"
string(9) "native:42"

@ -0,0 +1,28 @@
--TEST--
Native class: non-null parameters are validated at function entry
--FILE--
<?php
#[Native]
class NativeRequiredArgument
{
}
function acceptRequiredNative(NativeRequiredArgument $value): void
{
echo "entered\n";
}
function main(): void
{
$value = new NativeRequiredArgument();
unset($value);
try {
acceptRequiredNative($value);
} catch (Error $error) {
echo "rejected\n";
}
}
?>
--EXPECT--
rejected

@ -0,0 +1,50 @@
--TEST--
Native class: nullable parameters and returns stay native pointers
--FILE--
<?php
#[Native]
class NativeNullableValue
{
public int $value = 42;
}
function maybeNative(bool $create): ?NativeNullableValue
{
if ($create) {
return new NativeNullableValue();
}
return null;
}
function readMaybeNative(?NativeNullableValue $value): int
{
if ($value === null) {
return -1;
}
return $value->value;
}
function maybeNativeUnion(bool $create): NativeNullableValue|null
{
return $create ? new NativeNullableValue() : null;
}
function readMaybeNativeUnion(NativeNullableValue|null $value): int
{
return $value === null ? -2 : $value->value;
}
function main(): void
{
var_dump(readMaybeNative(maybeNative(false)));
var_dump(readMaybeNative(maybeNative(true)));
var_dump(readMaybeNativeUnion(maybeNativeUnion(false)));
var_dump(readMaybeNativeUnion(maybeNativeUnion(true)));
}
?>
--EXPECT--
int(-1)
int(42)
int(-2)
int(42)

@ -0,0 +1,43 @@
--TEST--
Native class: PHPX value properties retain normal string and array behavior
--FILE--
<?php
#[Native]
class NativePhpValues
{
public string $class = 'reserved';
public string $name = 'native';
public array $values = [1, 2];
public mixed $anything = null;
public ?int $maybe = null;
public function append(int $value): void
{
$this->values[] = $value;
}
}
function main(): void
{
$object = new NativePhpValues();
$object->append(3);
$object->anything = 42;
$object->maybe = 7;
var_dump($object->class, $object->name, $object->values, $object->anything, $object->maybe);
}
?>
--EXPECT--
string(8) "reserved"
string(6) "native"
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
int(42)
int(7)

@ -0,0 +1,51 @@
--TEST--
Native class: parent and child private properties use independent native slots
--FILE--
<?php
#[Native]
class NativePrivateBase
{
private int $value = 10;
public function baseValue(): int
{
return $this->value;
}
public function setBaseValue(int $value): void
{
$this->value = $value;
}
}
#[Native]
class NativePrivateChild extends NativePrivateBase
{
private int $value = 20;
public function childValue(): int
{
return $this->value;
}
public function setChildValue(int $value): void
{
$this->value = $value;
}
}
function main(): void
{
$value = new NativePrivateChild();
var_dump($value->baseValue(), $value->childValue());
$value->setBaseValue(11);
$value->setChildValue(22);
var_dump($value->baseValue(), $value->childValue());
}
?>
--EXPECT--
int(10)
int(20)
int(11)
int(22)

@ -0,0 +1,32 @@
--TEST--
Native class: PHP 8.4 property hooks use native getter and setter calls
--FILE--
<?php
#[Native]
class NativeHookValue
{
private int $stored = 1;
public int $value {
get {
return $this->stored * 2;
}
set(int $value) {
$this->stored = $value;
}
}
}
function main(): void
{
$object = new NativeHookValue();
var_dump($object->value);
$object->value = 21;
var_dump($object->value);
}
?>
--EXPECT--
int(2)
int(42)

@ -0,0 +1,73 @@
--TEST--
Native class: trait composition, inheritance and interface contracts
--FILE--
<?php
interface Named
{
public function label(): string;
}
trait HasCounter
{
public int $count = 0;
public function increment(): void
{
$this->count++;
}
}
#[Native]
class NativeBase
{
use HasCounter;
public function __construct(int $initial)
{
$this->count = $initial;
}
public function label(): string
{
return 'base';
}
}
#[Native]
class NativeChild extends NativeBase implements Named
{
public function __construct()
{
parent::__construct(2);
}
public function label(): string
{
return 'child';
}
}
function nativeLabel(NativeBase $value): string
{
return $value->label();
}
function makeNativeChild(): NativeChild
{
return new NativeChild();
}
function main(): void
{
$value = makeNativeChild();
$value->increment();
var_dump($value->label(), nativeLabel($value), $value->count, $value instanceof Named);
}
?>
--EXPECT--
string(5) "child"
string(5) "child"
int(3)
bool(true)

@ -0,0 +1,28 @@
--TEST--
Native class: unset and null clear only the local pointer slot
--FILE--
<?php
#[Native]
class NativeAliasValue
{
public int $value = 42;
}
function main(): void
{
$first = new NativeAliasValue();
$alias = $first;
unset($first);
var_dump($first === null, $alias->value);
$second = $alias;
$alias = null;
var_dump($alias === null, $second->value);
}
?>
--EXPECT--
bool(true)
int(42)
bool(true)
int(42)

@ -0,0 +1,44 @@
--TEST--
Native class: ternary, match and coalesce preserve native pointer types
--FILE--
<?php
#[Native]
class NativeSelectedValue
{
public int $value;
public function __construct(int $value)
{
$this->value = $value;
}
}
function selectWithMatch(int $kind): NativeSelectedValue
{
return match ($kind) {
1 => new NativeSelectedValue(10),
default => new NativeSelectedValue(20),
};
}
function selectWithCoalesce(?NativeSelectedValue $value): NativeSelectedValue
{
return $value ?? new NativeSelectedValue(30);
}
function main(): void
{
$first = true ? new NativeSelectedValue(1) : new NativeSelectedValue(2);
var_dump($first->value);
var_dump(selectWithMatch(1)->value, selectWithMatch(2)->value);
var_dump(selectWithCoalesce(null)->value);
var_dump(selectWithCoalesce($first)->value);
}
?>
--EXPECT--
int(1)
int(10)
int(20)
int(30)
int(1)

@ -0,0 +1,30 @@
--TEST--
Native class: native-only methods on Zend objects use direct calls and stay out of Zend metadata
--FILE--
<?php
#[Native]
class NativeMethodArgument
{
public int $value = 42;
}
class ZendMethodHost
{
public function read(NativeMethodArgument $value): int
{
return $value->value;
}
}
function main(): void
{
$host = new ZendMethodHost();
$value = new NativeMethodArgument();
var_dump($host->read($value));
var_dump(method_exists($host, 'read'));
}
?>
--EXPECT--
int(42)
bool(false)

@ -0,0 +1,43 @@
--TEST--
Native class: properties without explicit defaults use their type zero values
--FILE--
<?php
#[Native]
class NativeZeroValue
{
public bool $enabled;
public int $count;
public float $ratio;
public string $name;
public array $items;
public mixed $value;
public ?stdClass $object;
public ?NativeZeroValue $child;
}
function main(): void
{
$value = new NativeZeroValue();
var_dump(
$value->enabled,
$value->count,
$value->ratio,
$value->name,
$value->items,
$value->value,
$value->object,
$value->child === null,
);
}
?>
--EXPECT--
bool(false)
int(0)
float(0)
string(0) ""
array(0) {
}
NULL
NULL
bool(true)
Loading…
Cancel
Save