diff --git a/docs/MIXED_CPP_PHP.md b/docs/MIXED_CPP_PHP.md index ff333684..4ac3ff8b 100644 --- a/docs/MIXED_CPP_PHP.md +++ b/docs/MIXED_CPP_PHP.md @@ -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 {} +``` + +声明仍参与当前库编译,但不会进入 `.stub.php`,对应 `php_*` 符号也不添加 library export 修饰。类注解会级联到其全部方法;单个方法也可以独立标记。`NoExport` 位于根命名空间:全局命名空间可写 `#[NoExport]`,其他命名空间必须写 `#[\NoExport]`,并且该编译期 Attribute 不会进入运行时元数据。 + +`NoExport` 与 `ExtensionProvider` 都遵循 PHP 类名解析规则,支持完全限定名、`use` 和 `use ... as ...` 别名。只有解析结果严格指向根命名空间内建 Attribute 时,编译器才会消费它。 + `php__func_decl.h` 和 `php__data_decl.h` 都是 TypePHP 构建过程的内部生成文件,不是库的对外开发头文件。 `func_decl.h` 在 `-m lib` 构建时还会被强制包含,用于给当前 target 的 `php_*` C++ ABI 函数添加平台导出标记;`data_decl.h` 仅在 target 内部声明全局变量、字面量、常量对象和运行时映射等数据。 diff --git a/phpunit/code/compiler_api/library_import_global.php b/phpunit/code/compiler_api/library_import_global.php new file mode 100644 index 00000000..d85515da --- /dev/null +++ b/phpunit/code/compiler_api/library_import_global.php @@ -0,0 +1,7 @@ +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; +} diff --git a/phpunit/code/compiler_api/library_no_export_namespace.php b/phpunit/code/compiler_api/library_no_export_namespace.php new file mode 100644 index 00000000..4b8ec4ef --- /dev/null +++ b/phpunit/code/compiler_api/library_no_export_namespace.php @@ -0,0 +1,36 @@ +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); diff --git a/src/Entity/ClassDef.php b/src/Entity/ClassDef.php index 54d3215b..ae6f4649 100644 --- a/src/Entity/ClassDef.php +++ b/src/Entity/ClassDef.php @@ -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; /** diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index 2715c1ba..53929c3a 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -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; diff --git a/src/Generator/LibraryImportStubGenerator.php b/src/Generator/LibraryImportStubGenerator.php index 88ce614a..3cffb546 100644 --- a/src/Generator/LibraryImportStubGenerator.php +++ b/src/Generator/LibraryImportStubGenerator.php @@ -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; + } } diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 17c77fe7..c94bde60 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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, diff --git a/src/Translator.php b/src/Translator.php index d3e53f7e..ecce536d 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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); diff --git a/src/gen_stub.php b/src/gen_stub.php index c19ee29a..cc408c51 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -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()); diff --git a/src/polyfills.php b/src/polyfills.php index f6478689..1b823c52 100644 --- a/src/polyfills.php +++ b/src/polyfills.php @@ -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.