feat(compiler): add library-specific stub function handling

- Add stubLibrary property to CompilerBase to track owning library
- Implement @typephp-library annotation parsing in stub files
- Generate library-specific API macros for imported functions
- Add automatic linking of referenced libraries during compilation
- Update function declaration prefixes based on import/export status
- Create test case for cross-library stub function imports
- Document new @typephp-library annotation usage in documentation
- Modify DefaultArgumentGenerator to skip imported functions
- Add library tracking to FunctionDef entity structure
pull/34/head
韩天峰 1 month ago
parent 44899e18d4
commit 11e89428d0
  1. 13
      docs/MIXED_CPP_PHP.md
  2. 2
      examples/prime/php-src/vector.stub.php
  3. 9
      phpunit/code/compiler_api/default_argument_abi.stub.php
  4. 42
      phpunit/src/CompilerBaseApiTest.php
  5. 2
      src/CompilerBase.php
  6. 2
      src/Entity/FunctionDef.php
  7. 7
      src/Generator/DefaultArgumentGenerator.php
  8. 31
      src/Preprocessor.php
  9. 77
      src/Translator.php

@ -686,6 +686,19 @@ void php_print(php::Str msg); // void 不支持
### 3. .stub.php 文件要求 ### 3. .stub.php 文件要求
当 stub 中的函数由一个 TypePHP 库提供时,在文件顶部声明库名:
```php
<?php
/** @typephp-library prime2 */
function vector_new(int $size, bool $init = false): mixed {}
```
- 当当前 target 为 `prime2` 且使用 `-m lib` 构建时,这些 `php_*` 函数按库 ABI 导出。
- 当其他 target 引用该 stub 时,函数按 `prime2` 库 ABI 导入,且链接阶段自动加入 `prime2` 库。
- 注解只允许用于 `.stub.php` 文件;未声明时,stub 函数默认由当前 target 实现。
**正确**: **正确**:
```php ```php
<?php <?php

