feat(compiler): add NoExport attribute support for library ABI control

- Add exported property to ClassDef and FunctionDef entities
- Implement #[NoExport] compile-time attribute processing
- Exclude NoExport-marked declarations from library stub generation
- Preserve original names during AST name resolution
- Add comprehensive test coverage for NoExport functionality
- Document NoExport attribute usage in MIXED_CPP_PHP.md
- Support NoExport with various namespace resolution patterns
- Apply NoExport cascading from classes to their methods
- Generate proper extern declarations for non-exported symbols
pull/34/head
韩天峰 1 month ago
parent 3ac611220e
commit 5ed5c4e384
  1. 14
      docs/MIXED_CPP_PHP.md
  2. 7
      phpunit/code/compiler_api/library_import_global.php
  3. 5
      phpunit/code/compiler_api/library_import_native.stub.php
  4. 35
      phpunit/code/compiler_api/library_import_php.php
  5. 36
      phpunit/code/compiler_api/library_no_export_namespace.php
  6. 68
      phpunit/src/CompilerBaseApiTest.php
  7. 2
      src/Entity/ClassDef.php
  8. 2
      src/Entity/FunctionDef.php
  9. 26
      src/Generator/LibraryImportStubGenerator.php
  10. 42
      src/Preprocessor.php
  11. 4
      src/Translator.php
  12. 9
      src/gen_stub.php
  13. 5
      src/polyfills.php

@ -698,6 +698,20 @@ function vector_new(int $size, bool $init = false): mixed {}
外部 stub 中的类会在消费项目中生成类注册、属性和常量实体,但不生成 `php_*` 方法本体;方法本体由动态库提供。
Property hook 同样按方法处理:发布 stub 保留 `get`/`set` 的声明并移除实现,消费项目生成属性实体,hook 的 getter/setter `php_*` 实现从动态库导入。
库内部声明可使用编译期 Attribute `#[NoExport]` 从公开 ABI 排除:
```php
#[\NoExport]
function internal_helper(): void {}
#[\NoExport]
class InternalService {}
```
声明仍参与当前库编译,但不会进入 `<target>.stub.php`,对应 `php_*` 符号也不添加 library export 修饰。类注解会级联到其全部方法;单个方法也可以独立标记。`NoExport` 位于根命名空间:全局命名空间可写 `#[NoExport]`,其他命名空间必须写 `#[\NoExport]`,并且该编译期 Attribute 不会进入运行时元数据。
`NoExport``ExtensionProvider` 都遵循 PHP 类名解析规则,支持完全限定名、`use` 和 `use ... as ...` 别名。只有解析结果严格指向根命名空间内建 Attribute 时,编译器才会消费它。
`php_<target>_func_decl.h``php_<target>_data_decl.h` 都是 TypePHP 构建过程的内部生成文件,不是库的对外开发头文件。
`func_decl.h``-m lib` 构建时还会被强制包含,用于给当前 target 的 `php_*` C++ ABI 函数添加平台导出标记;`data_decl.h` 仅在 target 内部声明全局变量、字面量、常量对象和运行时映射等数据。

@ -0,0 +1,7 @@
<?php
#[NoExport]
function global_hidden(): int
{
return 42;
}

@ -2,6 +2,8 @@
namespace LibraryApi;
use \NoExport;
class NativeCounter
{
public const int INITIAL = 3;
@ -11,3 +13,6 @@ class NativeCounter
}
function native_value(string $name = 'typephp'): string {}
#[NoExport]
function native_hidden(): int {}

@ -2,6 +2,10 @@
namespace LibraryApi;
use \ExtensionProvider as Provider;
use \NoExport as Internal;
use \Type;
class Counter
{
public const int STEP = 2;
@ -20,9 +24,40 @@ class Counter
$this->value += $amount;
return $this->value;
}
#[Internal]
public function reset(): void
{
$this->value = 0;
}
}
#[Internal]
class InternalCounter
{
public function value(): int
{
return 42;
}
}
#[Internal]
#[Provider(Type::String)]
class InternalStringExtension
{
public static function byteLength(string $value): int
{
return strlen($value);
}
}
function twice(int $value): int
{
return $value * 2;
}
#[Internal]
function internal_twice(int $value = 2): int
{
return $value * 2;
}

