fix(compiler): resolve generated C++ symbol ambiguities

master
韩天峰 2 days ago
parent b3c7ee9bd3
commit 02a98ffc61
  1. 28
      docs/en/CPP_SYMBOL_NAMING.md
  2. 28
      docs/zh-cn/CPP_SYMBOL_NAMING.md
  3. 9
      phpunit/src/CompilerBaseApiTest.php
  4. 9
      src/CompilerBase.php
  5. 3
      src/Metadata/Constants.php
  6. 2
      src/Parser/AssignOpTrait.php
  7. 2
      src/Parser/ForeachTrait.php
  8. 2
      src/Translator.php

@ -13,7 +13,7 @@ This document is the internal C++ naming convention for TypePHP, PHPX, and TypeP
| --- | --- | --- | --- | --- |
| `typephp_` | TypePHP-specific runtime or compiled-artifact support logic | `typephp_call_parent_constructor()` | TypePHP/PHPX runtime | Internal or explicitly exported ABI |
| `php::` | C++ wrappers for PHP runtime capabilities such as ZendAPI, zval, HashTable, and call frames | `php::deindirect()` | PHPX C++ API | PHPX API |
| `typephp_<project>` | The private C++ namespace of a single compiled project | `namespace typephp_tpc` | Current generated project | Non-public ABI |
| `typephp_project_<project>` | The private C++ namespace of a single compiled project | `namespace typephp_project_tpc` | Current generated project | Non-public ABI |
| `php_` | C++ callable symbols mapped from user PHP functions and class methods | `php_app__user__save()` | Visible to the linker | TypePHP/stub callable ABI |
Core constraints:
@ -21,7 +21,7 @@ Core constraints:
1. Do not add new global framework `php_*` helpers.
2. Capabilities that are unrelated to TypePHP and only wrap ZendAPI must be placed in `namespace php`.
3. Logic that is unique to TypePHP and needs to be called across generated files uses the `typephp_` prefix.
4. Data and functions that serve only one compiled project go into the `typephp_<project>` namespace.
4. Data and functions that serve only one compiled project go into the `typephp_project_<project>` namespace.
5. Global `php_*` callable names are reserved for the compiled ABI of user PHP declarations.
## 2. `typephp_`: TypePHP-specific Logic
@ -144,23 +144,23 @@ php::stdCreateObject();
Do not mechanically preserve Zend's snake_case names as global C++ names. Lower-level calls can continue to use the original Zend API, such as `zend_objects_new()`, but the wrapper layer exposed to generated code should use `php::`.
## 4. `typephp_<project>`: Project-private Namespace
## 4. `typephp_project_<project>`: Project-private Namespace
Each TypePHP compiled project has an independent C++ namespace:
```text
typephp_<target-name>
typephp_project_<target-name>
```
For example, if the project name is `tpc`:
```cpp
namespace typephp_tpc {
namespace typephp_project_tpc {
// Project-private generated state and helpers.
}
```
The `-` and `*` in the project name are converted to `_`, and the remaining characters must satisfy the compiler's target identifier validation. Because of the fixed `typephp_` prefix, the final C++ namespace is a valid identifier even if the project name starts with a digit.
The `-` and `*` in the project name are converted to `_`, and the remaining characters must satisfy the compiler's target identifier validation. The distinct `typephp_project_` prefix prevents generated namespaces from colliding with global `typephp_*` runtime helpers. It also keeps the final C++ namespace valid when the project name starts with a digit.
### 4.1 Content That Should Go into This Namespace
@ -175,7 +175,7 @@ The `-` and `*` in the project name are converted to `_`, and the remaining char
Illustration:
```cpp
namespace typephp_demo {
namespace typephp_project_demo {
static php::Str literal_strings[] = {
php::Str{"hello"},
@ -195,12 +195,12 @@ static void module_init() {
// Initialize this project's generated state.
}
} // namespace typephp_demo
} // namespace typephp_project_demo
```
### 4.2 Visibility and ABI
- Names inside `typephp_<project>` are implementation details, not library stub ABI.
- Names inside `typephp_project_<project>` are implementation details, not library stub ABI.
- Objects and functions that can be limited to `static` should continue to be marked `static`.
- Generated headers may declare project-internal accessors that must be used across translation units, but must not expose underlying arrays or cache tables.
- External handwritten C++ code must not depend on literal indexes, cache indexes, or project-internal storage names.
@ -211,10 +211,10 @@ static void module_init() {
Historical generated names may still appear in the project namespace, for example:
```cpp
typephp_demo::php_class_entry_App_User
typephp_project_demo::php_class_entry_App_User
```
Although the member name starts with `php_`, the full symbol resides in `typephp_demo`, so it is a project-private implementation rather than the global user callable ABI described in Section 5. New project-internal helpers should prefer short names without `php_`, such as `get_class()`, `get_func()`, and `get_str()`.
Although the member name starts with `php_`, the full symbol resides in `typephp_project_demo`, so it is a project-private implementation rather than the global user callable ABI described in Section 5. New project-internal helpers should prefer short names without `php_`, such as `get_class()`, `get_func()`, and `get_str()`.
## 5. `php_`: The C++ ABI of User PHP Callables
@ -315,7 +315,7 @@ When adding a C++ API, judge in the following order:
1. **Is it the compiled body of a user PHP function or class method?**
- Yes: use the existing `php_` callable ABI generator; do not handwrite another mapping.
2. **Does it serve only one current TypePHP project?**
- Yes: place it in `typephp_<project>`, and use `static` or private accessors where possible.
- Yes: place it in `typephp_project_<project>`, and use `static` or private accessors where possible.
3. **Does it implement TypePHP-specific semantics?**
- Yes: use the `typephp_` prefix.
4. **Is it only a C++ wrapper of Zend/PHP runtime capabilities?**
@ -330,7 +330,7 @@ When adding or modifying generated helpers, check:
- [ ] No new global `php_*` helpers in `typephp_helper.h`;
- [ ] ZendAPI wrappers are in `namespace php`;
- [ ] TypePHP-specific logic uses `typephp_`;
- [ ] Project caches and storage are in `typephp_<project>`;
- [ ] Project caches and storage are in `typephp_project_<project>`;
- [ ] Project-private tables are not exposed directly via `extern` through generated headers;
- [ ] User callables still use the unified `php_` ABI generator;
- [ ] New names do not collide with user-declarable PHP functions or methods;
@ -350,7 +350,7 @@ tests/compiler/basic/helper-symbol-collision.phpt
| --- | --- |
| `php_` callable prefix and combination separator | `src/CompilerBase.php` |
| Callable combination collision detection | `src/Preprocessor.php` |
| `typephp_<project>` generation and project-private tables | `src/Translator.php` |
| `typephp_project_<project>` generation and project-private tables | `src/Translator.php` |
| TypePHP extension prefix constants | `src/Metadata/Constants.php` |
| PHPX/TypePHP helper classification | `vendor/swoole/phpx/include/typephp_helper.h` |
| Embed module accessor concatenation | `vendor/swoole/phpx/src/misc/typephp_main.cc` |

@ -13,7 +13,7 @@
| --- | --- | --- | --- | --- |
| `typephp_` | TypePHP 独有的运行时或编译产物支持逻辑 | `typephp_call_parent_constructor()` | TypePHP/PHPX 运行时 | 内部或显式导出 ABI |
| `php::` | 对 ZendAPI、zval、HashTable、call frame 等 PHP 运行时能力的 C++ 封装 | `php::deindirect()` | PHPX C++ API | PHPX API |
| `typephp_<project>` | 单个编译项目的私有 C++ 命名空间 | `namespace typephp_tpc` | 当前生成项目 | 非公共 ABI |
| `typephp_project_<project>` | 单个编译项目的私有 C++ 命名空间 | `namespace typephp_project_tpc` | 当前生成项目 | 非公共 ABI |
| `php_` | 用户 PHP 函数和类方法映射后的 C++ callable 符号 | `php_app__user__save()` | 链接器可见 | TypePHP/stub callable ABI |
核心约束:
@ -21,7 +21,7 @@
1. 不得新增全局的框架 `php_*` helper。
2. 与 TypePHP 无关、只是包装 ZendAPI 的能力必须放入 `namespace php`
3. TypePHP 独有且需要跨生成文件调用的逻辑使用 `typephp_` 前缀。
4. 只服务于一个编译项目的数据和函数放入 `typephp_<project>` 命名空间。
4. 只服务于一个编译项目的数据和函数放入 `typephp_project_<project>` 命名空间。
5. 全局 `php_*` callable 名称保留给用户 PHP 声明的编译 ABI。
## 2. `typephp_`:TypePHP 独有逻辑
@ -144,23 +144,23 @@ php::stdCreateObject();
不要把 Zend 的 snake_case 名称机械地保留为全局 C++ 名称。底层调用可以继续使用 Zend 原始 API,例如 `zend_objects_new()`,但对生成代码暴露的包装层应使用 `php::`
## 4. `typephp_<project>`:项目私有命名空间
## 4. `typephp_project_<project>`:项目私有命名空间
每个 TypePHP 编译项目拥有独立的 C++ 命名空间:
```text
typephp_<target-name>
typephp_project_<target-name>
```
例如项目名为 `tpc`
```cpp
namespace typephp_tpc {
namespace typephp_project_tpc {
// Project-private generated state and helpers.
}
```
项目名中的 `-``*` 会转换为 `_`,其余字符必须满足编译器的 target identifier 校验。由于固定带有 `typephp_` 前缀,即使项目名以数字开头,最终 C++ namespace 仍是合法标识符。
项目名中的 `-``*` 会转换为 `_`,其余字符必须满足编译器的 target identifier 校验。独立的 `typephp_project_` 前缀可防止生成 namespace 与全局 `typephp_*` 运行时 helper 冲突;即使项目名以数字开头,最终 C++ namespace 仍是合法标识符。
### 4.1 应放入该命名空间的内容
@ -175,7 +175,7 @@ namespace typephp_tpc {
示意:
```cpp
namespace typephp_demo {
namespace typephp_project_demo {
static php::Str literal_strings[] = {
php::Str{"hello"},
@ -195,12 +195,12 @@ static void module_init() {
// Initialize this project's generated state.
}
} // namespace typephp_demo
} // namespace typephp_project_demo
```
### 4.2 可见性与 ABI
- `typephp_<project>` 内的名称是实现细节,不是 library stub ABI。
- `typephp_project_<project>` 内的名称是实现细节,不是 library stub ABI。
- 可限制为 `static` 的对象和函数应继续标记为 `static`
- 生成头文件可以声明必须跨 translation unit 使用的项目内部 accessor,但不应暴露底层数组或缓存表。
- 外部手写 C++ 代码不得依赖 literal index、cache index 或项目内部 storage 名称。
@ -211,10 +211,10 @@ static void module_init() {
项目 namespace 中仍可能出现历史生成名称,例如:
```cpp
typephp_demo::php_class_entry_App_User
typephp_project_demo::php_class_entry_App_User
```
虽然成员名以 `php_` 开头,但完整符号位于 `typephp_demo` 中,因此它属于项目私有实现,而不是第 5 节所述的全局用户 callable ABI。新增项目内部 helper 应优先使用不带 `php_` 的短名称,例如 `get_class()`、`get_func()` 和 `get_str()`
虽然成员名以 `php_` 开头,但完整符号位于 `typephp_project_demo` 中,因此它属于项目私有实现,而不是第 5 节所述的全局用户 callable ABI。新增项目内部 helper 应优先使用不带 `php_` 的短名称,例如 `get_class()`、`get_func()` 和 `get_str()`
## 5. `php_`:用户 PHP callable 的 C++ ABI
@ -315,7 +315,7 @@ TypePHP 扩展不得分别编译或静态链接包含进程级 Zend 状态的 PH
1. **它是否是用户 PHP 函数或类方法的编译本体?**
- 是:使用既有 `php_` callable ABI 生成器,禁止手写另一套映射。
2. **它是否只服务于当前一个 TypePHP 项目?**
- 是:放入 `typephp_<project>`,并尽可能使用 `static` 或私有 accessor。
- 是:放入 `typephp_project_<project>`,并尽可能使用 `static` 或私有 accessor。
3. **它是否实现 TypePHP 独有语义?**
- 是:使用 `typephp_` 前缀。
4. **它是否只是对 Zend/PHP 运行时能力的 C++ 封装?**
@ -330,7 +330,7 @@ TypePHP 扩展不得分别编译或静态链接包含进程级 Zend 状态的 PH
- [ ] `typephp_helper.h` 中没有新增全局 `php_*` helper;
- [ ] ZendAPI 包装位于 `namespace php`
- [ ] TypePHP 独有逻辑使用 `typephp_`
- [ ] 项目缓存和 storage 位于 `typephp_<project>`
- [ ] 项目缓存和 storage 位于 `typephp_project_<project>`
- [ ] 项目私有表没有通过生成头文件直接 `extern` 暴露;
- [ ] 用户 callable 仍使用统一的 `php_` ABI 生成器;
- [ ] 新名称不会与用户可声明的 PHP 函数或方法发生冲突;
@ -350,7 +350,7 @@ tests/compiler/basic/helper-symbol-collision.phpt
| --- | --- |
| `php_` callable 前缀与组合分隔符 | `src/CompilerBase.php` |
| callable 组合冲突检测 | `src/Preprocessor.php` |
| `typephp_<project>` 生成及项目私有表 | `src/Translator.php` |
| `typephp_project_<project>` 生成及项目私有表 | `src/Translator.php` |
| TypePHP extension 前缀常量 | `src/Metadata/Constants.php` |
| PHPX/TypePHP helper 分类 | `vendor/swoole/phpx/include/typephp_helper.h` |
| embed module accessor 拼接 | `vendor/swoole/phpx/src/misc/typephp_main.cc` |

@ -5,6 +5,7 @@ namespace TypePhp\Tests;
use PHPUnit\Framework\TestCase;
use TypePhp\CompilerTest;
use TypePhp\CompilerBase;
use TypePhp\Metadata\Constants;
use TypePhp\Type;
use TypePhp\Exception\TestError;
use TypePhp\Platform\Macos;
@ -733,13 +734,15 @@ YAML, 'myproject.yml', 'examples/tetris-sdl');
$this->assertSame('demo.so', $this->invokeMethod('getTargetFileName'));
}
public function testGeneratedZendModuleAlwaysUsesTypePhpPrefix(): void
public function testGeneratedZendModuleAndProjectNamespaceUseDistinctPrefixes(): void
{
$this->compiler->setTargetName('demo');
$this->assertSame('typephp_demo', $this->compiler->getModuleName());
$this->assertSame('typephp_project_demo', $this->compiler->getProjectNamespace());
$this->compiler->setTargetName('123');
$this->assertSame('typephp_123', $this->compiler->getModuleName());
$this->assertSame('typephp_project_123', $this->compiler->getProjectNamespace());
}
public function testParseProjectYamlResolvesRelativePathOptionsAgainstYamlDirectory(): void
@ -1270,7 +1273,7 @@ YAML);
$compiler->genDataDeclarations($dataFile);
$data = file_get_contents($dataFile);
$extension = file_get_contents($compiler->genExtension());
$namespace = 'typephp_' . $target;
$namespace = Constants::CPP_PROJECT_NAMESPACE_PREFIX . $target;
$this->assertStringContainsString('namespace ' . $namespace . ' {', $data, $mode);
$this->assertStringContainsString('using namespace ' . $namespace . ';', $data, $mode);
@ -1303,7 +1306,7 @@ YAML);
$this->assertStringContainsString('#include <typephp_runtime.h>', $extension);
$this->assertStringContainsString('TYPEPHP_EMBED_GET_MODULE_FUNCTION(' . $target . ')', $extension);
$this->assertStringContainsString(
'return &' . $namespace . '::' . $namespace . '_module_entry;',
'return &' . $namespace . '::typephp_' . $target . '_module_entry;',
$extension,
);
} else {

@ -1546,13 +1546,16 @@ class CompilerBase implements PropertyAccessContext
return $list;
}
protected function parseArrayKey(NodeAbstract $expr): string
protected function parseArrayKey(NodeAbstract $expr, bool $keepStringObject = false): string
{
$this->assertNotNativeObjectArrayKey($expr);
$key = $this->parseIdentifier($expr);
if (str_starts_with($key, self::LITERAL_STRING_GETTER . '(')) {
$key = "{$key}.str()";
} elseif ($this->isZeroLiteral($expr)) {
// Array initializers and setters use zend_string* keys, while item()
// uses php::String to avoid an ambiguous conversion to Variant.
return $keepStringObject ? $key : "{$key}.str()";
}
if ($this->isZeroLiteral($expr)) {
$key = self::VALUE_ZERO;
}
return $key;

@ -14,6 +14,9 @@ class Constants
{
public const string EXTENSION_PREFIX = 'typephp_';
/** Keep generated project namespaces disjoint from global typephp_* runtime helpers. */
public const string CPP_PROJECT_NAMESPACE_PREFIX = 'typephp_project_';
public const array CPP_RESERVED_NAMES = [
'alignas',
'alignof',

@ -287,7 +287,7 @@ trait AssignOpTrait
continue;
}
if ($item instanceof ArrayItem) {
$key = $item->key ? $this->parseArrayKey($item->key) : (string) $k;
$key = $item->key ? $this->parseArrayKey($item->key, true) : (string) $k;
if ($item->value instanceof Expr\List_) {
$nestedTmp = $this->genTmpVarName();
$this->addLocalVar($nestedTmp, Type::ARRAY);

@ -26,7 +26,7 @@ trait ForeachTrait
if ($item->byRef) {
$this->fatalError($item, 'Foreach list destructuring cannot bind items by reference');
}
$key = $item->key ? $this->parseArrayKey($item->key) : (string) $k;
$key = $item->key ? $this->parseArrayKey($item->key, true) : (string) $k;
if ($item->value instanceof Expr\List_) {
$nestedTmpVar = $this->genTmpVarName();
$this->addLocalVar($nestedTmpVar, Type::VAR);

@ -1592,7 +1592,7 @@ CODE;
public function getProjectNamespace(): string
{
return $this->getModuleName();
return Constants::CPP_PROJECT_NAMESPACE_PREFIX . $this->targetName;
}
/**

Loading…
Cancel
Save