@ -1,4 +1,6 @@
<?php <?php
/** @typephp-library prime2 */
function vector_new(int $size, bool $init = false): mixed function vector_new(int $size, bool $init = false): mixed
{ {

@ -1,4 +1,5 @@
<?php <?php
/** @typephp-library prime2 */
const EXPORTED_ABI_INT = 42; const EXPORTED_ABI_INT = 42;
const EXPORTED_ABI_STRING = 'internal'; const EXPORTED_ABI_STRING = 'internal';
@ -10,10 +11,6 @@ function exported_defaults(
mixed $value = null, mixed $value = null,
int $count = 0, int $count = 0,
bool $enabled = false bool $enabled = false
): array { ): array {}
return [$text, $options, $value, $count, $enabled];
}
function exported_variadic(string ...$values): array { function exported_variadic(string ...$values): array {}
return $values;
}

@ -836,9 +836,9 @@ YAML);
global $translator; global $translator;
$translator = $this->compiler; $translator = $this->compiler;
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB); $this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB);
$this->compiler->setTargetName('abi_defaults'); $this->compiler->setTargetName('prime2');
$testFile = ROOT_PATH . '/phpunit/code/compiler_api/default_argument_abi.php'; $testFile = ROOT_PATH . '/phpunit/code/compiler_api/default_argument_abi.stub.php';
$this->compiler->addFiles([$testFile]); $this->compiler->addFiles([$testFile]);
$this->compiler->prepareFile($testFile); $this->compiler->prepareFile($testFile);
$this->compiler->convertFile($testFile); $this->compiler->convertFile($testFile);
@ -848,10 +848,10 @@ YAML);
$header = file_get_contents($headerFile); $header = file_get_contents($headerFile);
$this->assertStringContainsString('#pragma once', $header); $this->assertStringContainsString('#pragma once', $header);
$this->assertStringContainsString('TYPEPHP_ABI_DEFAULTS_API __declspec(dllexport)', $header); $this->assertStringContainsString('TYPEPHP_PRIME2_API __declspec(dllexport)', $header);
$this->assertStringContainsString('TYPEPHP_ABI_DEFAULTS_API __declspec(dllimport)', $header); $this->assertStringContainsString('TYPEPHP_PRIME2_API __declspec(dllimport)', $header);
$this->assertStringContainsString( $this->assertStringContainsString(
'TYPEPHP_ABI_DEFAULTS_API php::Str php_exported_defaults_arg_0_default_value();', 'TYPEPHP_PRIME2_API php::Str php_exported_defaults_arg_0_default_value();',
$header $header
); );
$this->assertStringContainsString( $this->assertStringContainsString(
@ -859,7 +859,7 @@ YAML);
$header $header
); );
$this->assertStringContainsString( $this->assertStringContainsString(
'TYPEPHP_ABI_DEFAULTS_API php::Array php_exported_variadic_arg_0_default_value();', 'TYPEPHP_PRIME2_API php::Array php_exported_variadic_arg_0_default_value();',
$header $header
); );
$this->assertStringNotContainsString('_literal_strings', $header); $this->assertStringNotContainsString('_literal_strings', $header);
@ -883,6 +883,36 @@ YAML);
$this->assertStringContainsString('php::Array php_exported_variadic_arg_0_default_value() {', $extension); $this->assertStringContainsString('php::Array php_exported_variadic_arg_0_default_value() {', $extension);
} }
public function testTypePhpLibraryStubFunctionsAreImportedOutsideOwningTarget(): void
{
global $translator;
$translator = $this->compiler;
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB);
$this->compiler->setTargetName('consumer');
$testFile = ROOT_PATH . '/phpunit/code/compiler_api/default_argument_abi.stub.php';
$this->compiler->addFiles([$testFile]);
$this->compiler->prepareFile($testFile);
$this->compiler->convertFile($testFile);
$headerFile = $this->testDir . '/php_consumer_func_decl.h';
$this->compiler->genFunctionDeclaration($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_API 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']);
}
public function testLibraryCompileOptionsExportOnlyPublicApiByDefault(): void public function testLibraryCompileOptionsExportOnlyPublicApiByDefault(): void
{ {
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB); $this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB);

@ -369,6 +369,7 @@ class CompilerBase implements PropertyAccessContext
protected int $debugLine = 0; protected int $debugLine = 0;
protected CLImate $climate; protected CLImate $climate;
protected bool $stubFile = false; protected bool $stubFile = false;
protected string $stubLibrary = '';
protected bool $enableProfiler = false; protected bool $enableProfiler = false;
protected bool $noProgress = false; protected bool $noProgress = false;
protected bool $forTest = false; protected bool $forTest = false;
@ -999,6 +1000,7 @@ class CompilerBase implements PropertyAccessContext
$this->classesDefineInFile = []; $this->classesDefineInFile = [];
$this->interfacesDefineInFile = []; $this->interfacesDefineInFile = [];
$this->functionDefineInFile = []; $this->functionDefineInFile = [];
$this->stubLibrary = '';
} }
protected function resetNamespace(): void protected function resetNamespace(): void

@ -25,6 +25,8 @@ class FunctionDef
public string $namespace; public string $namespace;
public bool $method = false; public bool $method = false;
public bool $stub = false; public bool $stub = false;
/** Library that owns this stub function, as declared by @typephp-library. */
public string $library = '';
public bool $returnTypeUndeclared = false; public bool $returnTypeUndeclared = false;
public bool $returnsByRef = false; public bool $returnsByRef = false;
public bool $generator = false; public bool $generator = false;