@ -0,0 +1,36 @@
<?php
namespace App\XX;
use \NoExport;
use NoExport as Hidden;
#[NoExport]
function imported_attribute(): int
{
return 1;
}
#[Hidden]
function aliased_attribute(): int
{
return 2;
}
#[\NoExport]
function fully_qualified_attribute(): int
{
return 3;
}
#[namespace\NoExport]
function relative_attribute(): int
{
return 4;
}
#[App\XX\NoExport]
function qualified_attribute(): int
{
return 5;
}

@ -923,12 +923,19 @@ YAML);
$files = [
ROOT_PATH . '/phpunit/code/compiler_api/library_import_php.php',
ROOT_PATH . '/phpunit/code/compiler_api/library_import_native.stub.php',
ROOT_PATH . '/phpunit/code/compiler_api/library_import_global.php',
];
$this->compiler->addFiles($files);
foreach ($files as $file) {
$this->compiler->prepareFile($file);
$this->compiler->convertFile($file);
$this->assertStringNotContainsString(
'NoExport',
file_get_contents($this->compiler->getArgInfoHeaderFile($file)),
);
}
$provider = $this->invokeMethod('getClass', 'LibraryApi\\InternalStringExtension');
$this->assertSame(Type::STR, $provider->extensionProviderTarget);
$stubFile = $this->compiler->genLibraryImportStub($files);
$stub = file_get_contents($stubFile);
@ -947,10 +954,53 @@ YAML);
$this->assertStringContainsString('function native_value(string $name = \'typephp\'): string', $stub);
$this->assertStringContainsString('class NativeCounter', $stub);
$this->assertStringContainsString('function bump(int $amount): int', $stub);
$this->assertStringNotContainsString('function reset()', $stub);
$this->assertStringNotContainsString('class InternalCounter', $stub);
$this->assertStringNotContainsString('class InternalStringExtension', $stub);
$this->assertStringNotContainsString('function internal_twice(', $stub);
$this->assertStringNotContainsString('function native_hidden(', $stub);
$this->assertStringNotContainsString('function global_hidden(', $stub);
$this->assertStringNotContainsString('NoExport', $stub);
$this->assertStringNotContainsString('return $this->value', $stub);
$this->assertStringNotContainsString('intdiv($value, 2)', $stub);
$this->assertStringNotContainsString('return $value * 2', $stub);
$libraryHeaderFile = $this->testDir . '/php_prime2_func_decl.h';
$this->compiler->genFunctionDeclarations($libraryHeaderFile);
$libraryHeader = file_get_contents($libraryHeaderFile);
$this->assertStringContainsString(
'TYPEPHP_PRIME2_API php::Int php_libraryapi__twice(',
$libraryHeader,
);
$this->assertStringContainsString(
'extern php::Int php_libraryapi__internal_twice(',
$libraryHeader,
);
$this->assertStringContainsString(
'extern php::Int php_libraryapi__internalcounter__value(',
$libraryHeader,
);
$this->assertStringContainsString(
'extern php::Int php_libraryapi__internalstringextension__bytelength(',
$libraryHeader,
);
$this->assertStringContainsString(
'extern void php_libraryapi__counter__reset(',
$libraryHeader,
);
$this->assertStringContainsString(
'extern php::Int php_libraryapi__native_hidden(',
$libraryHeader,
);
$this->assertStringContainsString(
'extern php::Int php_global_hidden(',
$libraryHeader,
);
$this->assertStringContainsString(
'extern php::Int php_libraryapi__internal_twice_arg_0_default_value();',
$libraryHeader,
);
$consumerDir = $this->testDir . '/consumer';
mkdir($consumerDir, 0777, true);
$consumer = CompilerTest::create($consumerDir);
@ -1007,6 +1057,24 @@ YAML);
$this->assertSame(['prime2'], $consumer->getLinkLibs());
}
public function testNoExportFollowsPhpNamespaceResolution(): void
{
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB);
$this->setPropertyValue('outputDir', $this->testDir);
$this->compiler->setTargetName('namespace_rules');
$stubFile = $this->compiler->genLibraryImportStub([
ROOT_PATH . '/phpunit/code/compiler_api/library_no_export_namespace.php',
]);
$stub = file_get_contents($stubFile);
$this->assertStringNotContainsString('function imported_attribute(): int', $stub);
$this->assertStringNotContainsString('function aliased_attribute(): int', $stub);
$this->assertStringNotContainsString('function fully_qualified_attribute(): int', $stub);
$this->assertStringContainsString('function relative_attribute(): int', $stub);
$this->assertStringContainsString('function qualified_attribute(): int', $stub);
}
public function testLibraryCompileOptionsExportOnlyPublicApiByDefault(): void
{
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB);

