Native Class 支持 [完结]

master
韩天峰 1 week ago
parent 2e6ddf98f1
commit 2ffd4ded19
  1. 11
      docs/NATIVE_CLASS_IMPLEMENTATION_AUDIT.md
  2. 5
      docs/NATIVE_CLASS_OBJECT.md
  3. 12
      phpunit/code/native-class-dynamic-globals-write.php
  4. 6
      phpunit/code/native-class-global-forward/a.php
  5. 10
      phpunit/code/native-class-global-forward/b.php
  6. 13
      phpunit/src/NativeClass/NativeClassValidationTest.php
  7. 5
      src/CompilerBase.php
  8. 65
      src/NativeClass/NativeClassSupportTrait.php
  9. 101
      src/NativeClass/NativeGlobalDiscovery.php
  10. 184
      src/NativeClass/NativeGlobalTypeResolver.php
  11. 5
      src/Parser/ArrayExpressionTrait.php
  12. 13
      src/Parser/AssignOpTrait.php
  13. 3
      src/Preprocessor.php
  14. 5
      src/Resolver/DeclarationSymbolTrait.php
  15. 7
      src/Translator.php
  16. 7
      src/gen_stub.php
  17. 12
      tests/compiler/native-class/global-and-static.phpt

@ -25,7 +25,7 @@
| 静态可解析的 `new NativeClass()` 使用 Native Heap | `CompilerBase::parseNew()`、`php::nativeConstruct()` | `basic.phpt`、`construction-gc-roots.phpt` | 已验证 |
| `new (表达式)()` 保持普通 PHP 动态实例化 | `parseNew()` 只对 `Node\\Name` 进入 Native 分支 | `testLeavesDynamicClassExpressionsToTheOrdinaryPhpPath` | 已验证 |
| Native 对象本身不能充当动态 class target | `assertNotNativeObjectDynamicClassTarget()` | dynamic new/static call/class constant 负向测试 | 已验证 |
| 所有不支持的用法在编译期终止 | Native 边界检查、类型兼容检查 | 128`NativeClassValidationTest` | 已验证 |
| 所有不支持的用法在编译期终止 | Native 边界检查、类型兼容检查 | 131`NativeClassValidationTest` | 已验证 |
## 3. 属性与固定布局
@ -113,7 +113,8 @@
|---|---|---|---|
| Native class 前向声明不依赖文件顺序 | declaration discovery pre-pass | `testDiscoversNativeTypesBeforeCrossFileSignaturePreprocessing` | 已验证 |
| global Native slot ABI 在任一 C++ 文件生成前确定 | `NativeGlobalDiscovery`、`NativeGlobalTypeResolver` | `testDiscoversNativeGlobalSlotBeforeEarlierReaderIsConverted`,实际双文件构建 | 已验证 |
| `global $slot` 与字面量 `$GLOBALS['slot']` 使用同一 Native root slot | literal global slot lowering、request root registration | `global-and-static.phpt`、跨文件 `$GLOBALS` fixture | 已验证 |
| `global $slot` 与静态可解析的 `$GLOBALS[...]` 使用同一 Native root slot | literal/constant global slot lowering、request root registration | `global-and-static.phpt`、跨文件 Closure/常量 `$GLOBALS` fixture | 已验证 |
| 动态 `$GLOBALS[$key]` 不得承载 Native Object | dynamic Zend boundary validation | `testRejectsNativeObjectStoredThroughDynamicGlobalsKey` | 已验证 |
| global slot 固定首个 Native 类型,只允许子类或 null | global registration/type validation | `global-and-static.phpt`、global type change 负向测试 | 已验证 |
| 未使用 Native Class 的项目跳过 Native global pre-pass | `discoverNativeGlobalObjects()` fast return | 源码检查、全量 PHPUnit | 已验证 |
@ -126,6 +127,6 @@ vendor/bin/phpunit phpunit/src/NativeClass/NativeClassValidationTest.php
--gtest_filter='wren_gc.*:native_gc.*'
```
本次结果分别为:69/69 PHPT、128/128 PHPUnit、17/17 PHPX C++ tests。
最终合入前仍需执行编译器完整 PHPUnit、完整 PHPT 和 PHPX 完整测试,防止 Native
分支的公共 hook 影响普通对象模型。
本次结果分别为:69/69 PHPT、131/131 PHPUnit、17/17 PHPX C++ tests。
完整回归结果为:编译器 PHPUnit 1431/1431、编译器 PHPT 1037/1037(另有 2 项按
环境跳过)、PHPX C++ tests 1016/1016。Native 分支的公共 hook 影响普通对象模型。

@ -507,7 +507,9 @@ struct FunctionNativeRoots {
- 只登记可能跨越 Native allocation 或显式 GC safe point 存活的变量。
- 普通方法 receiver 由调用者的 root 或对象图保持存活,不重复登记。
- 临时对象如果跨越一次可能触发 GC 的调用,必须先写入 root slot。
- Native Class 允许保存在 TypePHP global 和 static local 中。这些槽不进入 Zend `$GLOBALS`,而是生成独立 Native 指针槽。
- Native Class 允许保存在 TypePHP global 和 static local 中。这些槽不注册到 Zend symbol table,而是生成独立 Native 指针槽。
- `global $slot``$GLOBALS['slot']` 使用同一个 Native 指针槽;`$GLOBALS` 的键也可以是编译期可求值为字符串的全局常量、类常量或常量表达式。
- `$GLOBALS[$dynamicKey]` 仍按 PHP 语法走 Zend HashTable。由于 Native Object 没有 zval 表示,动态键不能用于读写 Native global。
- ZTS 构建中的 global/static 指针槽和 static 初始化状态均使用 `THREAD_LOCAL`,不同线程之间不共享 Native 对象。
- RINIT 将这些槽登记为 request root;RSHUTDOWN 清空槽和初始化状态,随后由 Native Heap 统一 finalization 和回收。
@ -1289,6 +1291,7 @@ $json = json_encode($nativeObject->toArray());
| Native 元素 Std Container 转 PHP array/mixed 或作为 PHP 参数 | 不支持;裸指针不得越过 ZendVM value boundary |
| Native Class 属性循环引用 | 支持,指针字段加 Native tracing GC |
| TypePHP global/static local | 支持;ZTS 使用 thread-local request roots,RSHUTDOWN 清理 |
| `$GLOBALS` 访问 Native global | 字面量或编译期可求值的字符串常量映射到同一 C++ slot;动态键不支持 Native Object |
| global/static local 类型 | 第一次 Native 赋值固定 C++ slot 类型;后续可写入其 Native 子类或 null,不可改为基类/无关类 |
| Native Class 属性循环类型 | 支持;字段零值为 `nullptr`,类型图使用 C++ 前置声明 |
| late static binding / `new static()` | 不支持;Native Class 无运行时 `zend_class_entry`,使用 `self::`、`parent::` 或具体类名 |

@ -0,0 +1,12 @@
<?php
#[Native]
class NativeDynamicGlobalValue
{
public int $value = 1;
}
function writeNativeDynamicGlobal(string $name): void
{
$GLOBALS[$name] = new NativeDynamicGlobalValue();
}

@ -22,3 +22,9 @@ function readNativeForwardGlobalsArray(): int
{
return $GLOBALS['nativeForwardGlobalsArray']->value;
}
function readNativeForwardClosureGlobal(): int
{
global $nativeForwardClosureGlobal;
return $nativeForwardClosureGlobal->value;
}

@ -71,3 +71,13 @@ function initializeNativeForwardGlobalsArray(): void
{
$GLOBALS['nativeForwardGlobalsArray'] = new NativeForwardGlobalValue();
}
const NATIVE_FORWARD_CLOSURE_GLOBAL = 'nativeForwardClosureGlobal';
function initializeNativeForwardClosureGlobal(): void
{
$initialize = static function (): void {
$GLOBALS[NATIVE_FORWARD_CLOSURE_GLOBAL] = new NativeForwardGlobalValue();
};
$initialize();
}

@ -63,6 +63,10 @@ final class NativeClassValidationTest extends \BaseTest
'/php::nativeDeref\\(tmp_var_\\d+, "NativeForwardGlobalValue"\\)\\.value/',
$code,
);
self::assertStringContainsString(
'php::nativeDeref(nativeForwardClosureGlobal, "NativeForwardGlobalValue").value',
$code,
);
self::assertStringNotContainsString('nativeForwardGlobal.attr(', $code);
}
@ -308,6 +312,15 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-php-array.php');
}
public function testRejectsNativeObjectStoredThroughDynamicGlobalsKey(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage(
'Native objects cannot be stored in PHP arrays, PHP object properties, static properties, or mixed variables',
);
$this->compile('native-class-dynamic-globals-write.php');
}
public function testRejectsNativeObjectStoredInPhpObjectProperty(): void
{
$this->expectException(TestError::class);

@ -83,6 +83,7 @@ use TypePhp\Symbol\SymbolRepository;
use TypePhp\TypeSystem\CompositeTypeCheckerTrait;
use TypePhp\TypeSystem\NativeTypeCompatibilityTrait;
use TypePhp\NativeClass\NativeClassSupportTrait;
use TypePhp\NativeClass\NativeGlobalTypeResolver;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\Node\ArrayItem;
@ -437,6 +438,8 @@ class CompilerBase implements PropertyAccessContext
protected array $globalVars = [];
/** @var array<string, string> Global/static Native pointer slot => class name. */
protected array $nativeGlobalObjects = [];
/** Immutable metadata shared by Native global pre-discovery and lowering. */
protected ?NativeGlobalTypeResolver $nativeGlobalTypeResolver = null;
/** @var array<string, string> Lowercase class name => declared Native class name. */
protected array $nativeClassDeclarations = [];
/** @var array<string, true> Request-reset initialization flags for Native static locals. */
@ -1974,7 +1977,7 @@ class CompilerBase implements PropertyAccessContext
return $this->getObjectType($object);
}
}
$globalSlot = $this->getLiteralGlobalsSlot($expr);
$globalSlot = $this->getStaticGlobalsSlot($expr);
if ($globalSlot !== null && isset($this->nativeGlobalObjects[$globalSlot])) {
return $this->nativeGlobalObjects[$globalSlot];
}

@ -375,6 +375,11 @@ trait NativeClassSupportTrait
}
}
/**
* 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
@ -387,6 +392,66 @@ trait NativeClassSupportTrait
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) {

@ -90,9 +90,6 @@ final class NativeGlobalDiscovery
$globals = [];
$this->collectGlobals($function->stmts, $globals);
if ($globals === [] && !$this->containsGlobalsArray($function->stmts)) {
return;
}
$locals = [];
$previousScope = $this->scopeClass;
@ -115,6 +112,51 @@ final class NativeGlobalDiscovery
$this->scopeClass = $previousScope;
}
/**
* Analyze a Closure as an independent PHP variable scope while retaining
* its lexical class scope. Native objects cannot be captured by a Zend
* Closure, but an ordinary captured object may still construct a Native
* object and a Closure may write a newly constructed object to a global.
*
* @param array<string, string> $outerLocals
* @param list<array{name: string, class: string, node: NodeAbstract}> $result
*/
private function discoverClosure(
Node\Expr\Closure $closure,
array $outerLocals,
array &$result,
): void {
if ($closure->stmts === null) {
return;
}
$globals = [];
$this->collectGlobals($closure->stmts, $globals);
$locals = [];
if (!$closure->static && isset($outerLocals['this'])) {
$locals['this'] = $outerLocals['this'];
}
foreach ($closure->uses as $use) {
if ($use->var instanceof Node\Expr\Variable
&& is_string($use->var->name)
&& isset($outerLocals[$use->var->name])
) {
$locals[$use->var->name] = $outerLocals[$use->var->name];
}
}
foreach ($closure->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($closure->stmts, $globals, $locals, $result);
}
/** @param array<string, true> $globals */
private function collectGlobals(mixed $node, array &$globals): void
{
@ -143,33 +185,6 @@ final class NativeGlobalDiscovery
}
}
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
@ -190,7 +205,27 @@ final class NativeGlobalDiscovery
}
return;
}
if (!$node instanceof NodeAbstract || $node instanceof Node\FunctionLike) {
if (!$node instanceof NodeAbstract) {
return;
}
if ($node instanceof Node\Expr\Closure) {
$this->discoverClosure($node, $locals, $result);
return;
}
if ($node instanceof Node\Stmt\Function_) {
$this->discoverFunction($node, '', $result);
return;
}
if ($node instanceof Node\Stmt\ClassLike) {
$className = isset($node->namespacedName)
? $node->namespacedName->toString()
: ($node->name?->toString() ?? '');
foreach ($node->getMethods() as $method) {
$this->discoverFunction($method, $className, $result);
}
return;
}
if ($node instanceof Node\FunctionLike) {
return;
}
if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp\Coalesce) {
@ -326,11 +361,11 @@ final class NativeGlobalDiscovery
if (!$node instanceof Node\Expr\ArrayDimFetch
|| !$node->var instanceof Node\Expr\Variable
|| $node->var->name !== 'GLOBALS'
|| !$node->dim instanceof Node\Scalar\String_
|| $node->dim === null
) {
return null;
}
return $node->dim->value;
return $this->resolver->staticString($node->dim, $this->scopeClass);
}
private function isNull(NodeAbstract $expression): bool

@ -8,6 +8,9 @@
namespace TypePhp\NativeClass;
use PhpParser\ConstExprEvaluator;
use PhpParser\Node;
use PhpParser\NodeAbstract;
use TypePhp\Entity\ClassDef;
/**
@ -22,6 +25,12 @@ final class NativeGlobalTypeResolver
/** @var array<string, string> */
private array $classes = [];
/** @var array<string, ClassDef> */
private array $classDefinitions = [];
/** @var array<string, Node\Expr> */
private array $globalConstantExpressions = [];
/** @var array<string, true> */
private array $nativeClasses = [];
@ -34,18 +43,30 @@ final class NativeGlobalTypeResolver
/** @var array<string, array<string, ?string>> */
private array $propertyClasses = [];
/** @param array<string, ClassDef> $classes */
public function __construct(array $classes)
/**
* @param array<string, ClassDef> $classes
* @param array<string, object> $constants
*/
public function __construct(array $classes, array $constants = [])
{
foreach ($classes as $class) {
$name = $class->getNamespacedName(false);
$key = strtolower(ltrim($name, '\\'));
$this->classes[$key] = $name;
$this->classDefinitions[$key] = $class;
if ($class->nativeObject) {
$this->nativeClasses[$key] = true;
}
}
foreach ($constants as $constant) {
if (isset($constant->name) && is_string($constant->name)
&& isset($constant->valueExpr) && $constant->valueExpr instanceof Node\Expr
) {
$this->globalConstantExpressions[ltrim($constant->name, '\\')] = $constant->valueExpr;
}
}
foreach ($classes as $class) {
$name = $class->getNamespacedName(false);
$key = strtolower(ltrim($name, '\\'));
@ -144,4 +165,163 @@ final class NativeGlobalTypeResolver
{
return $this->parents[strtolower(ltrim($class, '\\'))] ?? null;
}
/**
* Evaluate a PHP constant expression used as a `$GLOBALS[...]` key.
* Returning null keeps the expression on the ordinary dynamic Zend path.
*/
public function staticString(NodeAbstract $expression, string $scopeClass = ''): ?string
{
if ($expression instanceof Node\Scalar\String_) {
return $expression->value;
}
$visiting = [];
try {
$value = $this->evaluateConstantExpression($expression, $scopeClass, $visiting, 0);
} catch (\Throwable) {
return null;
}
return is_string($value) ? $value : null;
}
/** @param array<string, true> $visiting */
private function evaluateConstantExpression(
NodeAbstract $expression,
string $scopeClass,
array &$visiting,
int $depth,
): mixed {
if ($depth > 32 || !$expression instanceof Node\Expr) {
throw new \RuntimeException('Constant expression nesting is too deep');
}
$evaluator = new ConstExprEvaluator(function (Node\Expr $node) use (
$scopeClass,
&$visiting,
$depth,
): mixed {
if ($node instanceof Node\Expr\ConstFetch) {
foreach ($this->resolvedNameCandidates($node->name) as $name) {
$lower = strtolower($name);
if ($lower === 'true') {
return true;
}
if ($lower === 'false') {
return false;
}
if ($lower === 'null') {
return null;
}
if (isset($this->globalConstantExpressions[$name])) {
$key = 'global:' . $name;
if (isset($visiting[$key])) {
throw new \RuntimeException('Circular constant expression');
}
$visiting[$key] = true;
try {
return $this->evaluateConstantExpression(
$this->globalConstantExpressions[$name],
$scopeClass,
$visiting,
$depth + 1,
);
} finally {
unset($visiting[$key]);
}
}
if (defined($name)) {
return constant($name);
}
}
throw new \RuntimeException('Unresolved global constant');
}
if ($node instanceof Node\Expr\ClassConstFetch
&& $node->class instanceof Node\Name
&& $node->name instanceof Node\Identifier
) {
$class = $this->resolvedClassName($node->class, $scopeClass);
$constant = $node->name->toString();
if (strcasecmp($constant, 'class') === 0) {
return $class;
}
$classKey = strtolower(ltrim($class, '\\'));
while (isset($this->classDefinitions[$classKey])) {
$definition = $this->classDefinitions[$classKey];
if ($definition->hasConstant($constant)) {
$constantDefinition = $definition->getConstant($constant);
if (!$constantDefinition->valueExpr instanceof Node\Expr) {
throw new \RuntimeException('Class constant has no static expression');
}
$key = 'class:' . $classKey . '::' . $constant;
if (isset($visiting[$key])) {
throw new \RuntimeException('Circular class constant expression');
}
$visiting[$key] = true;
try {
return $this->evaluateConstantExpression(
$constantDefinition->valueExpr,
$definition->getNamespacedName(false),
$visiting,
$depth + 1,
);
} finally {
unset($visiting[$key]);
}
}
$parent = $this->parents[$classKey] ?? null;
if ($parent === null) {
break;
}
$classKey = strtolower($parent);
}
$runtimeConstant = $class . '::' . $constant;
if (defined($runtimeConstant)) {
return constant($runtimeConstant);
}
throw new \RuntimeException('Unresolved class constant');
}
throw new \RuntimeException('Unsupported constant expression');
});
return $evaluator->evaluateDirectly($expression);
}
private function resolvedClassName(Node\Name $name, string $scopeClass): string
{
$keyword = strtolower($name->toString());
if ($keyword === 'self' || $keyword === 'static') {
if ($scopeClass === '') {
throw new \RuntimeException('Class-relative constant outside class scope');
}
return ltrim($scopeClass, '\\');
}
if ($keyword === 'parent') {
$parent = $this->parentClass($scopeClass);
if ($parent === null) {
throw new \RuntimeException('Parent-relative constant without a parent');
}
return $parent;
}
$resolved = $name->getAttribute('resolvedName');
return ltrim($resolved instanceof Node\Name ? $resolved->toString() : $name->toString(), '\\');
}
/** @return list<string> */
private function resolvedNameCandidates(Node\Name $name): array
{
$names = [];
$resolved = $name->getAttribute('resolvedName');
if ($resolved instanceof Node\Name) {
$names[] = ltrim($resolved->toString(), '\\');
}
$fallback = $name->getAttribute('fallbackName');
if ($fallback instanceof Node\Name) {
$names[] = ltrim($fallback->toString(), '\\');
}
$names[] = ltrim($name->toString(), '\\');
return array_values(array_unique($names));
}
}

@ -90,8 +90,9 @@ trait ArrayExpressionTrait
if ($node->dim === null) {
$this->fatalError($node, 'Cannot use [] for GLOBALS');
}
if ($this->isScalarString($node->dim)) {
$name = $node->dim->value;
$staticSlot = $this->getStaticGlobalsSlot($node);
if ($staticSlot !== null) {
$name = $staticSlot;
if (!$this->hasGlobalVar($name)) {
$this->addGlobalVar($name, Type::VAR);
}

@ -275,7 +275,7 @@ trait AssignOpTrait
if ($allowed && ($this->hasScopeGlobalVar($leftName) || $this->hasStaticVar($leftName))) {
$this->promoteGlobalOrStaticToNativeObject($leftName, $rightClass, $right);
}
} elseif (($globalSlot = $this->getLiteralGlobalsSlot($left)) !== null) {
} elseif (($globalSlot = $this->getStaticGlobalsSlot($left)) !== null) {
if (!$this->hasGlobalVar($globalSlot)) {
$this->addGlobalVar($globalSlot, Type::VAR);
}
@ -1189,7 +1189,7 @@ trait AssignOpTrait
// declared as Variant because boxing the raw pointer would coerce it
// to bool. Other values retain the normal nullable Variant behavior.
$var = $this->isVarExpr($expr->var) ? $this->parseIdentifier($expr->var) : null;
$globalSlot = $this->getLiteralGlobalsSlot($expr->var);
$globalSlot = $this->getStaticGlobalsSlot($expr->var);
if ($globalSlot !== null) {
if (!$this->hasGlobalVar($globalSlot)) {
$this->addGlobalVar($globalSlot, Type::VAR);
@ -1201,6 +1201,15 @@ trait AssignOpTrait
$this->promoteGlobalOrStaticToNativeObject($globalSlot, $rightClass, $expr->expr);
}
$var = $globalSlot;
} elseif ($nativeRight
&& $expr->var instanceof Expr\ArrayDimFetch
&& $this->isVarExpr($expr->var->var)
&& $expr->var->var->name === 'GLOBALS'
) {
$this->fatalError(
$expr->var,
'Native objects cannot be stored in dynamically addressed `$GLOBALS`',
);
}
if ($var !== null && !$this->hasVar($var)) {
if ($nativeRight) {

@ -151,7 +151,8 @@ class Preprocessor extends CompilerBase
}
}
$resolver = new NativeGlobalTypeResolver($this->symbols->classes());
$resolver = new NativeGlobalTypeResolver($this->symbols->classes(), $this->constants);
$this->nativeGlobalTypeResolver = $resolver;
$discovery = new NativeGlobalDiscovery($resolver, $functionReturns);
foreach ($candidateSources as $source) {

@ -21,14 +21,15 @@ trait DeclarationSymbolTrait
if ($this->namespace) {
$name = $this->namespace . '\\' . $name;
}
$this->addConstant($name, $value);
$this->addConstant($name, $value, $const->value);
}
}
protected function addConstant(string $name, string $value): void
protected function addConstant(string $name, string $value, ?Node\Expr $valueExpr = null): void
{
$constInfo = new \stdClass();
$constInfo->value = $value;
$constInfo->valueExpr = $valueExpr;
$constInfo->type = $this->detectStrValueType($value);
$constInfo->namespace = $this->namespace;
$constInfo->name = $name;

@ -4500,7 +4500,12 @@ CODE;
private function checkInterfaceImplementation(NodeAbstract $node, ClassDef $classDef, string $interfaceName): void
{
if ($this->isInternalInterface($interfaceName)) {
$this->checkInternalInterfaceImplementation($node, $classDef, $interfaceName);
// Zend validates ordinary classes while registering their class
// entries. Only Native classes need the compiler-side reflection
// contract because they deliberately have no zend_class_entry.
if ($classDef->nativeObject) {
$this->checkInternalInterfaceImplementation($node, $classDef, $interfaceName);
}
return;
}
if (!$this->hasInterface($interfaceName)) {

@ -3351,14 +3351,9 @@ class PropertyInfo extends VariableLike
$template .= "zend_declare_property_ex(class_entry, $nameCode, &$zvalName, %s, $commentCode);\n";
}
$minimumFlagCompatibility = $this->phpVersionIdMinimumCompatibility;
if (($this->flags & (Modifiers::PRIVATE_SET | Modifiers::PROTECTED_SET))
&& ($minimumFlagCompatibility === null || $minimumFlagCompatibility >= PHP_84_VERSION_ID)) {
$minimumFlagCompatibility = PHP_82_VERSION_ID;
}
$code .= $this->getFlagsByPhpVersion()->generateVersionDependentFlagCode(
$template,
$minimumFlagCompatibility
$this->phpVersionIdMinimumCompatibility
);
$code .= $stringRelease;

@ -6,9 +6,13 @@ Native class: TypePHP globals and static locals retain request-rooted native obj
#[Native]
class NativeCounter
{
public const string GLOBAL_KEY = 'nativeGlobalsArray';
public int $value;
}
const NATIVE_GLOBAL_KEY = NativeCounter::GLOBAL_KEY;
#[Native]
class NativeCounterChild extends NativeCounter {}
@ -46,13 +50,13 @@ function globalIsNull(): bool
function initializeGlobalsArray(): void
{
$GLOBALS['nativeGlobalsArray'] ??= new NativeCounter();
$GLOBALS['nativeGlobalsArray']->value = 42;
$GLOBALS[NATIVE_GLOBAL_KEY] ??= new NativeCounter();
$GLOBALS[NativeCounter::GLOBAL_KEY]->value = 42;
}
function readGlobalsArray(): int
{
return $GLOBALS['nativeGlobalsArray']->value;
return $GLOBALS[NATIVE_GLOBAL_KEY]->value;
}
function resetGlobalsArray(): void
@ -62,7 +66,7 @@ function resetGlobalsArray(): void
function globalsArrayIsNull(): bool
{
return is_null($GLOBALS['nativeGlobalsArray']);
return is_null($GLOBALS[NativeCounter::GLOBAL_KEY]);
}
function nextStatic(): int

Loading…
Cancel
Save