diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index a927bcb9..bc5e0a55 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -63,6 +63,7 @@ class CompilerBase extends \PhpAot\Core\Translator public const string LITERAL_STRINGS = '_literal_strings'; public const string CLASS_MAP = 'class_map'; public const string FUNC_MAP = 'func_map'; + public const string PROP_MAP = 'property_map'; public const string EXPR_VARIABLE = 'Expr_Variable'; public const string EXPR_NEW = 'Expr_New'; @@ -93,7 +94,12 @@ class CompilerBase extends \PhpAot\Core\Translator */ protected array $classMap = []; protected int $funcIndex = 0; + /** + * @var array + */ protected array $funcMap = []; + protected int $propIndex = 0; + protected array $propMap = []; protected array $zendTypeMap = [ 'int' => self::TYPE_INT, 'float' => self::TYPE_FLOAT, @@ -673,11 +679,6 @@ class CompilerBase extends \PhpAot\Core\Translator return $class; } - protected function getPropertyOffset(string $property, string $class, string $namespace = ''): string - { - return $this->getNativeName('property_offset_' . $property, $namespace, $class); - } - protected function getNativeName(string $fn, string $ns = '', string $class = ''): string { $names = []; @@ -704,7 +705,7 @@ class CompilerBase extends \PhpAot\Core\Translator return $id; } - protected function getFuncId($funcName): int + protected function getFuncId(string $funcName): int { if (isset($this->funcMap[$funcName])) { $id = $this->funcMap[$funcName]; @@ -715,6 +716,23 @@ class CompilerBase extends \PhpAot\Core\Translator return $id; } + /** + * @param string $propName + * @param string $className 必须是带有命名空间的完整类名 + * @return int + */ + protected function getPropertyId(string $className, string $propName): int + { + $key = $className . '::' . $propName; + if (isset($this->propMap[$key])) { + $id = $this->propMap[$key]; + } else { + $id = $this->propIndex++; + $this->propMap[$key] = $id; + } + return $id; + } + protected function getClassEntryPtr(string $className): string { $id = $this->getClassId($className); @@ -748,6 +766,13 @@ class CompilerBase extends \PhpAot\Core\Translator return 'php_get_method(' . $funcId . ', ' . $this->getLiteralString($method) . ', ' . $classId . ', ' . $this->getLiteralString($class) . ')'; } + protected function getPropertyOffset(string $class, string $prop): string + { + $funcId = $this->getPropertyId($class, $prop); + $classId = $this->getClassId($class); + return 'php_get_prop(' . $funcId . ', ' . $this->getLiteralString($prop) . ', ' . $classId . ', ' . $this->getLiteralString($class) . ')'; + } + protected function parseTypeDecl(?NodeAbstract $type): string { // 联合类型暂时不支持,使用 var 类型代替 @@ -3554,7 +3579,7 @@ class CompilerBase extends \PhpAot\Core\Translator } } - protected function findNativeStaticProperty(Node\Expr\StaticPropertyFetch $expr, ?string &$class, ?string &$namespace): ?PropertyDef + protected function findNativeStaticProperty(Node\Expr\StaticPropertyFetch $expr, ?string &$class): ?PropertyDef { if ($this->isNameExpr($expr->class) and $this->isIdExpr($expr->name)) { $class = $this->parseIdentifier($expr->class); @@ -3569,10 +3594,10 @@ class CompilerBase extends \PhpAot\Core\Translator } $classDef = $this->getClassDef($fullName); - $namespace = $classDef->namespace; if ($classDef->hasProperty($prop)) { $propDef = $classDef->getProperty($prop); if ($propDef->isStatic()) { + $class = $fullName; return $propDef; } } @@ -3594,16 +3619,16 @@ class CompilerBase extends \PhpAot\Core\Translator if ($classDef->hasProperty($property)) { $propertyDef = $classDef->getProperty($property); if ($propertyDef->isPublic()) { - return self::PREFIX . $this->getPropertyOffset($property, $classDef->name, $classDef->namespace); + return $this->getPropertyOffset($classDef->getNamespacedName(), $property); } if ($propertyDef->isProtected()) { if ($scope) { - return self::PREFIX . $this->getPropertyOffset($property, $classDef->name, $classDef->namespace); + return $this->getPropertyOffset($classDef->getNamespacedName(), $property); } $this->fatalError($object, "Cannot access protected property `{$property}` of class `{$class}`"); } else { if ($scope === $findClass) { - return self::PREFIX . $this->getPropertyOffset($property, $classDef->name, $classDef->namespace); + return $this->getPropertyOffset($classDef->getNamespacedName(), $property); } $this->fatalError($object, "Cannot access private property `{$property}` of class `{$class}`"); } @@ -3621,10 +3646,10 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parseNativeStaticPropertyFetch(Node\Expr\StaticPropertyFetch $expr): string|bool { - $nativeProp = $this->findNativeStaticProperty($expr, $class, $namespace); + $nativeProp = $this->findNativeStaticProperty($expr, $class); if ($nativeProp) { - $classPtr = $this->getClassEntryPtr($this->getNamespacedClassName($class)); - $propOffset = self::PREFIX . $this->getPropertyOffset($nativeProp->name, $class, $namespace); + $classPtr = $this->getClassEntryPtr($class); + $propOffset = $this->getPropertyOffset($class, $nativeProp->name); return 'php::getStaticProperty(' . $classPtr . ', ' . $propOffset . ')'; } return false; diff --git a/src/Php/Translator.php b/src/Php/Translator.php index d0acb8bb..f9e4a96a 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -209,21 +209,17 @@ class Translator extends Preprocessor $lines[] = 'extern ' . self::TYPE_VAR . ' ' . $name . ';'; } - // property offset - foreach ($this->classes as $classDef) { - foreach ($classDef->properties as $propertyDef) { - $lines[] = 'extern uint32_t ' . self::PREFIX . $this->getPropertyOffset($propertyDef->name, $classDef->name, $classDef->namespace) . ';'; - } - } - $literalStringsCount = count($this->literalStrings); $lines[] = 'extern ' . self::TYPE_STR . ' ' . self::LITERAL_STRINGS . '[' . $literalStringsCount . '];' . PHP_EOL; $classCount = count($this->classMap); - $lines[] = 'extern zend_class_entry *' . self::PREFIX . self::CLASS_MAP . '[' . $classCount . '];' . PHP_EOL; + $lines[] = 'extern zend_class_entry *' . self::PREFIX . self::CLASS_MAP . '[' . $classCount . '];' . PHP_EOL; $funcCount = count($this->funcMap); - $lines[] = 'extern zend_function *' . self::PREFIX . self::FUNC_MAP . '[' . $funcCount . '];' . PHP_EOL; + $lines[] = 'extern zend_function *' . self::PREFIX . self::FUNC_MAP . '[' . $funcCount . '];' . PHP_EOL; + + $propCount = count($this->propMap); + $lines[] = 'extern uint32_t ' . self::PREFIX . self::PROP_MAP . '[' . $propCount . '];' . PHP_EOL; $code = implode(PHP_EOL, $lines) . PHP_EOL . PHP_EOL; $this->writeFile($file, $code); @@ -811,7 +807,8 @@ class Translator extends Preprocessor protected function genMethodWrapper(ClassDef $classDef, MethodDef $methodDef): string { - $name = $classDef->getNamespacedName(); + $name = $classDef->getNamespacedName(); + $fullClassName = $classDef->getNamespacedName(false); $cppCode = 'ZEND_METHOD(' . $name . ', ' . $methodDef->name . '){' . PHP_EOL; $cppCode .= $this->getIndent() . self::TYPE_OBJECT . ' this_(&execute_data->This);' . PHP_EOL; @@ -819,12 +816,12 @@ class Translator extends Preprocessor if ($property->type === self::TYPE_ARRAY and $property->default and $property->default !== self::TYPE_ARRAY . '{}') { if ($property->isStatic()) { $prop = new \stdClass(); - $prop->class = $classDef->getNamespacedName(false); + $prop->class = $fullClassName; $prop->name = $property->name; $prop->default = $property->default; $this->staticPropertyList[$name . '::' . $property->name] = $prop; } else { - $propOffset = self::PREFIX . $this->getPropertyOffset($property->name, $classDef->name, $classDef->namespace); + $propOffset = $this->getPropertyOffset($fullClassName, $property->name); $cppCode .= $this->getIndent() . 'this_.attr(' . $propOffset . ') = ' . $property->default . ';' . PHP_EOL; } } diff --git a/src/cpp/php_aot_helper.h b/src/cpp/php_aot_helper.h index 591378fb..37083e27 100644 --- a/src/cpp/php_aot_helper.h +++ b/src/cpp/php_aot_helper.h @@ -6,6 +6,7 @@ extern zend_class_entry *php_get_class(int class_id, const php::Str &class_name); extern zend_function *php_get_func(int func_id, const php::Str &func_name); extern zend_function *php_get_method(int func_id, const php::Str &method_name, int class_id, const php::Str &class_name); +extern uint32_t php_get_prop(int prop_id, const php::Str &prop_name, int class_id, const php::Str &class_name); extern const char *php_get_called_class(php::Object &this_); extern zend_class_entry *php_get_called_ce(php::Object &this_); diff --git a/src/template/extension.cc.php b/src/template/extension.cc.php index 0fc95e5e..fc525162 100644 --- a/src/template/extension.cc.php +++ b/src/template/extension.cc.php @@ -31,6 +31,9 @@ zend_class_entry *funcMap) . ']' ?>; +// property +uint32_t propMap) . ']' ?>; + zend_class_entry *php_get_class(int class_id, const php::String &class_name) { if (UNEXPECTED([class_id] == nullptr)) { [class_id] = php::getClassEntrySafe(class_name); @@ -53,6 +56,13 @@ zend_function *php_get_method(int func_id, const php::Str &method_name, int clas return [func_id]; } +uint32_t php_get_prop(int prop_id, const php::Str &prop_name, int class_id, const php::Str &class_name) { + if (UNEXPECTED([prop_id] == 0)) { + [prop_id] = php::getPropertyOffset(class_name, prop_name); + } + return [prop_id]; +} + // literal strings php::Str [] = { nativeConstants as $name => $const): type?> ; -// property offset -classes as $classDef): - foreach ($classDef->properties as $propertyDef): -?> -uint32_t getPropertyOffset($propertyDef->name, $classDef->name, $classDef->namespace)?>; - - // clang-format off static const zend_function_entry ext_functions[] = { PHP_FE(cli_set_process_title, arginfo_cli_set_process_title) @@ -136,16 +135,6 @@ foreach ($this->globalVars as $name => $type): php::initGlobal("", ); - // property offset -classes as $classDef): - foreach ($classDef->properties as $propertyDef): - ?> - getPropertyOffset($propertyDef->name, $classDef->name, $classDef->namespace)?> = php::getPropertyOffset(genCharPtr($classDef->getNamespacedName(false), true)?>, "name?>"); - // static property staticPropertyList as $prop): diff --git a/tests/aot/prop-001.phpt b/tests/aot/prop-001.phpt new file mode 100644 index 00000000..f43390fe --- /dev/null +++ b/tests/aot/prop-001.phpt @@ -0,0 +1,253 @@ +--TEST-- +Property +--FILE-- +context 存在性检查 + !isset($worker->$prop) && !isset($worker->context->$prop) && $worker->context->$prop = 'NNNN'; +} + +/** + * 改进后的安全写法 + */ +function safeLogic($worker, $prop) { + // ✅ 安全写法:先确保 context 存在,再使用空合并赋值 + if (!isset($worker->context)) { + $worker->context = new stdClass(); + } + $worker->context->$prop ??= 'NNNN'; +} + +/** + * 测试工具函数 + */ +function dumpState($label, $worker, $prop) { + echo "\n[$label]\n"; + echo " worker->$prop: " . (isset($worker->$prop) ? "'" . $worker->$prop . "'" : 'UNSET') . "\n"; + echo " context->$prop: " . ( + isset($worker->context) && isset($worker->context->$prop) + ? "'" . $worker->context->$prop . "'" + : (isset($worker->context) ? 'UNSET' : 'CONTEXT_NULL') + ) . "\n"; +} + +function main() { + echo "=".str_repeat("=", 70) . "\n"; + echo "TEST SUITE: isset + assignment short-circuit logic\n"; + echo "=".str_repeat("=", 70) . "\n"; + + $prop = 'name'; + + // ======================================================================== + // 测试 1: 两个属性都不存在 → 应触发赋值 + // ======================================================================== + echo "\n【TEST 1】两个属性都不存在 → 应触发赋值 'NNNN'\n"; + $worker = new Worker(); + $worker->context = new stdClass(); + + dumpState('BEFORE', $worker, $prop); + originalLogic($worker, $prop); + dumpState('AFTER (original)', $worker, $prop); + + // 验证 + assert(isset($worker->context->name), "TEST 1 FAILED: context->name should be set"); + assert($worker->context->name === 'NNNN', "TEST 1 FAILED: value should be 'NNNN'"); + echo "✅ TEST 1 PASSED\n"; + + // ======================================================================== + // 测试 2: $worker->$prop 存在 → 不应触发赋值 + // ======================================================================== + echo "\n【TEST 2】\$worker->\$prop 存在 → 不应触发赋值\n"; + $worker = new Worker(); + $worker->context = new stdClass(); + $worker->name = 'Alice'; // worker 有值 + + dumpState('BEFORE', $worker, $prop); + originalLogic($worker, $prop); + dumpState('AFTER (original)', $worker, $prop); + + // 验证:context->name 应该仍不存在 + assert(!isset($worker->context->name), "TEST 2 FAILED: context->name should NOT be set"); + echo "✅ TEST 2 PASSED\n"; + + // ======================================================================== + // 测试 3: $worker->context->$prop 存在 → 不应触发赋值 + // ======================================================================== + echo "\n【TEST 3】\$worker->context->\$prop 存在 → 不应触发赋值\n"; + $worker = new Worker(); + $worker->context = new stdClass(); + $worker->context->name = 'Bob'; // context 有值 + + dumpState('BEFORE', $worker, $prop); + originalLogic($worker, $prop); + dumpState('AFTER (original)', $worker, $prop); + + // 验证:context->name 应保持原值 + assert($worker->context->name === 'Bob', "TEST 3 FAILED: value should remain 'Bob'"); + echo "✅ TEST 3 PASSED\n"; + + // ======================================================================== + // 测试 4: 两个属性都存在 → 不应触发赋值 + // ======================================================================== + echo "\n【TEST 4】两个属性都存在 → 不应触发赋值\n"; + $worker = new Worker(); + $worker->context = new stdClass(); + $worker->name = 'Charlie'; + $worker->context->name = 'David'; + + dumpState('BEFORE', $worker, $prop); + originalLogic($worker, $prop); + dumpState('AFTER (original)', $worker, $prop); + + // 验证:context->name 应保持原值 + assert($worker->context->name === 'David', "TEST 4 FAILED: value should remain 'David'"); + echo "✅ TEST 4 PASSED\n"; + + // ======================================================================== + // 测试 5: $worker->context 为 null → 原始代码会触发错误! + // ======================================================================== + echo "\n【TEST 5】\$worker->context 为 null → 原始代码会触发 FATAL ERROR\n"; + $worker = new Worker(); + $worker->context = null; // context 为 null + + dumpState('BEFORE', $worker, $prop); + + try { + originalLogic($worker, $prop); + echo "❌ TEST 5 FAILED: Should have thrown error!\n"; + } catch (Error $e) { + echo "✅ TEST 5 PASSED: Caught expected error: " . $e->getMessage() . "\n"; + } + + // 安全写法测试 + $worker2 = new Worker(); + $worker2->context = null; + safeLogic($worker2, $prop); + dumpState('AFTER (safe)', $worker2, $prop); + assert(isset($worker2->context->name), "TEST 5 SAFE FAILED: context should be created"); + echo "✅ TEST 5 SAFE PASSED: Safe logic handled null context\n"; + + // ======================================================================== + // 测试 6: $worker->context 完全不存在(未定义)→ 原始代码会触发错误! + // ======================================================================== + echo "\n【TEST 6】\$worker->context 未定义 → 原始代码会触发 NOTICE\n"; + $worker = new Worker(); + unset($worker->context); // 完全移除 context 属性 + + dumpState('BEFORE', $worker, $prop); + + // PHP 8.0+ 会抛出 Error,PHP 7.x 会触发 Notice + try { + originalLogic($worker, $prop); + echo "❌ TEST 6 FAILED: Should have thrown error!\n"; + } catch (Error $e) { + echo "✅ TEST 6 PASSED: Caught expected error: " . $e->getMessage() . "\n"; + } catch (Exception $e) { + echo "⚠️ TEST 6: Notice triggered (PHP 7.x behavior)\n"; + } + + // 安全写法测试 + $worker2 = new Worker(); + unset($worker2->context); + safeLogic($worker2, $prop); + dumpState('AFTER (safe)', $worker2, $prop); + assert(isset($worker2->context->name), "TEST 6 SAFE FAILED: context should be created"); + echo "✅ TEST 6 SAFE PASSED: Safe logic handled undefined context\n"; + + // ======================================================================== + // 测试 7: 属性值为 null(isset 返回 false) + // ======================================================================== + echo "\n【TEST 7】属性值为 null → isset 返回 false,应触发赋值\n"; + $worker = new Worker(); + $worker->context = new stdClass(); + $worker->name = null; // null 值 + $worker->context->name = null; // null 值 + + dumpState('BEFORE', $worker, $prop); + originalLogic($worker, $prop); + dumpState('AFTER (original)', $worker, $prop); + + // 验证:null 被视为"不存在",应触发赋值 + assert($worker->context->name === 'NNNN', "TEST 7 FAILED: null should trigger assignment"); + echo "✅ TEST 7 PASSED\n"; + + // ======================================================================== + // 测试 8: 属性值为假值(0, '', false)→ isset 返回 true,不应触发赋值 + // ======================================================================== + echo "\n【TEST 8】属性值为假值(0, '', false)→ isset 返回 true\n"; + + // 子测试 8a: 空字符串 + $worker = new Worker(); + $worker->context = new stdClass(); + $worker->context->name = ''; + originalLogic($worker, $prop); + assert($worker->context->name === '', "TEST 8a FAILED: empty string should NOT trigger assignment"); + echo "✅ TEST 8a PASSED: empty string preserved\n"; + + // 子测试 8b: 数字 0 + $worker = new Worker(); + $worker->context = new stdClass(); + $worker->context->name = 0; + originalLogic($worker, $prop); + assert($worker->context->name === 0, "TEST 8b FAILED: zero should NOT trigger assignment"); + echo "✅ TEST 8b PASSED: zero preserved\n"; + + // 子测试 8c: false + $worker = new Worker(); + $worker->context = new stdClass(); + $worker->context->name = false; + originalLogic($worker, $prop); + assert($worker->context->name === false, "TEST 8c FAILED: false should NOT trigger assignment"); + echo "✅ TEST 8c PASSED: false preserved\n"; + + // ======================================================================== + // 测试 9: 动态属性名(变量) + // ======================================================================== + echo "\n【TEST 9】动态属性名(变量)\n"; + $worker = new Worker(); + $worker->context = new stdClass(); + $dynamicProp = 'email'; + + originalLogic($worker, $dynamicProp); + assert(isset($worker->context->email), "TEST 9 FAILED: dynamic property should be set"); + assert($worker->context->email === 'NNNN', "TEST 9 FAILED: value should be 'NNNN'"); + echo "✅ TEST 9 PASSED\n"; + + // ======================================================================== + // 测试 10: 多次调用(幂等性) + // ======================================================================== + echo "\n【TEST 10】多次调用(幂等性)\n"; + $worker = new Worker(); + $worker->context = new stdClass(); + + originalLogic($worker, $prop); // 第一次:赋值 + $firstValue = $worker->context->name; + + originalLogic($worker, $prop); // 第二次:不应覆盖 + $secondValue = $worker->context->name; + + assert($firstValue === 'NNNN', "TEST 10 FAILED: first call should set value"); + assert($secondValue === 'NNNN', "TEST 10 FAILED: second call should not change value"); + assert($firstValue === $secondValue, "TEST 10 FAILED: value should be idempotent"); + echo "✅ TEST 10 PASSED: Idempotent behavior confirmed\n"; + + // ======================================================================== + // 总结 + // ======================================================================== + echo "\n".str_repeat("=", 70); + echo "\n✅ ALL TESTS COMPLETED SUCCESSFULLY\n"; + echo "=".str_repeat("=", 70) . "\n"; +} +?> +--EXPECT-- +string(10) "Processing" +string(7) "unknown" +string(2) "OK" +string(2) "OK" \ No newline at end of file