TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

1732 lines
70 KiB

<?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
{
private const string NATIVE_VIRTUAL_CLONE_METHOD = '__typephp_native_clone';
/**
* 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,
];
/** Internal interfaces which PHP does not allow an ordinary class to implement directly. */
private const NON_IMPLEMENTABLE_INTERNAL_INTERFACES = [
'throwable' => true,
'traversable' => true,
'datetimeinterface' => true,
'unitenum' => true,
'backedenum' => 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 {
if (isset(self::NON_IMPLEMENTABLE_INTERNAL_INTERFACES[strtolower(ltrim($interfaceName, '\\'))])) {
$this->fatalError(
$node,
"Native class `{$classDef->getNamespacedName(false)}` cannot implement internal interface `{$interfaceName}`",
);
}
$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(),
));
}
if (!$type instanceof \ReflectionNamedType) {
throw new \LogicException('Unsupported reflection type: ' . $type::class);
}
return $this->reflectionNamedTypeToNode($type, $declaringClass, $allowNullableWrapper);
}
private function reflectionNamedTypeToNode(
\ReflectionNamedType $type,
\ReflectionClass $declaringClass,
bool $allowNullableWrapper,
): NodeAbstract {
$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, '\\');
if ($class === '') {
return false;
}
if (isset($this->nativeClassDeclarations[strtolower($class)])) {
return true;
}
return $this->hasClass($class) && $this->getClass($class)->nativeObject;
}
/**
* Native objects have identity but no Zend object handler capable of PHP's
* recursive loose/value comparison. Reject unsupported operators before a
* raw pointer can reach php::equals() or a C++ arithmetic expression.
*/
protected function assertNativeObjectBinaryOperatorSupported(Node\Expr\BinaryOp $expr): void
{
$leftNative = $this->isNativeObjectClass($this->detectClassOfExpr($expr->left));
$rightNative = $this->isNativeObjectClass($this->detectClassOfExpr($expr->right));
if (!$leftNative && !$rightNative) {
return;
}
if ($expr instanceof Node\Expr\BinaryOp\Identical
|| $expr instanceof Node\Expr\BinaryOp\NotIdentical
|| $expr instanceof Node\Expr\BinaryOp\Coalesce
|| $expr instanceof Node\Expr\BinaryOp\Concat
|| $expr instanceof Node\Expr\BinaryOp\BooleanAnd
|| $expr instanceof Node\Expr\BinaryOp\LogicalAnd
|| $expr instanceof Node\Expr\BinaryOp\BooleanOr
|| $expr instanceof Node\Expr\BinaryOp\LogicalOr
|| $expr instanceof Node\Expr\BinaryOp\LogicalXor
|| $expr instanceof Node\Expr\BinaryOp\Pipe
) {
return;
}
$operator = match (true) {
$expr instanceof Node\Expr\BinaryOp\Equal => '==',
$expr instanceof Node\Expr\BinaryOp\NotEqual => '!=',
$expr instanceof Node\Expr\BinaryOp\Plus => '+',
$expr instanceof Node\Expr\BinaryOp\Minus => '-',
$expr instanceof Node\Expr\BinaryOp\Mul => '*',
$expr instanceof Node\Expr\BinaryOp\Div => '/',
$expr instanceof Node\Expr\BinaryOp\Mod => '%',
$expr instanceof Node\Expr\BinaryOp\Pow => '**',
$expr instanceof Node\Expr\BinaryOp\Smaller => '<',
$expr instanceof Node\Expr\BinaryOp\SmallerOrEqual => '<=',
$expr instanceof Node\Expr\BinaryOp\Greater => '>',
$expr instanceof Node\Expr\BinaryOp\GreaterOrEqual => '>=',
$expr instanceof Node\Expr\BinaryOp\Spaceship => '<=>',
$expr instanceof Node\Expr\BinaryOp\ShiftLeft => '<<',
$expr instanceof Node\Expr\BinaryOp\ShiftRight => '>>',
$expr instanceof Node\Expr\BinaryOp\BitwiseAnd => '&',
$expr instanceof Node\Expr\BinaryOp\BitwiseOr => '|',
$expr instanceof Node\Expr\BinaryOp\BitwiseXor => '^',
default => $expr->getType(),
};
$suffix = in_array($operator, ['==', '!='], true)
? '; use `===` or `!==` for identity comparison'
: '';
$this->fatalError($expr, "Native objects do not support the `{$operator}` operator{$suffix}");
}
/**
* A Native object expression is a raw C++ pointer. Applying arithmetic
* unary or update operators to it could otherwise become pointer
* arithmetic, which is valid C++ but has no PHP object semantics.
*/
protected function assertNativeObjectOperatorOperandSupported(
NodeAbstract $operand,
NodeAbstract $errorNode,
string $operator,
bool $unary = false,
): void {
if (!$this->isNativeObjectClass($this->detectClassOfExpr($operand))) {
return;
}
$prefix = $unary ? 'unary ' : '';
$this->fatalError($errorNode, "Native objects do not support the {$prefix}`{$operator}` operator");
}
protected function assertNotNativeObjectArrayKey(NodeAbstract $key): void
{
if ($this->isNativeObjectClass($this->detectClassOfExpr($key))) {
$this->fatalError($key, 'Native objects cannot be used as PHP array keys');
}
}
/**
* Return the statically known Native receiver class for array syntax.
* PHP only dispatches [] through ArrayAccess; having similarly named
* methods without the interface is not sufficient.
*/
protected function getNativeArrayAccessClass(
NodeAbstract $receiver,
NodeAbstract $errorNode,
): ?string {
$class = $this->detectClassOfExpr($receiver);
if (!$this->isNativeObjectClass($class)) {
return null;
}
$implementsArrayAccess = false;
foreach ($this->getClassImplementedInterfaces($this->getClass($class)) as $interface) {
if (strcasecmp(ltrim($interface, '\\'), 'ArrayAccess') === 0) {
$implementsArrayAccess = true;
break;
}
}
if (!$implementsArrayAccess) {
$this->fatalError(
$errorNode,
"Native class `{$class}` must implement `ArrayAccess` to use array access syntax",
);
}
return $class;
}
/** Locate the first Native ArrayAccess dimension inside a writable chain. */
protected function findNativeArrayAccessDimension(NodeAbstract $expression): ?Node\Expr\ArrayDimFetch
{
if ($expression instanceof Node\Expr\ArrayDimFetch) {
if ($this->isNativeObjectClass($this->detectClassOfExpr($expression->var))) {
return $expression;
}
return $this->findNativeArrayAccessDimension($expression->var);
}
if ($expression instanceof Node\Expr\PropertyFetch
|| $expression instanceof Node\Expr\NullsafePropertyFetch
) {
return $this->findNativeArrayAccessDimension($expression->var);
}
return null;
}
protected function assertNativeArrayAccessDirectWrite(
NodeAbstract $target,
bool $allowDirectDimension,
): void {
$dimension = $this->findNativeArrayAccessDimension($target);
if ($dimension === null) {
return;
}
$this->getNativeArrayAccessClass($dimension->var, $dimension);
if ($allowDirectDimension && $dimension === $target) {
return;
}
$this->fatalError(
$target,
'Indirect modification of Native ArrayAccess elements is not supported',
);
}
protected function assertNativeArrayAccessReferenceForbidden(NodeAbstract $expression): void
{
$dimension = $this->findNativeArrayAccessDimension($expression);
if ($dimension === null) {
return;
}
$this->getNativeArrayAccessClass($dimension->var, $dimension);
$this->fatalError(
$expression,
'References to Native ArrayAccess elements are not supported',
);
}
/**
* Build a normal MethodCall so Native dispatch, visibility, signature
* checks, virtual thunks and PHP evaluation ordering remain centralized.
*
* @param list<Node\Arg> $arguments
*/
protected function parseNativeArrayAccessCall(
Node\Expr\ArrayDimFetch $access,
string $method,
array $arguments,
): ?string {
if ($this->getNativeArrayAccessClass($access->var, $access) === null) {
return null;
}
if ($access->dim !== null) {
$this->assertNotNativeObjectArrayKey($access->dim);
}
return $this->parseMethodCall(new Node\Expr\MethodCall(
$access->var,
new Node\Identifier($method),
$arguments,
$access->getAttributes(),
));
}
protected function assertNotNativeObjectDynamicClassTarget(
NodeAbstract $target,
NodeAbstract $errorNode,
): void {
if ($this->isNativeObjectClass($this->detectClassOfExpr($target))) {
$this->fatalError($errorNode, 'Native objects cannot be used as dynamic class targets');
}
}
/**
* Resolve only the `$GLOBALS['literal']` form. A fixed key can share the
* compiler-owned Native global slot without touching Zend's symbol table;
* dynamically addressed `$GLOBALS[$name]` remains a Zend value boundary.
*/
protected function getLiteralGlobalsSlot(NodeAbstract $expression): ?string
{
if (!$expression instanceof Node\Expr\ArrayDimFetch
|| !$expression->var instanceof Node\Expr\Variable
|| $expression->var->name !== 'GLOBALS'
|| !$expression->dim instanceof Node\Scalar\String_
) {
return null;
}
return $expression->dim->value;
}
/**
* Resolve a statically known `$GLOBALS[...]` key to the compiler-owned
* Native global slot. Dynamic keys deliberately return null and remain on
* the ordinary Zend HashTable path.
*/
protected function getStaticGlobalsSlot(NodeAbstract $expression): ?string
{
$literal = $this->getLiteralGlobalsSlot($expression);
if ($literal !== null) {
return $literal;
}
if (!$expression instanceof Node\Expr\ArrayDimFetch
|| !$expression->var instanceof Node\Expr\Variable
|| $expression->var->name !== 'GLOBALS'
|| $expression->dim === null
) {
return null;
}
// Preserve PHP visibility diagnostics even though the key itself is
// folded and never emitted as a runtime class-constant fetch.
$finder = new \PhpParser\NodeFinder();
foreach ($finder->findInstanceOf($expression->dim, Node\Expr\ClassConstFetch::class) as $fetch) {
if (!$fetch->class instanceof Node\Name
|| !$fetch->name instanceof Node\Identifier
|| strcasecmp($fetch->name->toString(), 'class') === 0
) {
continue;
}
$class = $fetch->class->toString();
if (strcasecmp($class, 'self') === 0 || strcasecmp($class, 'static') === 0) {
$class = $this->getFullClassName();
} elseif (strcasecmp($class, 'parent') === 0) {
$class = $this->classDef?->extends ?? '';
} else {
$resolved = $fetch->class->getAttribute('resolvedName');
$class = $resolved instanceof Node\Name
? $resolved->toString()
: $this->getNamespacedClassName($class);
}
if ($class !== '') {
$this->getClassConstValue(
$fetch,
$class,
$fetch->name->toString(),
$this->getFullClassName(),
);
}
}
$this->nativeGlobalTypeResolver ??= new NativeGlobalTypeResolver(
$this->symbols->classes(),
$this->constants,
);
return $this->nativeGlobalTypeResolver->staticString(
$expression->dim,
$this->getFullClassName(),
);
}
protected function getNativeObjectCppName(string|ClassDef $class): string
{
if ($class instanceof ClassDef) {
return self::PREFIX . $this->getNativeName('', $class->namespace, $class->name);
}
$class = ltrim($class, '\\');
if ($this->hasClass($class)) {
$classDef = $this->getClass($class);
return self::PREFIX . $this->getNativeName('', $classDef->namespace, $classDef->name);
}
// The Native declaration catalog is built before semantic
// preprocessing, so signatures may name a Native class declared in a
// later file. Its C++ symbol is derivable from the fully-qualified PHP
// name without requiring the complete ClassDef yet.
$separator = strrpos($class, '\\');
$namespace = $separator === false ? '' : substr($class, 0, $separator);
$name = $separator === false ? $class : substr($class, $separator + 1);
return self::PREFIX . $this->getNativeName('', $namespace, $name);
}
protected function getNativeObjectDescriptorName(string|ClassDef $class): string
{
return $this->getNativeObjectCppName($class) . '__type';
}
protected function getNativeObjectPointerType(string|ClassDef $class): string
{
return $this->getNativeObjectCppName($class) . ' *';
}
/**
* A statically typed base pointer may hold any Native subclass. Only such
* inheritance hierarchies need a vtable entry for clone; standalone/final
* object layouts retain the zero-overhead static clone path.
*/
protected function nativeObjectUsesVirtualClone(string|ClassDef $class): bool
{
$classDef = $class instanceof ClassDef ? $class : $this->getClass(ltrim($class, '\\'));
if ($classDef->extends !== '' && $this->isNativeObjectClass($classDef->extends)) {
return true;
}
$className = $classDef->getNamespacedName(false);
foreach ($this->symbols->classes() as $candidate) {
if (!$candidate->nativeObject
|| $this->isSameClassName($candidate->getNamespacedName(false), $className)
) {
continue;
}
$parent = $candidate->extends;
while ($parent !== '' && $this->isNativeObjectClass($parent)) {
if ($this->isSameClassName($parent, $className)) {
return true;
}
$parent = $this->getClass($parent)->extends;
}
}
return false;
}
protected function getNativeObjectArgumentType(ArgInfo $argument): ?string
{
$class = $argument->declaredClass ?: $argument->class;
if ($argument->type !== Type::OBJECT || !$this->isNativeObjectClass($class)) {
return null;
}
return $this->getNativeObjectPointerType($class);
}
/**
* A virtual adapter may widen value parameters, but C++ cannot safely
* adapt T*& to Base*&: the callee could replace it with a Base that is not
* a T. Native objects intentionally carry no runtime class tag, so reject
* that one PHP variance case at the declaration boundary.
*/
protected function assertNativeVirtualByRefStorageCompatible(
NodeAbstract $node,
FunctionDef $child,
FunctionDef $parent,
): void {
foreach ($parent->argInfoList as $index => $parentArgument) {
if (!$parentArgument->byRef || !isset($child->argInfoList[$index])) {
continue;
}
$childArgument = $child->argInfoList[$index];
$parentStorage = $this->getNativeObjectArgumentType($parentArgument)
?? $parentArgument->type;
$childStorage = $this->getNativeObjectArgumentType($childArgument)
?? $childArgument->type;
if ($parentStorage !== $childStorage) {
$this->fatalError(
$node,
'Native virtual by-reference parameters must keep the same storage type',
);
}
}
}
protected function resolveNullableNativeObjectType(?NodeAbstract $type, int $declarationKind): ?array
{
$inner = $type instanceof Node\NullableType ? $type->type : null;
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;
}
$common = $this->getCommonNativeObjectClass($common, $class);
if ($common === '') {
return '';
}
}
return $common;
}
protected function getCommonNativeObjectClass(string $left, string $right): string
{
$leftAncestors = [];
while ($this->isNativeObjectClass($left)) {
$definition = $this->getClass($left);
$canonical = $definition->getNamespacedName(false);
$leftAncestors[strtolower($canonical)] = $canonical;
$left = $definition->extends;
}
while ($this->isNativeObjectClass($right)) {
$definition = $this->getClass($right);
$canonical = $definition->getNamespacedName(false);
$key = strtolower($canonical);
if (isset($leftAncestors[$key])) {
return $leftAncestors[$key];
}
$right = $definition->extends;
}
return '';
}
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->fatalError(
$errorNode,
'Native object types do not support union or intersection declarations; use nullable ?Class syntax',
);
}
}
protected function assertNativeObjectFunctionSignature(
Node\Stmt\Function_|Node\Stmt\ClassMethod $node,
FunctionDef $function,
): void {
if ($this->classDef?->nativeObject && $function->returnTypeKeyword === 'static') {
$this->fatalError(
$node,
'Native classes do not support late static binding in return types',
);
}
if ($this->isNativeObjectClass($function->returnClass) && $function->returnsByRef) {
$this->fatalError($node, 'Native objects cannot be returned by reference');
}
foreach ($function->argInfoList as $index => $argument) {
$class = $argument->declaredClass ?: $argument->class;
if (!$this->isNativeObjectClass($class)) {
continue;
}
$parameter = $node->params[$index] ?? $node;
if ($argument->byRef) {
$this->fatalError($parameter, 'Native object parameters cannot be passed by reference');
}
if ($argument->variadic) {
$this->fatalError($parameter, 'Native object parameters cannot be variadic');
}
if ($parameter instanceof Node\Param
&& $parameter->default !== null
&& $this->isNull($parameter->default)
&& !$argument->nullable
) {
$this->fatalError(
$parameter,
'A Native object parameter with a null default must use explicit nullable ?Class syntax',
);
}
}
}
/**
* Validate a Native reference entirely from compile-time metadata.
*
* A Native object variable is a typed pointer and must never expose its
* pointer slot as a PHP reference. A Native property may expose a reference
* only when it was explicitly declared `any`: that field intentionally
* permits arbitrary PHP values. Every other declaration, including
* `mixed`, must reject references because dynamic Zend code could replace
* the referenced value with one that violates the Native field contract.
*/
protected function assertNativeObjectReferenceForbidden(
NodeAbstract $expr,
NodeAbstract $errorNode,
): void {
if ($expr instanceof Node\Expr\PropertyFetch) {
$receiverClass = $this->detectClassOfExpr($expr->var);
if ($this->isNativeObjectClass($receiverClass)) {
if (!$expr->name instanceof Node\Identifier) {
$this->fatalError($errorNode, 'Dynamic native object property access is not supported');
}
$property = $expr->name->toString();
$resolution = $this->resolveNativeInstanceProperty($expr, $property, $receiverClass);
if ($resolution === null) {
$this->fatalError(
$errorNode,
"Native class `{$receiverClass}` has no property `\${$property}`",
);
}
$this->applyNativePropertyAccessResult($expr, $resolution);
$definition = $resolution->propertyDef;
if (!$definition->explicitAny || $definition->getter !== null || $definition->setter !== null) {
$this->fatalError(
$errorNode,
'Only Native object properties declared as any can be referenced',
);
}
return;
}
}
$class = $this->detectDeclaredClassOfExpr($expr);
if ($this->isNativeObjectClass($class)) {
$this->fatalError(
$errorNode,
'Native objects cannot be referenced; object assignment already shares identity',
);
}
}
/**
* Reflection constructors accept a class name string, so the ordinary
* Native-pointer Zend boundary check cannot see this escape. Reject known
* Native class literals before generating a lookup for a class which is
* intentionally absent from the Zend class table.
*/
protected function assertNativeClassNotUsedWithReflection(
Node\Expr\New_ $expr,
string $constructedClass,
): void {
$reflectionClass = strtolower(ltrim($constructedClass, '\\'));
if (!in_array($reflectionClass, [
'reflectionclass',
'reflectionmethod',
'reflectionproperty',
'reflectionclassconstant',
'reflectionenum',
'reflectionenumunitcase',
'reflectionenumbackedcase',
], true) || $expr->args === []) {
return;
}
$target = $expr->args[0]->value;
$nativeClass = '';
if ($this->isScalarString($target)) {
// Reflection string arguments are runtime names, not names relative
// to the current PHP namespace.
$candidate = ltrim($target->value, '\\');
if ($this->isNativeObjectClass($candidate)) {
$nativeClass = $candidate;
}
} elseif ($this->isClassConstFetch($target)
&& $this->isNameExpr($target->class)
&& $this->isIdExpr($target->name)
&& strtolower($this->parseIdentifier($target->name)) === 'class'
) {
$candidate = $this->parseIdentifier($target->class);
if ($candidate === 'self' || $candidate === 'static') {
$candidate = $this->getFullClassName();
} elseif ($candidate === 'parent') {
$candidate = $this->classDef?->extends ?? '';
} else {
$candidate = $this->getNamespacedClassName($candidate);
}
if ($this->isNativeObjectClass($candidate)) {
$nativeClass = $candidate;
}
}
if ($nativeClass !== '') {
$reflectionName = strrchr($constructedClass, '\\');
$reflectionName = $reflectionName === false
? $constructedClass
: substr($reflectionName, 1);
$this->fatalError(
$target,
"Native class `{$nativeClass}` cannot be used with {$reflectionName}",
);
}
}
protected function getNativeObjectReturnType(FunctionDef $function): ?string
{
if ($function->returnType !== Type::OBJECT || !$this->isNativeObjectClass($function->returnClass)) {
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 markNativeObjectNonNull(string $name): void
{
$this->context->nonNullNativeObjects[$name] = true;
}
protected function forgetNativeObjectNonNull(string $name): void
{
unset($this->context->nonNullNativeObjects[$name]);
}
protected function isNativeObjectKnownNonNull(string $name): bool
{
return isset($this->context->nonNullNativeObjects[$name]);
}
/**
* Conservative straight-line non-null proof for a Native pointer value.
* This deliberately excludes properties (a non-nullable Native field has
* a nullptr zero value) and control-flow expressions. The caller may only
* retain the proof at function top level, where no branch merge is needed.
*/
protected function isNativeObjectExpressionKnownNonNull(NodeAbstract $expr): bool
{
if ($expr instanceof Node\Expr\ErrorSuppress) {
return $this->isNativeObjectExpressionKnownNonNull($expr->expr);
}
if ($expr instanceof Node\Expr\New_ || $expr instanceof Node\Expr\Clone_) {
return $this->isNativeObjectClass($this->detectClassOfExpr($expr));
}
if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) {
return $this->isNativeObjectKnownNonNull($this->parseIdentifier($expr));
}
if ($expr instanceof Node\Expr\FuncCall
&& ($this->isNameExpr($expr->name) || $this->isFullNameExpr($expr->name))
) {
$native = $this->findNativeFunction($this->parseIdentifier($expr->name));
if ($native !== false) {
$function = $this->getFunction($native);
return $this->isNativeObjectClass($function->returnClass)
&& !$function->returnNullable;
}
}
if ($expr instanceof Node\Expr\MethodCall
&& $this->isIdExpr($expr->name)
) {
$receiverClass = $this->detectClassOfExpr($expr->var);
if ($this->isNativeObjectClass($receiverClass)) {
$method = $this->findNativeObjectMethod(
$receiverClass,
$this->parseIdentifier($expr->name),
);
return $method !== null
&& $this->isNativeObjectClass($method->functionDef->returnClass)
&& !$method->functionDef->returnNullable;
}
}
return false;
}
protected function getNativeObjectReceiver(string $name): string
{
if ($name === 'this_') {
return 'this_';
}
if ($this->isNativeObjectKnownNonNull($name)) {
return '(*' . $name . ')';
}
$class = $this->getNativeObjectVarClass($name);
return 'php::nativeDeref(' . $name . ', "' . addslashes($class) . '")';
}
/**
* Native method bodies receive `this_` by C++ reference so direct member
* access remains zero-cost. In a PHP value context, however, `$this` is an
* object handle and must therefore become the address of that reference.
* Keep this conversion out of parseIdentifier(): receiver contexts need
* the reference itself, while assignments, returns, arguments and
* comparisons need the typed pointer.
*/
protected function normalizeNativeObjectValueExpr(NodeAbstract $expr, string $value): string
{
if ($this->classDef?->nativeObject
&& $this->isVarExpr($expr)
&& $this->parseVariable($expr) === 'this_'
) {
return '&this_';
}
return $value;
}
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;
}
/**
* Lower a nullsafe chain whose root and every intermediate receiver are
* Native pointers. The generic implementation deliberately uses
* php::Object/Variant and therefore cannot represent this object model.
*/
protected function parseNativeNullsafeAccess(
Node\Expr\PropertyFetch|Node\Expr\MethodCall|Node\Expr\NullsafePropertyFetch|Node\Expr\NullsafeMethodCall $expr,
): ?string {
$steps = [];
$base = $expr;
while ($base instanceof Node\Expr\PropertyFetch
|| $base instanceof Node\Expr\MethodCall
|| $base instanceof Node\Expr\NullsafePropertyFetch
|| $base instanceof Node\Expr\NullsafeMethodCall
) {
array_unshift($steps, $base);
$base = $base->var;
}
$baseClass = $this->detectClassOfExpr($base);
if ($baseClass === '' && $this->isVarExpr($base)) {
$baseName = $this->parseIdentifier($base);
if ($this->isNativeObjectVar($baseName)) {
$baseClass = $this->getNativeObjectVarClass($baseName);
}
}
if (!$this->isNativeObjectClass($baseClass)) {
return null;
}
$current = $this->isVarExpr($base)
? $this->parseIdentifier($base)
: $this->materializeNativeObjectReceiver($base, $baseClass);
if ($this->isVarExpr($base) && !$this->hasVar($current) && $current !== 'this_') {
$this->errorUndefinedVariable($base);
}
$body = '';
$last = array_key_last($steps);
$finalClass = '';
$finalType = Type::VAR;
$nullToken = '__TYPEPHP_NATIVE_NULLSAFE_NULL__';
foreach ($steps as $index => $step) {
$nullsafe = $step instanceof Node\Expr\NullsafePropertyFetch
|| $step instanceof Node\Expr\NullsafeMethodCall;
if ($nullsafe) {
$body .= $this->getIndent() . 'if (' . $current . ' == nullptr) { return '
. $nullToken . '; }' . PHP_EOL;
}
$receiver = new Node\Expr\Variable($current, $step->var->getAttributes());
if ($step instanceof Node\Expr\PropertyFetch || $step instanceof Node\Expr\NullsafePropertyFetch) {
if (!$step->name instanceof Node\Identifier) {
$this->fatalError($step, 'Dynamic native object property access is not supported');
}
$access = new Node\Expr\PropertyFetch($receiver, clone $step->name, $step->getAttributes());
} else {
if (!$step->name instanceof Node\Identifier) {
$this->fatalError($step, 'Dynamic native object method calls are not supported');
}
$access = new Node\Expr\MethodCall(
$receiver,
clone $step->name,
$step->args,
$step->getAttributes(),
);
}
[$value, $before, $after] = $this->parseExprWithCapturedStmts($access);
$valueClass = $this->detectClassOfExpr($access);
$valueType = $this->detectTypeOfExpr($access);
$body .= $this->formatCapturedStmtLines($before);
if ($index !== $last) {
if (!$this->isNativeObjectClass($valueClass)) {
$this->fatalError(
$step,
'A Native nullsafe chain cannot continue through a non-Native value',
);
}
$next = $this->genTmpVarName();
$this->addLocalVar($next, $this->getNativeObjectPointerType($valueClass));
$this->addNativeObject($next, $valueClass);
$body .= $this->getIndent() . $next . ' = ' . $value . ';' . PHP_EOL;
$body .= $this->formatCapturedStmtLines($after);
$current = $next;
continue;
}
$finalClass = $valueClass;
$finalType = $valueType;
if ($finalType === Type::VOID) {
$body .= $this->getIndent() . $value . ';' . PHP_EOL;
$body .= $this->formatCapturedStmtLines($after);
$body .= $this->getIndent() . 'return php::null;' . PHP_EOL;
continue;
}
if ($after !== []) {
if ($this->isNativeObjectClass($finalClass)) {
$result = $this->genTmpVarName();
$this->addLocalVar($result, $this->getNativeObjectPointerType($finalClass));
$this->addNativeObject($result, $finalClass);
$body .= $this->getIndent() . $result . ' = ' . $value . ';' . PHP_EOL;
} else {
$result = $this->genTmpVarName();
$body .= $this->getIndent() . 'auto ' . $result . ' = ' . $value . ';' . PHP_EOL;
}
$body .= $this->formatCapturedStmtLines($after);
$value = $result;
}
$body .= $this->getIndent() . 'return '
. ($this->isNativeObjectClass($finalClass) ? $value : 'php::Var(' . $value . ')')
. ';' . PHP_EOL;
}
$nativeResult = $this->isNativeObjectClass($finalClass);
$returnType = $nativeResult ? $this->getNativeObjectPointerType($finalClass) : Type::VAR;
$nullValue = $nativeResult ? 'nullptr' : 'php::null';
$body = str_replace($nullToken, $nullValue, $body);
return '[&]() -> ' . $returnType . ' {' . PHP_EOL
. $body
. $this->getIndent() . '}()';
}
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);
}
if ($classDef->hasAbstractMethod($method)
&& isset($classDef->abstractMethodDefs[strtolower($method)])
) {
return $classDef->getAbstractMethod($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 parseNativeObjectExplicitConversion(NodeAbstract $expr, string $method): ?string
{
$class = $this->detectClassOfExpr($expr);
if (!$this->isNativeObjectClass($class)) {
return null;
}
return $this->parseMethodCall(new Node\Expr\MethodCall(
$expr,
new Node\Identifier($method),
[],
$expr->getAttributes(),
));
}
protected function getNativeVirtualMethodName(string|ClassDef $slotClass, string $method): string
{
return '__typephp_virtual_' . strtolower($this->getNativeObjectCppName($slotClass))
. '__' . strtolower($method);
}
/** Whether this declaration owns a virtual dispatch slot. */
protected function isNativeVirtualMethod(ClassDef $class, MethodDef $method): bool
{
if ($method->flags & (Modifiers::STATIC | Modifiers::PRIVATE | Modifiers::FINAL)) {
return false;
}
if ($method->flags & Modifiers::ABSTRACT) {
return true;
}
if (in_array(strtolower($method->name), ['__construct', '__destruct', '__clone'], true)) {
return false;
}
if ($this->isOverrideMethod($class->getNamespacedName(false) . '::' . $method->name)) {
return true;
}
return false;
}
/**
* Return every virtual slot a declaration must implement. PHP permits
* contravariant parameters and covariant returns, so one C++ virtual
* signature cannot represent the whole family. Each source declaration
* owns a stable slot; an override supplies adapters for all ancestor slots.
*
* @return list<array{ClassDef, MethodDef}>
*/
protected function getNativeVirtualMethodSlots(ClassDef $class, MethodDef $method): array
{
$slots = [];
$current = $class;
while (true) {
$declaration = null;
if ($current->hasMethod($method->name)) {
$declaration = $current->getMethod($method->name);
} elseif ($current->hasAbstractMethod($method->name)
&& isset($current->abstractMethodDefs[strtolower($method->name)])
) {
$declaration = $current->getAbstractMethod($method->name);
}
if ($declaration !== null && $this->isNativeVirtualMethod($current, $declaration)) {
$slots[] = [$current, $declaration];
}
if ($current->extends === '' || !$this->isNativeObjectClass($current->extends)) {
break;
}
$current = $this->getClass($current->extends);
}
return $slots;
}
protected function getNativeMethodReturnCppType(FunctionDef $function): string
{
return $function->returnsByRef
? Type::REF
: ($this->getNativeObjectReturnType($function) ?? $function->returnType);
}
protected function getNativeMethodParameterDeclarations(
FunctionDef $function,
?int $parameterCount = null,
): string
{
$args = [];
$arguments = $parameterCount === null
? $function->argInfoList
: array_slice($function->argInfoList, 0, $parameterCount);
foreach ($arguments as $argument) {
if ($argument->variadic) {
$declaration = Type::ARRAY . ' ' . $argument->name;
} else {
$declaration = $this->genArgumentDeclaration($argument);
}
$args[] = $declaration;
}
return implode(', ', $args);
}
/**
* C++ binds a default argument from the receiver's static type, while PHP
* uses the default declared by the dynamically selected override. Emit an
* overload for every positional arity instead of putting C++ defaults on
* a virtual declaration. Each override adapter can then call its concrete
* php_* function with the supplied prefix and let that declaration provide
* the correct dynamic defaults.
*
* @return list<int>
*/
protected function getNativeVirtualMethodArities(FunctionDef $function): array
{
$total = count($function->argInfoList);
return range(min($function->argCountRequired, $total), $total);
}
protected function getNativeObjectPropertyType(PropertyDef $property): string
{
if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) {
return $this->getNativeObjectPointerType($property->class);
}
return match ($property->type) {
// These language values use PHPX's boxed Variant ABI. Embedding
// the implementation classes by value would be incompatible with
// every arithmetic/conversion helper, all of which accepts and
// returns Variant while preserving immutable value semantics.
Type::STREAM, Type::BOX, Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL => Type::VAR,
default => $property->type,
};
}
protected function getNativeObjectInitializerName(string|ClassDef $class): string
{
// Keep compiler-owned helpers outside the php_* user symbol namespace.
// A PHP method named initialize() previously collided with
// php_<class>__initialize and produced duplicate C++ definitions.
return 'typephp_native_initialize_fields__' . $this->getNativeObjectCppName($class);
}
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, ...$class->abstractMethodDefs] as $method) {
foreach ($this->getNativeVirtualMethodSlots($class, $method) as [$slotClass, $slotMethod]) {
$ownsSlot = $this->isSameClassName(
$slotClass->getNamespacedName(false),
$class->getNamespacedName(false),
);
foreach ($this->getNativeVirtualMethodArities($slotMethod->functionDef) as $arity) {
$code .= ' virtual ' . $this->getNativeMethodReturnCppType($slotMethod->functionDef)
. ' ' . $this->getNativeVirtualMethodName($slotClass, $method->name) . '('
. $this->getNativeMethodParameterDeclarations($slotMethod->functionDef, $arity) . ')'
. ($ownsSlot ? '' : ' override')
. (($method->flags & Modifiers::ABSTRACT) ? ' = 0' : '')
. ';' . PHP_EOL;
}
}
}
if ($this->nativeObjectUsesVirtualClone($class)) {
$code .= ' virtual ' . $name . ' *' . self::NATIVE_VIRTUAL_CLONE_METHOD . '() const';
if ($class->extends !== '' && $this->isNativeObjectClass($class->extends)) {
$code .= ' override';
}
if ($class->isAbstract()) {
$code .= ' = 0';
}
$code .= ';' . PHP_EOL;
}
$code .= '};' . PHP_EOL;
$code .= 'void ' . $this->getNativeObjectInitializerName($class)
. '(' . $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 ' . $this->getNativeObjectInitializerName($class)
. '(' . $cpp . ' &this_) {' . PHP_EOL;
if ($class->extends !== '' && $this->isNativeObjectClass($class->extends)) {
$code .= ' ' . $this->getNativeObjectInitializerName($class->extends)
. '(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) {
$nativeFunction = self::PREFIX . $this->getNativeName(
$method->name,
$class->namespace,
$class->name,
);
foreach ($this->getNativeVirtualMethodSlots($class, $method) as [$slotClass, $slotMethod]) {
$slotFunction = $slotMethod->functionDef;
$returnType = $this->getNativeMethodReturnCppType($slotFunction);
foreach ($this->getNativeVirtualMethodArities($slotFunction) as $arity) {
$args = array_map(
static fn (ArgInfo $arg): string => $arg->name,
array_slice($slotFunction->argInfoList, 0, $arity),
);
$code .= $returnType . ' ' . $cpp . '::'
. $this->getNativeVirtualMethodName($slotClass, $method->name)
. '(' . $this->getNativeMethodParameterDeclarations($slotFunction, $arity) . ') {' . PHP_EOL;
$call = $nativeFunction . '(*this' . ($args === [] ? '' : ', ' . implode(', ', $args)) . ')';
$code .= ' ' . ($returnType === Type::VOID ? '' : 'return ') . $call . ';' . PHP_EOL;
$code .= '}' . PHP_EOL . PHP_EOL;
}
}
}
if ($this->nativeObjectUsesVirtualClone($class) && !$class->isAbstract()) {
$initializer = '';
$cloneMethod = $this->findNativeObjectMethod($class->getNamespacedName(false), '__clone');
if ($cloneMethod !== null) {
$declaringClass = $this->getClass($cloneMethod->functionDef->declaringClass);
$clone = self::PREFIX . $this->getNativeName(
'__clone',
$declaringClass->namespace,
$declaringClass->name,
);
$initializer = $clone . '(this_); ';
}
$code .= $cpp . ' *' . $cpp . '::' . self::NATIVE_VIRTUAL_CLONE_METHOD . '() const {' . PHP_EOL;
$code .= ' return php::nativeClone<' . $cpp . '>('
. $this->getNativeObjectDescriptorName($class) . ', *this, '
. '[&](auto &this_) { ' . $initializer . '});' . 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;
if (count($destructors) === 1) {
[$destructor, $destructorCpp] = $destructors[0];
$code .= ' ' . $destructor
. '(*static_cast<' . $destructorCpp . ' *>(object));' . PHP_EOL;
} else {
$code .= ' php::NativeFinalizerChain chain;' . PHP_EOL;
foreach ($destructors as [$destructor, $destructorCpp]) {
$code .= ' chain.run([&] { ' . $destructor
. '(*static_cast<' . $destructorCpp . ' *>(object)); });' . PHP_EOL;
}
$code .= ' chain.rethrow();' . PHP_EOL;
}
$code .= '}' . PHP_EOL;
}
$code .= 'static void ' . $prefix . '_destroy(void *object) noexcept {' . PHP_EOL;
$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;
}
}