Native Class 支持 [第八阶段]

master
韩天峰 1 week ago
parent 587818d5f9
commit c9666664aa
  1. 11
      phpunit/code/native-class-dynamic-class-expression.php
  2. 7
      phpunit/code/native-class-global-forward/a.php
  3. 22
      phpunit/code/native-class-global-forward/b.php
  4. 30
      phpunit/src/NativeClass/NativeClassValidationTest.php
  5. 4
      src/Build/SourcePipelineTrait.php
  6. 4
      src/CompilerBase.php
  7. 18
      src/Context/CompilationStateTrait.php
  8. 392
      src/NativeClass/NativeGlobalDiscovery.php
  9. 130
      src/Preprocessor.php
  10. 82
      tests/compiler/native-class/clone-phpx-fields.phpt
  11. 60
      tests/compiler/native-class/failed-clone-finalizer.phpt
  12. 18
      tests/compiler/native-class/global-and-static.phpt

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

@ -0,0 +1,7 @@
<?php
function readNativeForwardGlobal(): int
{
global $nativeForwardGlobal;
return $nativeForwardGlobal->value;
}

@ -0,0 +1,22 @@
<?php
#[Native]
class NativeForwardGlobalValue
{
public int $value = 42;
}
class NativeForwardGlobalFactory
{
public static function create(): NativeForwardGlobalValue
{
return new NativeForwardGlobalValue();
}
public static function initialize(): void
{
global $nativeForwardGlobal;
$local = self::create();
$nativeForwardGlobal = $local;
}
}

@ -25,6 +25,31 @@ final class NativeClassValidationTest extends \BaseTest
$this->addToAssertionCount(1);
}
public function testDiscoversNativeGlobalSlotBeforeEarlierReaderIsConverted(): void
{
global $translator;
$compiler = \TypePhp\CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$directory = dirname(__DIR__, 2) . '/code/native-class-global-forward';
$files = [$directory . '/a.php', $directory . '/b.php'];
$compiler->discoverNativeClassDeclarations($files);
foreach ($files as $file) {
$compiler->prepareFile($file);
}
$compiler->discoverNativeGlobalObjects($files);
$reader = $compiler->convertFile($files[0]);
$compiler->convertFile($files[1]);
$code = file_get_contents($reader);
self::assertIsString($code);
self::assertStringContainsString(
'php::nativeDeref(nativeForwardGlobal, "NativeForwardGlobalValue").value',
$code,
);
self::assertStringNotContainsString('nativeForwardGlobal.attr(', $code);
}
public function testRejectsNativeAttributeOnInterface(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
@ -241,6 +266,11 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-dynamic-new.php');
}
public function testLeavesDynamicClassExpressionsToTheOrdinaryPhpPath(): void
{
$this->compile('native-class-dynamic-class-expression.php');
}
public function testRejectsNativeObjectAsDynamicStaticCallTarget(): void
{
$this->expectException(TestError::class);

@ -150,6 +150,10 @@ trait SourcePipelineTrait
}
}
}
// Global slots are shared by every translation unit. Fix any Native
// pointer ABI now, after declarations are known and before the first
// per-file C++ body is generated.
$this->discoverNativeGlobalObjects(array_values($files));
$files = $this->getSortedFiles($files);
return $files;
}

@ -3780,6 +3780,10 @@ class CompilerBase implements PropertyAccessContext
if ($nativeCtor === false) {
$this->fatalError($expr, "Native constructor `{$className}::__construct()` cannot be resolved");
}
// Keep the generated C++ argument text separate from
// the AST argument array used by the ordinary-class
// path below. The self-hosted compiler assigns one
// fixed C++ type to each PHP local variable.
$nativeArgs = $expr->args === []
? ''
: ', ' . $this->parseNativeCallArgs($expr->args, $nativeCtor);