@ -32,6 +32,8 @@ class ClassDef extends ClassLikeDef
public string $extends = '';
public bool $requireCtor = false;
public bool $enum = false;
/** Whether this class and its methods are part of the public ABI of a library build. */
public bool $exported = true;
public ?string $extensionProviderTarget = null;
/**

@ -25,6 +25,8 @@ class FunctionDef
public string $namespace;
public bool $method = false;
public bool $stub = false;
/** Whether this function is part of the public ABI of a library build. */
public bool $exported = true;
/** External library imported by the stub containing this function. */
public string $importLibrary = '';
public bool $returnTypeUndeclared = false;

@ -47,7 +47,7 @@ final class LibraryImportStubGenerator
}
$ast = $this->parser->parse($code) ?? [];
$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver());
$traverser->addVisitor(new NameResolver(null, ['preserveOriginalNames' => true]));
$ast = $traverser->traverse($ast);
foreach ($ast as $stmt) {
@ -98,6 +98,10 @@ final class LibraryImportStubGenerator
private function makeImportDeclaration(Node\Stmt $stmt): ?Node\Stmt
{
if ($this->hasNoExportAttribute($stmt)) {
return null;
}
$comments = array_filter(
$stmt->getComments(),
static fn(\PhpParser\Comment $comment): bool => preg_match(
@ -116,6 +120,9 @@ final class LibraryImportStubGenerator
$members = [];
foreach ($stmt->stmts as $member) {
if ($member instanceof Node\Stmt\ClassMethod) {
if ($this->hasNoExportAttribute($member)) {
continue;
}
if (!($member->flags & Modifiers::ABSTRACT)
&& !($stmt instanceof Node\Stmt\Interface_)) {
$member->stmts = [];
@ -144,4 +151,21 @@ final class LibraryImportStubGenerator
return null;
}
private function hasNoExportAttribute(Node $node): bool
{
if (!property_exists($node, 'attrGroups')) {
return false;
}
foreach ($node->attrGroups as $group) {
foreach ($group->attrs as $attribute) {
$parts = $attribute->name->getParts();
if (count($parts) === 1 && strcasecmp($parts[0], 'NoExport') === 0) {
return true;
}
}
}
return false;
}
}

@ -29,6 +29,7 @@ use PhpParser\Node\UnionType;
use PhpParser\NodeAbstract;
use PhpParser\NodeFinder;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
class Preprocessor extends CompilerBase
{
@ -136,6 +137,7 @@ class Preprocessor extends CompilerBase
}
$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
$traverser->addVisitor(new Visitor());
$stmts = $traverser->traverse($ast);
$this->validateUnsupportedAttributeArguments($stmts);
@ -216,6 +218,37 @@ class Preprocessor extends CompilerBase
return false;
}
private function hasNoExportAttribute(NodeAbstract $node): bool
{
foreach ($node->attrGroups as $group) {
foreach ($group->attrs as $attribute) {
if (!$this->isRootCompileTimeAttribute($attribute, 'NoExport')) {
continue;
}
if ($attribute->args !== []) {
$this->fatalError($attribute, 'NoExport does not accept arguments');
}
return true;
}
}
return false;
}
private function isRootCompileTimeAttribute(Node\Attribute $attribute, string $name): bool
{
return strcasecmp($this->getResolvedPhpName($attribute->name), $name) === 0;
}
private function getResolvedPhpName(Node\Name $name): string
{
$resolvedName = $name->getAttribute('resolvedName')
?? $name->getAttribute('namespacedName')
?? $name;
return ltrim($resolvedName->toString(), '\\');
}
private function getExternalImportLibraryName(string $stubFile): string
{
$name = basename($stubFile, '.stub.php');
@ -500,6 +533,7 @@ class Preprocessor extends CompilerBase
}
$functionDef = new FunctionDef($fnName, $returnType, $this->namespace);
$functionDef->exported = !($this->classDef?->exported === false || $this->hasNoExportAttribute($v));
$functionDef->returnClass = $class;
// Record late-bound return type keywords so they can be re-resolved to
// the consuming class when a trait method is flattened into a class.
@ -627,6 +661,7 @@ class Preprocessor extends CompilerBase
}
$this->classDef = new ClassDef($this->class, $flags, $this->namespace);
$this->classDef->exported = !$this->hasNoExportAttribute($class);
$this->classDef->extensionProviderTarget = $this->parseExtensionProviderTarget($class);
$this->addClass($fullClassName, $this->classDef);
@ -723,8 +758,7 @@ class Preprocessor extends CompilerBase
{
foreach ($class->attrGroups as $groupIndex => $group) {
foreach ($group->attrs as $attributeIndex => $attribute) {
$parts = $attribute->name->getParts();
if (strtolower((string) end($parts)) !== 'extensionprovider') {
if (!$this->isRootCompileTimeAttribute($attribute, 'ExtensionProvider')) {
continue;
}
if (!$class instanceof Node\Stmt\Class_) {
@ -754,10 +788,10 @@ class Preprocessor extends CompilerBase
if ($value instanceof Node\Expr\ClassConstFetch
&& $this->isNameExpr($value->class)
&& $this->isIdExpr($value->name)) {
$class = ltrim($value->class->toString(), '\\');
$class = $this->getResolvedPhpName($value->class);
$constant = $value->name->toString();
if (strtolower($constant) === 'class') {
return $this->getNamespacedClassName($value->class->toString());
return $class;
}
$targets = [
'Int' => Type::INT,

@ -43,6 +43,7 @@ use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\NodeAbstract;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
class Translator extends Preprocessor
{
@ -1649,7 +1650,7 @@ CODE;
if ($this->isImportedFunction($function)) {
return $this->getNamedLibraryImportMacroName($function->importLibrary) . ' ';
}
if ($this->isBuildModeLib()) {
if ($this->isBuildModeLib() && $function->exported) {
return $this->getLibraryApiMacroName() . ' ';
}
return 'extern ';
@ -2285,6 +2286,7 @@ CODE;
$ast = $this->parser->parse($phpCode);
$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
$traverser->addVisitor(new Visitor());
$stmts = $traverser->traverse($ast);

@ -3583,7 +3583,9 @@ class AttributeInfo {
foreach ($attributeGroups as $attrGroup) {
foreach ($attrGroup->attrs as $attr) {
$parts = $attr->name->getParts();
if (strtolower((string) end($parts)) === 'extensionprovider') {
$compileTimeAttribute = count($parts) === 1
&& in_array(strtolower($parts[0]), ['extensionprovider', 'noexport'], true);
if ($compileTimeAttribute) {
continue;
}
$attributes[] = new AttributeInfo($attr->name->toString(), $attr->args);
@ -4506,7 +4508,10 @@ class FileInfo {
$parser = (new PhpParser\ParserFactory())->createForVersion(PhpParser\PhpVersion::fromString($phpVersion));
$nodeTraverser = new PhpParser\NodeTraverser;
$nodeTraverser->addVisitor(new TypePhp\Transform\Visitor());
$nodeTraverser->addVisitor(new PhpParser\NodeVisitor\NameResolver);
$nodeTraverser->addVisitor(new PhpParser\NodeVisitor\NameResolver(
null,
['preserveOriginalNames' => true]
));
$prettyPrinter = new class extends Standard {
protected function pName_FullyQualified(PhpParser\Node\Name\FullyQualified $node): string {
return implode('\\', $node->getParts());

@ -14,6 +14,11 @@ final readonly class ExtensionProvider
}
}
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
final readonly class NoExport
{
}
/**
* Public compile-time type symbols shared by extension providers and std containers.
* This root class is deliberately distinct from the compiler-internal TypePhp\Type.

Loading…
Cancel
Save