feat(compiler): 添加属性映射支持用于PHP编译器

- 添加PROP_MAP常量和相关属性映射数据结构
- 实现getPropertyId方法用于获取属性标识符
- 更新getPropertyOffset方法以使用新的属性映射机制
- 修改静态属性查找逻辑以返回完整命名空间类名
- 在模板文件中添加属性映射数组声明和初始化
- 更新外部函数声明以包含属性获取功能
- 移除旧的属性偏移量预定义方式,改用统一的映射表管理
- 添加完整的属性访问测试用例验证功能正确性
pull/1/head
韩天峰 5 months ago
parent e869ee49a8
commit fed0d7c2a0
  1. 53
      src/Php/CompilerBase.php
  2. 21
      src/Php/Translator.php
  3. 1
      src/cpp/php_aot_helper.h
  4. 31
      src/template/extension.cc.php
  5. 253
      tests/aot/prop-001.phpt

@ -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<string, int>
*/
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;

@ -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;
}
}

@ -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_);

@ -31,6 +31,9 @@ zend_class_entry *<?= Translator::PREFIX . Translator::CLASS_MAP . '[' . count($
// func
zend_function *<?= Translator::PREFIX . Translator::FUNC_MAP . '[' . count($this->funcMap) . ']' ?>;
// property
uint32_t <?= Translator::PREFIX . Translator::PROP_MAP . '[' . count($this->propMap) . ']' ?>;
zend_class_entry *php_get_class(int class_id, const php::String &class_name) {
if (UNEXPECTED(<?= Translator::PREFIX . Translator::CLASS_MAP ?>[class_id] == nullptr)) {
<?= Translator::PREFIX . Translator::CLASS_MAP ?>[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 <?= Translator::PREFIX . Translator::FUNC_MAP ?>[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(<?= Translator::PREFIX . Translator::PROP_MAP ?>[prop_id] == 0)) {
<?= Translator::PREFIX . Translator::PROP_MAP ?>[prop_id] = php::getPropertyOffset(class_name, prop_name);
}
return <?= Translator::PREFIX . Translator::PROP_MAP ?>[prop_id];
}
// literal strings
php::Str <?=Translator::LITERAL_STRINGS?>[] = {
<?php
@ -69,17 +79,6 @@ foreach ($this->nativeConstants as $name => $const):
<?=$const->type?> <?=$name?>;
<?php endforeach; ?>
// property offset
<?php
foreach ($this->classes as $classDef):
foreach ($classDef->properties as $propertyDef):
?>
uint32_t <?=Translator::PREFIX . $this->getPropertyOffset($propertyDef->name, $classDef->name, $classDef->namespace)?>;
<?php
endforeach;
endforeach;
?>
// 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("<?=$name?>", <?= $name ?>);
<?php endforeach; ?>
// property offset
<?php
foreach ($this->classes as $classDef):
foreach ($classDef->properties as $propertyDef):
?>
<?=Translator::PREFIX . $this->getPropertyOffset($propertyDef->name, $classDef->name, $classDef->namespace)?> = php::getPropertyOffset(<?=$this->genCharPtr($classDef->getNamespacedName(false), true)?>, "<?=$propertyDef->name?>");
<?php
endforeach;
endforeach;
?>
// static property
<?php
foreach ($this->staticPropertyList as $prop):

@ -0,0 +1,253 @@
--TEST--
Property
--FILE--
<?php
class Worker {
public $context;
}
/**
* 原始代码(有缺陷)
*/
function originalLogic($worker, $prop) {
// ⚠ 有缺陷的写法:缺少 $worker->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"
Loading…
Cancel
Save