@ -91,6 +91,22 @@ trait CompilationStateTrait
} else {
return;
}
$class = $this->registerNativeGlobalObject($slot, $class, $node);
$this->addNativeObject($name, $class);
}
/**
* Fix the C++ pointer ABI of a project-wide global/static slot.
*
* The project discovery pass calls this before C++ emission; the ordinary
* convert path calls it again to validate every concrete assignment.
*/
protected function registerNativeGlobalObject(
string $slot,
string $class,
?NodeAbstract $node = null,
): string {
$class = ltrim($class, '\\');
if (isset($this->nativeGlobalObjects[$slot])) {
$existing = $this->nativeGlobalObjects[$slot];
if (!$this->isObjectClassStaticallyAssignableTo($class, $existing)) {
@ -106,7 +122,7 @@ trait CompilationStateTrait
}
$this->globalVars[$slot] = $this->getNativeObjectPointerType($class);
$this->nativeGlobalObjects[$slot] = $class;
$this->addNativeObject($name, $class);
return $class;
}
protected function addScopeGlobalVar(string $name, string $type): void

@ -0,0 +1,392 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\NativeClass;
use PhpParser\Node;
use PhpParser\NodeAbstract;
/**
* Discovers project-wide global slots which carry Native Object pointers.
*
* C++ files are emitted one at a time, while a PHP global can be written in a
* later file. The slot ABI must therefore be known before the first file is
* converted. This deliberately small, flow-local analysis only tracks Native
* pointer identities; ordinary PHP value inference remains in the compiler's
* normal convert phase.
*/
final class NativeGlobalDiscovery
{
/** @var \Closure(string): ?string */
private \Closure $canonicalClass;
/** @var \Closure(string): ?string */
private \Closure $nativeClass;
/** @var \Closure(list<string>): ?string */
private \Closure $functionReturn;
/** @var \Closure(string, string): ?string */
private \Closure $methodReturn;
/** @var \Closure(string, string): ?string */
private \Closure $propertyClass;
/** @var \Closure(string, string): ?string */
private \Closure $commonClass;
/** @var \Closure(string): ?string */
private \Closure $parentClass;
private string $scopeClass = '';
/**
* @param \Closure(string): ?string $canonicalClass
* @param \Closure(string): ?string $nativeClass
* @param \Closure(list<string>): ?string $functionReturn
* @param \Closure(string, string): ?string $methodReturn
* @param \Closure(string, string): ?string $propertyClass
* @param \Closure(string, string): ?string $commonClass
* @param \Closure(string): ?string $parentClass
*/
public function __construct(
\Closure $canonicalClass,
\Closure $nativeClass,
\Closure $functionReturn,
\Closure $methodReturn,
\Closure $propertyClass,
\Closure $commonClass,
\Closure $parentClass,
) {
$this->canonicalClass = $canonicalClass;
$this->nativeClass = $nativeClass;
$this->functionReturn = $functionReturn;
$this->methodReturn = $methodReturn;
$this->propertyClass = $propertyClass;
$this->commonClass = $commonClass;
$this->parentClass = $parentClass;
}
/**
* @param list<Node\Stmt> $statements
* @return list<array{name: string, class: string, node: NodeAbstract}>
*/
public function discover(array $statements): array
{
$result = [];
$this->discoverStatements($statements, '', $result);
return $result;
}
/**
* @param list<Node\Stmt> $statements
* @param list<array{name: string, class: string, node: NodeAbstract}> $result
*/
private function discoverStatements(array $statements, string $class, array &$result): void
{
foreach ($statements as $statement) {
if ($statement instanceof Node\Stmt\Namespace_) {
$this->discoverStatements($statement->stmts, '', $result);
continue;
}
if ($statement instanceof Node\Stmt\Function_) {
$this->discoverFunction($statement, '', $result);
continue;
}
if ($statement instanceof Node\Stmt\ClassLike) {
$className = isset($statement->namespacedName)
? $statement->namespacedName->toString()
: ($statement->name?->toString() ?? '');
foreach ($statement->getMethods() as $method) {
$this->discoverFunction($method, $className, $result);
}
}
}
}
/**
* @param list<array{name: string, class: string, node: NodeAbstract}> $result
*/
private function discoverFunction(
Node\Stmt\Function_|Node\Stmt\ClassMethod $function,
string $class,
array &$result,
): void {
if ($function->stmts === null) {
return;
}
$globals = [];
$this->collectGlobals($function->stmts, $globals);
if ($globals === [] && !$this->containsGlobalsArray($function->stmts)) {
return;
}
$locals = [];
$previousScope = $this->scopeClass;
$thisClass = ($this->canonicalClass)($class);
$this->scopeClass = $thisClass ?? '';
if ($thisClass !== null) {
$locals['this'] = $thisClass;
}
foreach ($function->params as $parameter) {
if (!$parameter->var instanceof Node\Expr\Variable || !is_string($parameter->var->name)) {
continue;
}
$parameterClass = $this->classFromType($parameter->type);
if ($parameterClass !== null) {
$locals[$parameter->var->name] = $parameterClass;
}
}
$this->analyzeNodes($function->stmts, $globals, $locals, $result);
$this->scopeClass = $previousScope;
}
/** @param array<string, true> $globals */
private function collectGlobals(mixed $node, array &$globals): void
{
if ($node === null) {
return;
}
if (is_array($node)) {
foreach ($node as $item) {
$this->collectGlobals($item, $globals);
}
return;
}
if (!$node instanceof NodeAbstract || $node instanceof Node\FunctionLike) {
return;
}
if ($node instanceof Node\Stmt\Global_) {
foreach ($node->vars as $variable) {
if ($variable instanceof Node\Expr\Variable && is_string($variable->name)) {
$globals[$variable->name] = true;
}
}
return;
}
foreach ($node->getSubNodeNames() as $name) {
$this->collectGlobals($node->{$name}, $globals);
}
}
private function containsGlobalsArray(mixed $node): bool
{
if ($node === null) {
return false;
}
if (is_array($node)) {
foreach ($node as $item) {
if ($this->containsGlobalsArray($item)) {
return true;
}
}
return false;
}
if (!$node instanceof NodeAbstract || $node instanceof Node\FunctionLike) {
return false;
}
if ($this->globalArrayName($node) !== null) {
return true;
}
foreach ($node->getSubNodeNames() as $name) {
if ($this->containsGlobalsArray($node->{$name})) {
return true;
}
}
return false;
}
/**
* @param array<string, true> $globals
* @param array<string, string> $locals
* @param list<array{name: string, class: string, node: NodeAbstract}> $result
*/
private function analyzeNodes(
mixed $node,
array $globals,
array &$locals,
array &$result,
): void {
if ($node === null) {
return;
}
if (is_array($node)) {
foreach ($node as $item) {
$this->analyzeNodes($item, $globals, $locals, $result);
}
return;
}
if (!$node instanceof NodeAbstract || $node instanceof Node\FunctionLike) {
return;
}
if ($node instanceof Node\Expr\Assign) {
$this->analyzeNodes($node->expr, $globals, $locals, $result);
$class = $this->inferClass($node->expr, $locals);
$globalName = $this->assignmentGlobalName($node->var, $globals);
$nativeClass = $class === null ? null : ($this->nativeClass)($class);
if ($globalName !== null && $nativeClass !== null) {
$result[] = ['name' => $globalName, 'class' => $nativeClass, 'node' => $node];
} elseif ($node->var instanceof Node\Expr\Variable && is_string($node->var->name)) {
if ($class !== null) {
$locals[$node->var->name] = $class;
} elseif (!$this->isNull($node->expr)) {
unset($locals[$node->var->name]);
}
}
$this->analyzeNodes($node->var, $globals, $locals, $result);
return;
}
foreach ($node->getSubNodeNames() as $name) {
$this->analyzeNodes($node->{$name}, $globals, $locals, $result);
}
}
/** @param array<string, string> $locals */
private function inferClass(NodeAbstract $expression, array $locals): ?string
{
if ($expression instanceof Node\Expr\Assign) {
return $this->inferClass($expression->expr, $locals);
}
if ($expression instanceof Node\Expr\New_ && $expression->class instanceof Node\Name) {
return $this->resolvedClass($expression->class);
}
if ($expression instanceof Node\Expr\Variable && is_string($expression->name)) {
return $locals[$expression->name] ?? null;
}
if ($expression instanceof Node\Expr\Clone_
|| $expression instanceof Node\Expr\ErrorSuppress
) {
return $this->inferClass($expression->expr, $locals);
}
if ($expression instanceof Node\Expr\FuncCall && $expression->name instanceof Node\Name) {
return ($this->functionReturn)($this->resolvedNameCandidates($expression->name));
}
if ($expression instanceof Node\Expr\StaticCall
&& $expression->class instanceof Node\Name
&& $expression->name instanceof Node\Identifier
) {
$class = $this->resolvedClass($expression->class);
return $class === null ? null : ($this->methodReturn)($class, $expression->name->toString());
}
if ($expression instanceof Node\Expr\MethodCall
&& $expression->name instanceof Node\Identifier
) {
$class = $this->inferClass($expression->var, $locals);
return $class === null ? null : ($this->methodReturn)($class, $expression->name->toString());
}
if ($expression instanceof Node\Expr\PropertyFetch
&& $expression->name instanceof Node\Identifier
) {
$class = $this->inferClass($expression->var, $locals);
return $class === null ? null : ($this->propertyClass)($class, $expression->name->toString());
}
if ($expression instanceof Node\Expr\Ternary) {
$if = $expression->if === null
? $this->inferClass($expression->cond, $locals)
: $this->inferClass($expression->if, $locals);
return $this->mergeClasses($if, $this->inferClass($expression->else, $locals));
}
if ($expression instanceof Node\Expr\BinaryOp\Coalesce) {
return $this->mergeClasses(
$this->inferClass($expression->left, $locals),
$this->inferClass($expression->right, $locals),
);
}
if ($expression instanceof Node\Expr\Match_) {
$class = null;
foreach ($expression->arms as $arm) {
$class = $this->mergeClasses($class, $this->inferClass($arm->body, $locals));
}
return $class;
}
return null;
}
private function mergeClasses(?string $left, ?string $right): ?string
{
if ($left === null) {
return $right;
}
if ($right === null) {
return $left;
}
return ($this->commonClass)($left, $right);
}
private function classFromType(?NodeAbstract $type): ?string
{
if ($type instanceof Node\NullableType) {
$type = $type->type;
}
return $type instanceof Node\Name
? $this->resolvedClass($type)
: null;
}
/** @param array<string, true> $globals */
private function assignmentGlobalName(NodeAbstract $target, array $globals): ?string
{
if ($target instanceof Node\Expr\Variable
&& is_string($target->name)
&& isset($globals[$target->name])
) {
return $target->name;
}
return $this->globalArrayName($target);
}
private function globalArrayName(NodeAbstract $node): ?string
{
if (!$node instanceof Node\Expr\ArrayDimFetch
|| !$node->var instanceof Node\Expr\Variable
|| $node->var->name !== 'GLOBALS'
|| !$node->dim instanceof Node\Scalar\String_
) {
return null;
}
return $node->dim->value;
}
private function isNull(NodeAbstract $expression): bool
{
return $expression instanceof Node\Expr\ConstFetch
&& strtolower($expression->name->toString()) === 'null';
}
private function resolvedName(Node\Name $name): string
{
$resolved = $name->getAttribute('resolvedName');
return ltrim($resolved instanceof Node\Name ? $resolved->toString() : $name->toString(), '\\');
}
private function resolvedClass(Node\Name $name): ?string
{
$keyword = strtolower($name->toString());
if ($keyword === 'self' || $keyword === 'static') {
return $this->scopeClass !== '' ? $this->scopeClass : null;
}
if ($keyword === 'parent') {
return $this->scopeClass !== '' ? ($this->parentClass)($this->scopeClass) : null;
}
return ($this->canonicalClass)($this->resolvedName($name));
}
/** @return list<string> */
private function resolvedNameCandidates(Node\Name $name): array
{
$names = [$this->resolvedName($name)];
$fallback = $name->getAttribute('fallbackName');
if ($fallback instanceof Node\Name) {
$names[] = ltrim($fallback->toString(), '\\');
}
$names[] = ltrim($name->toString(), '\\');
return array_values(array_unique($names));
}
}

@ -30,6 +30,7 @@ use TypePhp\Transform\FunctionAttributeLowering;
use TypePhp\Transform\ConstantExpressionValidationVisitor;
use TypePhp\Transform\RuntimeAttributeFactoryLowering;
use TypePhp\Transform\Visitor;
use TypePhp\NativeClass\NativeGlobalDiscovery;
use PhpParser\Modifiers;
use PhpParser\ConstExprEvaluator;
use PhpParser\Node;
@ -106,6 +107,135 @@ class Preprocessor extends CompilerBase
}
}
/**
* Discover Native Object globals after all declarations have been prepared
* but before the first translation unit is emitted.
*
* @param list<string> $files
*/
public function discoverNativeGlobalObjects(array $files): void
{
$hasNativeClass = false;
foreach ($this->symbols->classes() as $class) {
if ($class->nativeObject) {
$hasNativeClass = true;
break;
}
}
if (!$hasNativeClass) {
return;
}
$functionReturns = [];
foreach ($this->symbols->functions() as $function) {
if (!$function->method && $this->hasClass($function->returnClass)) {
$functionReturns[strtolower(ltrim($function->getNamespacedName(), '\\'))]
= $this->getClass($function->returnClass)->getNamespacedName(false);
}
}
$discovery = new NativeGlobalDiscovery(
function (string $class): ?string {
$class = ltrim($class, '\\');
if (!$this->hasClass($class)) {
return null;
}
return $this->getClass($class)->getNamespacedName(false);
},
function (string $class): ?string {
$class = ltrim($class, '\\');
if (!$this->isNativeObjectClass($class)) {
return null;
}
return $this->getClass($class)->getNamespacedName(false);
},
static function (array $names) use ($functionReturns): ?string {
foreach ($names as $name) {
$key = strtolower(ltrim($name, '\\'));
if (isset($functionReturns[$key])) {
return $functionReturns[$key];
}
}
return null;
},
function (string $class, string $method): ?string {
while ($this->hasClass($class)) {
$classDefinition = $this->getClass($class);
if ($classDefinition->hasMethod($method)) {
$returnClass = $classDefinition->getMethod($method)->functionDef->returnClass;
return $this->hasClass($returnClass)
? $this->getClass($returnClass)->getNamespacedName(false)
: null;
}
$class = $classDefinition->extends;
}
return null;
},
function (string $class, string $property): ?string {
while ($this->hasClass($class)) {
$classDefinition = $this->getClass($class);
if ($classDefinition->hasProperty($property)) {
$propertyClass = $classDefinition->getProperty($property)->class;
return $this->hasClass($propertyClass)
? $this->getClass($propertyClass)->getNamespacedName(false)
: null;
}
$class = $classDefinition->extends;
}
return null;
},
function (string $left, string $right): ?string {
if ($this->isObjectClassStaticallyAssignableTo($left, $right)) {
return $right;
}
if ($this->isObjectClassStaticallyAssignableTo($right, $left)) {
return $left;
}
return null;
},
function (string $class): ?string {
if (!$this->hasClass($class)) {
return null;
}
$parent = $this->getClass($class)->extends;
return $this->hasClass($parent)
? $this->getClass($parent)->getNamespacedName(false)
: null;
},
);
foreach ($files as $file) {
if (!$this->isPhpFileForNativeDiscovery($file)) {
continue;
}
$source = file_get_contents($file);
if (!is_string($source)
|| (!str_contains($source, 'global') && !str_contains($source, '$GLOBALS'))
) {
continue;
}
try {
$ast = $this->parser->parse($source);
} catch (\PhpParser\Error) {
// prepareFile() has already emitted the authoritative syntax
// diagnostic. This pass must not report it a second time.
continue;
}
$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
$ast = $traverser->traverse($ast);
foreach ($discovery->discover($ast) as $slot) {
$this->registerNativeGlobalObject($slot['name'], $slot['class'], $slot['node']);
}
}
// The resolver closures are bound to the Translator. Release the
// short-lived discovery object before returning so an embedded,
// self-hosted compiler never keeps that object graph alive until
// php_embed_shutdown().
unset($discovery);
}
public function getSortedFiles(array $list): array
{
$sorter = new StringSort();

@ -0,0 +1,82 @@
--TEST--
Native class: clone preserves PHPX field semantics and traces shared native children
--FILE--
<?php
#[Native]
class NativeCloneChild
{
public string $name = 'child';
}
#[Native]
class NativeCloneFields
{
public string $name = 'source';
public array $values = [1];
public object $object;
public Stream $stream;
public mixed $mixed = null;
public NativeCloneChild $child;
}
function createClone(): NativeCloneFields
{
$source = new NativeCloneFields();
$source->object = (object) ['value' => 1];
$source->stream = fopen('php://memory', 'w+');
$source->mixed = ['mixed'];
$source->child = new NativeCloneChild();
$copy = clone $source;
$copy->name = 'copy';
$copy->values[] = 2;
$copy->object->value = 2;
fwrite($copy->stream, 'shared');
$copy->mixed[] = 'copy';
// Allocate enough garbage to exercise tracing of the cloned Native child.
for ($i = 0; $i < 10000; $i++) {
new NativeCloneChild();
}
var_dump($source->name, $source->values, $source->object->value, $source->mixed);
rewind($source->stream);
echo stream_get_contents($source->stream), "\n";
echo $copy->child->name, "\n";
return $copy;
}
function main(): void
{
$copy = createClone();
var_dump($copy->name, $copy->values, $copy->object->value, $copy->mixed);
}
?>
--EXPECT--
string(6) "source"
array(1) {
[0]=>
int(1)
}
int(2)
array(1) {
[0]=>
string(5) "mixed"
}
shared
child
string(4) "copy"
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
int(2)
array(2) {
[0]=>
string(5) "mixed"
[1]=>
string(4) "copy"
}

@ -0,0 +1,60 @@
--TEST--
Native class: an unreachable clone whose __clone throws is finalized by the GC
--FILE--
<?php
#[Native]
class NativeFailedCloneFinalizer
{
public string $kind = 'source';
public function __clone(): void
{
$this->kind = 'clone';
throw new RuntimeException('clone failed');
}
public function __destruct()
{
global $finalizedKinds;
$finalizedKinds[] = $this->kind;
}
}
#[Native]
class NativeFailedClonePressure
{
public int $value;
}
function forceFailedCloneCollection(): void
{
for ($i = 0; $i < 400000; $i++) {
new NativeFailedClonePressure();
}
}
function main(): void
{
global $finalizedKinds;
$finalizedKinds = [];
$source = new NativeFailedCloneFinalizer();
try {
clone $source;
} catch (RuntimeException $error) {
echo $error->getMessage(), "\n";
}
forceFailedCloneCollection();
var_dump($finalizedKinds);
echo $source->kind, "\n";
}
?>
--EXPECT--
clone failed
array(1) {
[0]=>
string(5) "clone"
}
source

@ -32,6 +32,18 @@ function replaceGlobalWithChild(): void
$nativeGlobal->value = 41;
}
function clearGlobal(): void
{
global $nativeGlobal;
$nativeGlobal = null;
}
function globalIsNull(): bool
{
global $nativeGlobal;
return is_null($nativeGlobal);
}
function nextStatic(): int
{
static $counter = new NativeCounter();
@ -44,6 +56,10 @@ function main(): void
var_dump(readGlobal());
replaceGlobalWithChild();
var_dump(readGlobal());
clearGlobal();
var_dump(globalIsNull());
replaceGlobalWithChild();
var_dump(readGlobal());
var_dump(nextStatic());
var_dump(nextStatic());
}
@ -51,5 +67,7 @@ function main(): void
--EXPECT--
int(40)
int(41)
bool(true)
int(41)
int(1)
int(2)

Loading…
Cancel
Save