diff --git a/docs/MIXED_CPP_PHP.md b/docs/MIXED_CPP_PHP.md index 784aa95d..ff333684 100644 --- a/docs/MIXED_CPP_PHP.md +++ b/docs/MIXED_CPP_PHP.md @@ -686,25 +686,24 @@ void php_print(php::Str msg); // void 不支持 ### 3. .stub.php 文件要求 -当 stub 中的函数由一个 TypePHP 库提供时,在文件顶部声明库名: +库项目中的 `.stub.php` 用于声明由 C++ 实现的函数,不需要添加库名注解: ```php .stub.php`。该发布 stub 自动带有 `@import-library`,其他项目加载后,其中的所有函数和类方法都按外部库 ABI 导入。库名由文件名推导,例如 `prime2.stub.php` 对应 `prime2` 库。 + +外部 stub 中的类会在消费项目中生成类注册、属性和常量实体,但不生成 `php_*` 方法本体;方法本体由动态库提供。 +Property hook 同样按方法处理:发布 stub 保留 `get`/`set` 的声明并移除实现,消费项目生成属性实体,hook 的 getter/setter `php_*` 实现从动态库导入。 `php__func_decl.h` 和 `php__data_decl.h` 都是 TypePHP 构建过程的内部生成文件,不是库的对外开发头文件。 `func_decl.h` 在 `-m lib` 构建时还会被强制包含,用于给当前 target 的 `php_*` C++ ABI 函数添加平台导出标记;`data_decl.h` 仅在 target 内部声明全局变量、字面量、常量对象和运行时映射等数据。 发布 TypePHP 库时,对外提供: -- 描述 TypePHP 函数接口和所属库的 `.stub.php`; +- 由 `-m lib` 自动生成的 `.stub.php`; - Windows 平台的 `.dll` 和导入库 `.lib`; - Linux 等平台的 `.so`。 diff --git a/examples/prime/php-src/vector.stub.php b/examples/prime/php-src/vector.stub.php index d0dc7e65..296c3e0d 100644 --- a/examples/prime/php-src/vector.stub.php +++ b/examples/prime/php-src/vector.stub.php @@ -1,5 +1,4 @@ value * 2; + } + set(int $value) { + $this->value = intdiv($value, 2); + } + } + + public function add(int $amount = self::STEP): int + { + $this->value += $amount; + return $this->value; + } +} + +function twice(int $value): int +{ + return $value * 2; +} diff --git a/phpunit/code/compiler_api/prime2.stub.php b/phpunit/code/compiler_api/prime2.stub.php new file mode 100644 index 00000000..34615f1a --- /dev/null +++ b/phpunit/code/compiler_api/prime2.stub.php @@ -0,0 +1,13 @@ + 'fast'], + mixed $value = null, + int $count = 0, + bool $enabled = false +): array {} + +function exported_variadic(string ...$values): array {} diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index a8b21e16..8083e984 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -883,34 +883,128 @@ YAML); $this->assertStringContainsString('php::Array php_exported_variadic_arg_0_default_value() {', $extension); } - public function testTypePhpLibraryStubFunctionsAreImportedOutsideOwningTarget(): void + public function testExternalImportStubFunctionsAreAlwaysImported(): void { global $translator; $translator = $this->compiler; $this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB); - $this->compiler->setTargetName('consumer'); + $this->compiler->setTargetName('prime2'); - $testFile = ROOT_PATH . '/phpunit/code/compiler_api/default_argument_abi.stub.php'; + $testFile = ROOT_PATH . '/phpunit/code/compiler_api/prime2.stub.php'; $this->compiler->addFiles([$testFile]); $this->compiler->prepareFile($testFile); $this->compiler->convertFile($testFile); - $headerFile = $this->testDir . '/php_consumer_func_decl.h'; + $headerFile = $this->testDir . '/php_prime2_func_decl.h'; $this->compiler->genFunctionDeclarations($headerFile); $header = file_get_contents($headerFile); - $this->assertStringContainsString('TYPEPHP_PRIME2_API __declspec(dllimport)', $header); - $this->assertStringContainsString('TYPEPHP_CONSUMER_API __declspec(dllexport)', $header); + $this->assertStringContainsString('TYPEPHP_PRIME2_IMPORT __declspec(dllimport)', $header); + $this->assertStringContainsString('TYPEPHP_PRIME2_API __declspec(dllexport)', $header); $this->assertStringContainsString( - 'TYPEPHP_PRIME2_API php::Array php_exported_defaults(', + 'TYPEPHP_PRIME2_IMPORT php::Array php_exported_defaults(', $header ); $this->assertSame(['prime2'], $this->getPropertyValue('linkLibs')); $this->assertSame('', $this->invokeMethod('genDefaultArgumentHelperDefinitions')); $options = $this->invokeMethod('getCompileCommandOptions'); - $this->assertContains('TYPEPHP_CONSUMER_EXPORTS=1', $options['user_defines']); - $this->assertNotContains('TYPEPHP_PRIME2_EXPORTS=1', $options['user_defines']); + $this->assertContains('TYPEPHP_PRIME2_EXPORTS=1', $options['user_defines']); + } + + public function testLibraryImportStubCombinesPhpFunctionsClassesAndNativeStubs(): void + { + global $translator; + $translator = $this->compiler; + $this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB); + $this->setPropertyValue('outputDir', $this->testDir); + $this->compiler->setTargetName('prime2'); + + $files = [ + ROOT_PATH . '/phpunit/code/compiler_api/library_import_php.php', + ROOT_PATH . '/phpunit/code/compiler_api/library_import_native.stub.php', + ]; + $this->compiler->addFiles($files); + foreach ($files as $file) { + $this->compiler->prepareFile($file); + $this->compiler->convertFile($file); + } + + $stubFile = $this->compiler->genLibraryImportStub($files); + $stub = file_get_contents($stubFile); + $this->assertSame($this->testDir . '/prime2.stub.php', $stubFile); + $this->assertStringContainsString('/** @import-library */', $stub); + $this->assertStringContainsString('namespace LibraryApi;', $stub); + $this->assertStringContainsString('class Counter', $stub); + $this->assertStringContainsString('public const int STEP = 2;', $stub); + $this->assertStringContainsString('public int $value = 1;', $stub); + $this->assertMatchesRegularExpression( + '/public int \$doubled\s*\{\s*get\s*\{\s*\}\s*set\(int \$value\)\s*\{\s*\}\s*\}/s', + $stub, + ); + $this->assertStringContainsString('function add(int $amount = self::STEP): int', $stub); + $this->assertStringContainsString('function twice(int $value): int', $stub); + $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('return $this->value', $stub); + $this->assertStringNotContainsString('intdiv($value, 2)', $stub); + $this->assertStringNotContainsString('return $value * 2', $stub); + + $consumerDir = $this->testDir . '/consumer'; + mkdir($consumerDir, 0777, true); + $consumer = CompilerTest::create($consumerDir); + $translator = $consumer; + $consumerRef = new \ReflectionClass($consumer); + $buildMode = $consumerRef->getProperty('buildMode'); + $buildMode->setAccessible(true); + $buildMode->setValue($consumer, CompilerBase::BUILD_MODE_BIN); + $consumer->setTargetName('consumer'); + $consumer->addFiles([$stubFile]); + $consumer->prepareFile($stubFile); + $stubCpp = $consumer->convertFile($stubFile); + + $headerFile = $consumerDir . '/php_consumer_func_decl.h'; + $consumer->genFunctionDeclarations($headerFile); + $header = file_get_contents($headerFile); + $this->assertStringContainsString( + 'TYPEPHP_PRIME2_IMPORT php::Int php_libraryapi__counter__add(', + $header, + ); + $this->assertStringContainsString( + 'TYPEPHP_PRIME2_IMPORT php::Int php_libraryapi__twice(', + $header, + ); + $this->assertStringContainsString( + 'TYPEPHP_PRIME2_IMPORT php::Str php_libraryapi__native_value(', + $header, + ); + $this->assertStringContainsString( + 'TYPEPHP_PRIME2_IMPORT php::Int php_libraryapi__nativecounter__bump(', + $header, + ); + $this->assertStringContainsString( + 'TYPEPHP_PRIME2_IMPORT php::Int php_libraryapi__counter____typephp_property_get_646f75626c6564(', + $header, + ); + $this->assertStringContainsString( + 'TYPEPHP_PRIME2_IMPORT void php_libraryapi__counter____typephp_property_set_646f75626c6564(', + $header, + ); + + $stubCppCode = file_get_contents($stubCpp); + $this->assertStringContainsString('ZEND_METHOD(LibraryApi_Counter, add)', $stubCppCode); + $this->assertStringContainsString('php_libraryapi__counter__add(this_, arg_amount)', $stubCppCode); + $this->assertStringNotContainsString( + 'php::Int php_libraryapi__counter__add(php::Object &this_', + $stubCppCode, + ); + + $arginfoFile = $consumer->getArgInfoHeaderFile($stubFile); + $arginfo = file_get_contents($arginfoFile); + $this->assertStringContainsString('const_STEP_value', $arginfo); + $this->assertStringContainsString('property_value_default_value', $arginfo); + $this->assertSame(['prime2'], $consumer->getLinkLibs()); } public function testLibraryCompileOptionsExportOnlyPublicApiByDefault(): void diff --git a/src/Build/SourcePipelineTrait.php b/src/Build/SourcePipelineTrait.php index 3ce3fccf..7f212e39 100644 --- a/src/Build/SourcePipelineTrait.php +++ b/src/Build/SourcePipelineTrait.php @@ -56,6 +56,19 @@ trait SourcePipelineTrait // 在所有配置加载完成后,应用命令行参数(确保优先级最高) $this->applyCommandLineArguments(); + // The generated public import stub is an output artifact, not an input + // of the library that produced it. Exclude a previous build's copy when + // a project scans its output directory recursively. + if ($this->isBuildModeLib()) { + $generatedStub = realpath($this->getLibraryImportStubFile()); + if ($generatedStub !== false) { + $list = array_values(array_filter( + $list, + static fn(string $file): bool => realpath($file) !== $generatedStub, + )); + } + } + return $this->filterIgnoredFiles($list); } @@ -193,6 +206,10 @@ trait SourcePipelineTrait $this->stop('No valid source file found'); } + if ($this->isBuildModeLib()) { + $this->genLibraryImportStub($files); + } + // 生成构建期内部头文件:函数声明、运行时数据声明 $this->genFunctionDeclarations($this->getIncludeDir() . "/php_{$this->targetName}_func_decl.h"); $this->genDataDeclarations($this->getIncludeDir() . "/php_{$this->targetName}_data_decl.h"); diff --git a/src/CompilerBase.php b/src/CompilerBase.php index ac200ce0..d1ac79c4 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -369,7 +369,10 @@ class CompilerBase implements PropertyAccessContext protected int $debugLine = 0; protected CLImate $climate; protected bool $stubFile = false; - protected string $stubLibrary = ''; + protected string $stubImportLibrary = ''; + + /** @var array */ + protected array $externalImportStubFiles = []; protected bool $enableProfiler = false; protected bool $noProgress = false; protected bool $forTest = false; @@ -1000,7 +1003,7 @@ class CompilerBase implements PropertyAccessContext $this->classesDefineInFile = []; $this->interfacesDefineInFile = []; $this->functionDefineInFile = []; - $this->stubLibrary = ''; + $this->stubImportLibrary = ''; } protected function resetNamespace(): void diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index 64698bd8..2715c1ba 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -25,8 +25,8 @@ class FunctionDef public string $namespace; public bool $method = false; public bool $stub = false; - /** Library that owns this stub function, as declared by @typephp-library. */ - public string $library = ''; + /** External library imported by the stub containing this function. */ + public string $importLibrary = ''; public bool $returnTypeUndeclared = false; public bool $returnsByRef = false; public bool $generator = false; diff --git a/src/Generator/LibraryImportStubGenerator.php b/src/Generator/LibraryImportStubGenerator.php new file mode 100644 index 00000000..88ce614a --- /dev/null +++ b/src/Generator/LibraryImportStubGenerator.php @@ -0,0 +1,147 @@ + $files + * @param array $externalImportStubFiles + */ + public function generate(array $files, array $externalImportStubFiles): string + { + /** @var array> $namespaces */ + $namespaces = []; + + foreach ($files as $file) { + $realFile = realpath($file); + if ($realFile === false || isset($externalImportStubFiles[$realFile])) { + continue; + } + if (pathinfo($realFile, PATHINFO_EXTENSION) !== 'php') { + continue; + } + + $code = file_get_contents($realFile); + if ($code === false) { + throw new \RuntimeException('Can not read file: ' . $realFile); + } + $ast = $this->parser->parse($code) ?? []; + $traverser = new NodeTraverser(); + $traverser->addVisitor(new NameResolver()); + $ast = $traverser->traverse($ast); + + foreach ($ast as $stmt) { + if ($stmt instanceof Node\Stmt\Namespace_) { + $namespace = $stmt->name?->toString() ?? ''; + $this->appendDeclarations($namespaces, $namespace, $stmt->stmts); + continue; + } + $this->appendDeclarations($namespaces, '', [$stmt]); + } + } + + $namespaceNodes = []; + foreach ($namespaces as $namespace => $stmts) { + if ($stmts === []) { + continue; + } + $namespaceNodes[] = new Node\Stmt\Namespace_( + $namespace === '' ? null : new Node\Name($namespace), + $stmts, + ); + } + + $code = "printer->prettyPrint($namespaceNodes) . "\n"; + } + return $code; + } + + /** + * @param array> $namespaces + * @param array $stmts + */ + private function appendDeclarations(array &$namespaces, string $namespace, array $stmts): void + { + foreach ($stmts as $stmt) { + if ($namespace === '' && $stmt instanceof Node\Stmt\Function_ + && strtolower($stmt->name->toString()) === 'main') { + continue; + } + $declaration = $this->makeImportDeclaration($stmt); + if ($declaration !== null) { + $namespaces[$namespace][] = $declaration; + } + } + } + + private function makeImportDeclaration(Node\Stmt $stmt): ?Node\Stmt + { + $comments = array_filter( + $stmt->getComments(), + static fn(\PhpParser\Comment $comment): bool => preg_match( + '/@import-library\b/', + $comment->getText(), + ) !== 1, + ); + $stmt->setAttribute('comments', array_values($comments)); + + if ($stmt instanceof Node\Stmt\Function_) { + $stmt->stmts = []; + return $stmt; + } + + if ($stmt instanceof Node\Stmt\ClassLike) { + $members = []; + foreach ($stmt->stmts as $member) { + if ($member instanceof Node\Stmt\ClassMethod) { + if (!($member->flags & Modifiers::ABSTRACT) + && !($stmt instanceof Node\Stmt\Interface_)) { + $member->stmts = []; + } + $members[] = $member; + continue; + } + if ($member instanceof Node\Stmt\Property) { + foreach ($member->hooks as $hook) { + if ($hook->body !== null) { + $hook->body = []; + } + } + $members[] = $member; + continue; + } + if ($member instanceof Node\Stmt\ClassConst + || $member instanceof Node\Stmt\TraitUse + || $member instanceof Node\Stmt\EnumCase) { + $members[] = $member; + } + } + $stmt->stmts = $members; + return $stmt; + } + + return null; + } +} diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 1288fbc9..17c77fe7 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -125,10 +125,14 @@ class Preprocessor extends CompilerBase throw new SyntaxError($e->getMessage(), $e->getCode()); } - $this->stubLibrary = $this->stubFile ? $this->parseTypePhpLibrary($ast) : ''; - if ($this->stubLibrary !== '' && $this->stubLibrary !== $this->targetName - && !in_array($this->stubLibrary, $this->linkLibs, true)) { - $this->linkLibs[] = $this->stubLibrary; + $this->stubImportLibrary = $this->stubFile && $this->hasLibraryImportAnnotation($ast) + ? $this->getExternalImportLibraryName($this->file) + : ''; + if ($this->stubImportLibrary !== '') { + $this->externalImportStubFiles[$this->file] = true; + } + if ($this->stubImportLibrary !== '' && !in_array($this->stubImportLibrary, $this->linkLibs, true)) { + $this->linkLibs[] = $this->stubImportLibrary; } $traverser = new NodeTraverser(); @@ -199,27 +203,28 @@ class Preprocessor extends CompilerBase } /** @param array $stmts */ - private function parseTypePhpLibrary(array $stmts): string + private function hasLibraryImportAnnotation(array $stmts): bool { - $library = ''; foreach ($stmts as $stmt) { foreach ($stmt->getComments() as $comment) { - if (!preg_match('/@typephp-library\s+([^\s*]+)/', $comment->getText(), $matches)) { - continue; - } - - $candidate = str_replace('-', '_', trim($matches[1])); - if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $candidate)) { - $this->fatalError($stmt, 'Invalid @typephp-library name `' . $matches[1] . '`'); - } - if ($library !== '' && $library !== $candidate) { - $this->fatalError($stmt, 'A stub file cannot declare multiple @typephp-library values'); + if (preg_match('/@import-library\b/', $comment->getText()) === 1) { + return true; } - $library = $candidate; } } - return $library; + return false; + } + + private function getExternalImportLibraryName(string $stubFile): string + { + $name = basename($stubFile, '.stub.php'); + $name = str_replace(['-', '*'], '_', $name); + if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name)) { + throw new SyntaxError('Invalid external import stub filename `' . basename($stubFile) . '`'); + } + + return $name; } protected function findSymbolUsing(NodeAbstract $ast) @@ -356,8 +361,9 @@ class Preprocessor extends CompilerBase } $phpName = $param->var->name; $name = $this->escapeVarName($phpName); - // .stub 存根定义 C++ Native 函数,必须设置函数的参数类型 - if ($this->stubFile and !$param->type) { + // Local stubs define C++ native functions and require explicit ABI types. + // Generated external stubs may preserve an untyped PHP declaration as php::Var. + if ($this->stubFile && $this->stubImportLibrary === '' && !$param->type) { throw new \RuntimeException('No type for ' . $phpName); } // 构造方法属性定义语法(Constructor Property Promotion) @@ -456,8 +462,9 @@ class Preprocessor extends CompilerBase protected function parseFunctionDecl(Node\Stmt\Function_|Node\Stmt\ClassMethod $v): FunctionDef { - // .stub 存根定义 C++ Native 函数,必须设置返回值类型 - if ($this->stubFile and !$v->returnType) { + // Local stubs define C++ native functions and require an explicit ABI return type. + // Generated external stubs may preserve an untyped PHP declaration as php::Var. + if ($this->stubFile && $this->stubImportLibrary === '' && !$v->returnType) { // 以下魔术方法都不能声明返回值类型 __construct()/__destruct()/__clone() if (($this->method and !in_array($this->method, ['__construct', '__destruct', '__clone'])) or !$this->method) { $name = $this->class ? $this->class . '::' . $v->name : $v->name; @@ -498,7 +505,7 @@ class Preprocessor extends CompilerBase // the consuming class when a trait method is flattened into a class. $functionDef->returnTypeKeyword = $returnTypeKeyword; $functionDef->stub = $this->stubFile; - $functionDef->library = $this->stubLibrary; + $functionDef->importLibrary = $this->stubImportLibrary; $functionDef->returnTypeUndeclared = $v->returnType === null; $functionDef->returnsByRef = $v->byRef; if ($this->containsYield($v)) { diff --git a/src/Translator.php b/src/Translator.php index 1dc8ea29..d3e53f7e 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -31,6 +31,7 @@ use TypePhp\Entity\PropertyDef; use TypePhp\Exception\Redo; use TypePhp\Exception\Skip; use TypePhp\Generator\DefaultArgumentGenerator; +use TypePhp\Generator\LibraryImportStubGenerator; use TypePhp\Generator\Symbol; use TypePhp\Metadata\Constants; use TypePhp\Platform\PlatformFactory; @@ -632,6 +633,22 @@ class Translator extends Preprocessor return $targetFile; } + public function getLibraryImportStubFile(): string + { + $directory = $this->outputDir !== '' ? $this->outputDir : (getcwd() ?: $this->rootPath); + return rtrim($directory, '/\\') . '/' . $this->targetName . '.stub.php'; + } + + /** @param array $files */ + public function genLibraryImportStub(array $files): string + { + $file = $this->getLibraryImportStubFile(); + $generator = new LibraryImportStubGenerator($this->parser, $this->printer); + $this->writeFile($file, $generator->generate($files, $this->externalImportStubFiles)); + $this->climate->info('generate library import stub: ' . $this->getRelativePath($file)); + return $file; + } + public function preprocessArgvAdvanced(): void { global $argv; @@ -1545,17 +1562,17 @@ CODE; $code .= '#include ' . PHP_EOL; $code .= '#include ' . PHP_EOL; - $apiLibraries = []; if ($this->isBuildModeLib()) { - $apiLibraries[$this->targetName] = true; + $code .= $this->genLibraryApiMacro($this->targetName); } + $importLibraries = []; foreach ($this->symbols->functions() as $function) { if ($this->isImportedFunction($function)) { - $apiLibraries[$function->library] = true; + $importLibraries[$function->importLibrary] = true; } } - foreach (array_keys($apiLibraries) as $library) { - $code .= $this->genLibraryApiMacro($library); + foreach (array_keys($importLibraries) as $library) { + $code .= $this->genLibraryImportMacro($library); } $code .= $this->genDefaultArgumentHelperDeclarations(); @@ -1615,10 +1632,22 @@ CODE; return $code . "#endif\n\n"; } + protected function genLibraryImportMacro(string $library): string + { + $importMacro = $this->getNamedLibraryImportMacroName($library); + $code = "#if defined(_WIN32)\n"; + $code .= "# define {$importMacro} __declspec(dllimport)\n"; + $code .= "#elif defined(__GNUC__) && __GNUC__ >= 4\n"; + $code .= "# define {$importMacro} __attribute__((visibility(\"default\")))\n"; + $code .= "#else\n"; + $code .= "# define {$importMacro}\n"; + return $code . "#endif\n\n"; + } + protected function getFunctionDeclarationPrefix(FunctionDef $function): string { if ($this->isImportedFunction($function)) { - return $this->getNamedLibraryApiMacroName($function->library) . ' '; + return $this->getNamedLibraryImportMacroName($function->importLibrary) . ' '; } if ($this->isBuildModeLib()) { return $this->getLibraryApiMacroName() . ' '; @@ -1628,7 +1657,7 @@ CODE; protected function isImportedFunction(FunctionDef $function): bool { - return $function->library !== '' && $function->library !== $this->targetName; + return $function->importLibrary !== ''; } protected function getNamedLibraryApiMacroName(string $library): string @@ -1636,6 +1665,11 @@ CODE; return 'TYPEPHP_' . strtoupper($library) . '_API'; } + protected function getNamedLibraryImportMacroName(string $library): string + { + return 'TYPEPHP_' . strtoupper($library) . '_IMPORT'; + } + protected function getNamedLibraryExportsMacroName(string $library): string { return 'TYPEPHP_' . strtoupper($library) . '_EXPORTS';