@ -43,7 +43,7 @@ trait DefaultArgumentGenerator
return "do {\n" . $plan->init . $body . $plan->clean . "} while (0);\n"; return "do {\n" . $plan->init . $body . $plan->clean . "} while (0);\n";
} }
protected function genDefaultArgumentHelperDeclarations(string $declarationPrefix): string protected function genDefaultArgumentHelperDeclarations(): string
{ {
$code = ''; $code = '';
foreach ($this->symbols->functions() as $nativeName => $func) { foreach ($this->symbols->functions() as $nativeName => $func) {
@ -54,7 +54,7 @@ trait DefaultArgumentGenerator
$type = $this->getDefaultArgumentHelperType($argInfo); $type = $this->getDefaultArgumentHelperType($argInfo);
$helper = $this->getDefaultArgumentHelperName($nativeName, $argumentIndex); $helper = $this->getDefaultArgumentHelperName($nativeName, $argumentIndex);
$code .= $declarationPrefix . $type . ' ' . $helper . '();' . PHP_EOL; $code .= $this->getFunctionDeclarationPrefix($func) . $type . ' ' . $helper . '();' . PHP_EOL;
} }
} }
@ -65,6 +65,9 @@ trait DefaultArgumentGenerator
{ {
$code = ''; $code = '';
foreach ($this->symbols->functions() as $nativeName => $func) { foreach ($this->symbols->functions() as $nativeName => $func) {
if ($this->isImportedFunction($func)) {
continue;
}
foreach ($func->argInfoList as $argumentIndex => $argInfo) { foreach ($func->argInfoList as $argumentIndex => $argInfo) {
if (!$this->shouldGenerateDefaultArgumentHelper($argInfo)) { if (!$this->shouldGenerateDefaultArgumentHelper($argInfo)) {
continue; continue;

@ -125,6 +125,12 @@ class Preprocessor extends CompilerBase
throw new SyntaxError($e->getMessage(), $e->getCode()); 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;
}
$traverser = new NodeTraverser(); $traverser = new NodeTraverser();
$traverser->addVisitor(new Visitor()); $traverser->addVisitor(new Visitor());
$stmts = $traverser->traverse($ast); $stmts = $traverser->traverse($ast);
@ -192,6 +198,30 @@ class Preprocessor extends CompilerBase
} }
} }
/** @param array<Node> $stmts */
private function parseTypePhpLibrary(array $stmts): string
{
$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');
}
$library = $candidate;
}
}
return $library;
}
protected function findSymbolUsing(NodeAbstract $ast) protected function findSymbolUsing(NodeAbstract $ast)
{ {
$nodeFinder = new NodeFinder(); $nodeFinder = new NodeFinder();
@ -468,6 +498,7 @@ class Preprocessor extends CompilerBase
// the consuming class when a trait method is flattened into a class. // the consuming class when a trait method is flattened into a class.
$functionDef->returnTypeKeyword = $returnTypeKeyword; $functionDef->returnTypeKeyword = $returnTypeKeyword;
$functionDef->stub = $this->stubFile; $functionDef->stub = $this->stubFile;
$functionDef->library = $this->stubLibrary;
$functionDef->returnTypeUndeclared = $v->returnType === null; $functionDef->returnTypeUndeclared = $v->returnType === null;
$functionDef->returnsByRef = $v->byRef; $functionDef->returnsByRef = $v->byRef;
if ($this->containsYield($v)) { if ($this->containsYield($v)) {

@ -1545,27 +1545,23 @@ CODE;
$code .= '#include <phpx.h>' . PHP_EOL; $code .= '#include <phpx.h>' . PHP_EOL;
$code .= '#include <typephp_fiber_generator.h>' . PHP_EOL; $code .= '#include <typephp_fiber_generator.h>' . PHP_EOL;
$declarationPrefix = 'extern '; $apiLibraries = [];
if ($this->isBuildModeLib()) { if ($this->isBuildModeLib()) {
$apiMacro = $this->getLibraryApiMacroName(); $apiLibraries[$this->targetName] = true;
$exportsMacro = $this->getLibraryExportsMacroName(); }
$code .= "#if defined(_WIN32)\n"; foreach ($this->symbols->functions() as $function) {
$code .= "# if defined({$exportsMacro})\n"; if ($this->isImportedFunction($function)) {
$code .= "# define {$apiMacro} __declspec(dllexport)\n"; $apiLibraries[$function->library] = true;
$code .= "# else\n"; }
$code .= "# define {$apiMacro} __declspec(dllimport)\n"; }
$code .= "# endif\n"; foreach (array_keys($apiLibraries) as $library) {
$code .= "#elif defined(__GNUC__) && __GNUC__ >= 4\n"; $code .= $this->genLibraryApiMacro($library);
$code .= "# define {$apiMacro} __attribute__((visibility(\"default\")))\n";
$code .= "#else\n";
$code .= "# define {$apiMacro}\n";
$code .= "#endif\n\n";
$declarationPrefix = $apiMacro . ' ';
} }
$code .= $this->genDefaultArgumentHelperDeclarations($declarationPrefix); $code .= $this->genDefaultArgumentHelperDeclarations();
foreach ($this->symbols->functions() as $name => $func) { foreach ($this->symbols->functions() as $name => $func) {
$functionDeclarationPrefix = $this->getFunctionDeclarationPrefix($func);
$list = []; $list = [];
if ($func->method) { if ($func->method) {
$list[] = Type::OBJECT . ' &this_'; $list[] = Type::OBJECT . ' &this_';
@ -1586,10 +1582,10 @@ CODE;
} }
} }
$params = implode(', ', $list); $params = implode(', ', $list);
$code .= $declarationPrefix . ($func->returnsByRef ? Type::REF : $func->returnType) . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL; $code .= $functionDeclarationPrefix . ($func->returnsByRef ? Type::REF : $func->returnType) . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL;
if ($func->hasMultiReturn()) { if ($func->hasMultiReturn()) {
$code .= 'namespace ' . self::MULTI_RETURN_NAMESPACE . ' {' . PHP_EOL; $code .= 'namespace ' . self::MULTI_RETURN_NAMESPACE . ' {' . PHP_EOL;
$code .= $declarationPrefix . $func->getMultiReturnCppType() . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL; $code .= $functionDeclarationPrefix . $func->getMultiReturnCppType() . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL;
$code .= '}' . PHP_EOL; $code .= '}' . PHP_EOL;
} }
} }
@ -1602,9 +1598,52 @@ CODE;
return 'TYPEPHP_' . strtoupper($this->targetName) . '_API'; return 'TYPEPHP_' . strtoupper($this->targetName) . '_API';
} }
protected function genLibraryApiMacro(string $library): string
{
$apiMacro = $this->getNamedLibraryApiMacroName($library);
$exportsMacro = $this->getNamedLibraryExportsMacroName($library);
$code = "#if defined(_WIN32)\n";
$code .= "# if defined({$exportsMacro})\n";
$code .= "# define {$apiMacro} __declspec(dllexport)\n";
$code .= "# else\n";
$code .= "# define {$apiMacro} __declspec(dllimport)\n";
$code .= "# endif\n";
$code .= "#elif defined(__GNUC__) && __GNUC__ >= 4\n";
$code .= "# define {$apiMacro} __attribute__((visibility(\"default\")))\n";
$code .= "#else\n";
$code .= "# define {$apiMacro}\n";
return $code . "#endif\n\n";
}
protected function getFunctionDeclarationPrefix(FunctionDef $function): string
{
if ($this->isImportedFunction($function)) {
return $this->getNamedLibraryApiMacroName($function->library) . ' ';
}
if ($this->isBuildModeLib()) {
return $this->getLibraryApiMacroName() . ' ';
}
return 'extern ';
}
protected function isImportedFunction(FunctionDef $function): bool
{
return $function->library !== '' && $function->library !== $this->targetName;
}
protected function getNamedLibraryApiMacroName(string $library): string
{
return 'TYPEPHP_' . strtoupper($library) . '_API';
}
protected function getNamedLibraryExportsMacroName(string $library): string
{
return 'TYPEPHP_' . strtoupper($library) . '_EXPORTS';
}
protected function getLibraryExportsMacroName(): string protected function getLibraryExportsMacroName(): string
{ {
return 'TYPEPHP_' . strtoupper($this->targetName) . '_EXPORTS'; return $this->getNamedLibraryExportsMacroName($this->targetName);
} }
public function getBuildMode(): string public function getBuildMode(): string

Loading…
Cancel
Save