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. 4
      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. 81
      src/Translator.php

@ -686,6 +686,19 @@ void php_print(php::Str msg); // void 不支持
### 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

@ -1,4 +1,6 @@
<?php
/** @typephp-library prime2 */
function vector_new(int $size, bool $init = false): mixed
{
@ -12,4 +14,4 @@ function vector_get(mixed $vector, int $offset): bool
function vector_set(mixed $vector, int $offset, bool $value): void
{
}
}

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

@ -836,9 +836,9 @@ YAML);
global $translator;
$translator = $this->compiler;
$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->prepareFile($testFile);
$this->compiler->convertFile($testFile);
@ -848,10 +848,10 @@ YAML);
$header = file_get_contents($headerFile);
$this->assertStringContainsString('#pragma once', $header);
$this->assertStringContainsString('TYPEPHP_ABI_DEFAULTS_API __declspec(dllexport)', $header);
$this->assertStringContainsString('TYPEPHP_ABI_DEFAULTS_API __declspec(dllimport)', $header);
$this->assertStringContainsString('TYPEPHP_PRIME2_API __declspec(dllexport)', $header);
$this->assertStringContainsString('TYPEPHP_PRIME2_API __declspec(dllimport)', $header);
$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
);
$this->assertStringContainsString(
@ -859,7 +859,7 @@ YAML);
$header
);
$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
);
$this->assertStringNotContainsString('_literal_strings', $header);
@ -883,6 +883,36 @@ YAML);
$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
{
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB);

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

@ -25,6 +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 = '';
public bool $returnTypeUndeclared = false;
public bool $returnsByRef = false;
public bool $generator = false;

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

@ -125,6 +125,12 @@ 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;
}
$traverser = new NodeTraverser();
$traverser->addVisitor(new Visitor());
$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)
{
$nodeFinder = new NodeFinder();
@ -468,6 +498,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->returnTypeUndeclared = $v->returnType === null;
$functionDef->returnsByRef = $v->byRef;
if ($this->containsYield($v)) {

@ -1545,27 +1545,23 @@ CODE;
$code .= '#include <phpx.h>' . PHP_EOL;
$code .= '#include <typephp_fiber_generator.h>' . PHP_EOL;
$declarationPrefix = 'extern ';
$apiLibraries = [];
if ($this->isBuildModeLib()) {
$apiMacro = $this->getLibraryApiMacroName();
$exportsMacro = $this->getLibraryExportsMacroName();
$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";
$code .= "#endif\n\n";
$declarationPrefix = $apiMacro . ' ';
}
$code .= $this->genDefaultArgumentHelperDeclarations($declarationPrefix);
$apiLibraries[$this->targetName] = true;
}
foreach ($this->symbols->functions() as $function) {
if ($this->isImportedFunction($function)) {
$apiLibraries[$function->library] = true;
}
}
foreach (array_keys($apiLibraries) as $library) {
$code .= $this->genLibraryApiMacro($library);
}
$code .= $this->genDefaultArgumentHelperDeclarations();
foreach ($this->symbols->functions() as $name => $func) {
$functionDeclarationPrefix = $this->getFunctionDeclarationPrefix($func);
$list = [];
if ($func->method) {
$list[] = Type::OBJECT . ' &this_';
@ -1586,10 +1582,10 @@ CODE;
}
}
$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()) {
$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;
}
}
@ -1602,9 +1598,52 @@ CODE;
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
{
return 'TYPEPHP_' . strtoupper($this->targetName) . '_EXPORTS';
return $this->getNamedLibraryExportsMacroName($this->targetName);
}
public function getBuildMode(): string

Loading…
Cancel
Save