feat(compiler): add library function header exports and default value helpers

- Implement test case for library function header exports with default value helpers
- Add test case for library compile options to export only public API by default
- Generate default argument helper declarations and definitions for library builds
- Add platform-specific visibility control for library symbols on non-Windows
- Implement forced include headers for library compilation mode
- Add DLL import/export macros for Windows library builds
- Support variadic arguments with default value helpers
- Update precompiled header compilation options to exclude forced includes
- Add pragma once directive to generated function declaration headers
- Implement proper conditional compilation for different platforms and build modes
pull/34/head
韩天峰 1 month ago
parent 576222998c
commit bb113b2186
  1. 15
      phpunit/code/compiler_api/default_argument_abi.php
  2. 58
      phpunit/src/CompilerBaseApiTest.php
  3. 9
      src/Backend/GccLikeBackend.php
  4. 4
      src/Backend/Msvc.php
  5. 15
      src/Build/NativeCommandOptionsTrait.php
  6. 81
      src/Generator/DefaultArgumentGenerator.php
  7. 63
      src/Translator.php

@ -0,0 +1,15 @@
<?php
function exported_defaults(
string $text = 'hello',
array $options = ['mode' => 'fast'],
mixed $value = null,
int $count = 0,
bool $enabled = false
): array {
return [$text, $options, $value, $count, $enabled];
}
function exported_variadic(string ...$values): array {
return $values;
}

@ -831,6 +831,64 @@ YAML);
$this->assertStringNotContainsString('property_map = {}', $code);
}
public function testLibraryFunctionHeaderExportsDefaultValueHelpersWithoutLiteralStorage(): void
{
global $translator;
$translator = $this->compiler;
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB);
$this->compiler->setTargetName('abi_defaults');
$testFile = ROOT_PATH . '/phpunit/code/compiler_api/default_argument_abi.php';
$this->compiler->addFiles([$testFile]);
$this->compiler->prepareFile($testFile);
$this->compiler->convertFile($testFile);
$headerFile = $this->testDir . '/php_abi_defaults_func_decl.h';
$this->compiler->genFunctionDeclaration($headerFile);
$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_ABI_DEFAULTS_API php::Str php_exported_defaults_arg_0_default_value();',
$header
);
$this->assertStringContainsString(
'php::Str text = php_exported_defaults_arg_0_default_value()',
$header
);
$this->assertStringContainsString(
'TYPEPHP_ABI_DEFAULTS_API php::Array php_exported_variadic_arg_0_default_value();',
$header
);
$this->assertStringNotContainsString('_literal_strings', $header);
$this->assertStringNotContainsString('php_func_map', $header);
$this->assertStringNotContainsString('php_class_map', $header);
$extensionFile = $this->compiler->genExtension();
$extension = file_get_contents($extensionFile);
$this->assertStringContainsString('php::Str php_exported_defaults_arg_0_default_value() {', $extension);
$this->assertStringContainsString('return _literal_strings[', $extension);
$this->assertStringContainsString('php::Array php_exported_variadic_arg_0_default_value() {', $extension);
}
public function testLibraryCompileOptionsExportOnlyPublicApiByDefault(): void
{
$this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB);
$this->compiler->setTargetName('abi_defaults');
$options = $this->invokeMethod('getCompileCommandOptions');
$this->assertContains('TYPEPHP_ABI_DEFAULTS_EXPORTS=1', $options['user_defines']);
$this->assertStringEndsWith('/php_abi_defaults_func_decl.h', $options['forced_include']);
if (!$this->compiler->isWindows()) {
$flags = $this->getPropertyValue('compilerBackend')->buildCompileOptions($options->toArray());
$this->assertStringContainsString('-fvisibility=hidden', $flags);
$this->assertStringContainsString('-include', $flags);
}
}
public function testObjectiveCppCompileCommandOptionsKeepCppOptions(): void
{
$this->setPropertyValue('cxxStd', 'c++20');

@ -3,6 +3,7 @@
namespace TypePhp\Backend;
use TypePhp\Platform\PlatformBase;
use TypePhp\Platform\Windows;
/**
* GCC/Clang 共享后端基类
@ -101,6 +102,10 @@ abstract class GccLikeBackend extends CompilerBackend
$cmd .= $this->getPICFlag($config);
if (($config['build_mode'] ?? null) === 'lib' && !($this->platform instanceof Windows)) {
$cmd .= ' -fvisibility=hidden';
}
if (!empty($config['enable_profiler'])) {
$cmd .= ' ' . $this->formatDefineFlag('PPROF_ON=1', '-D');
if (!empty($config['prof_output'])) {
@ -127,6 +132,10 @@ abstract class GccLikeBackend extends CompilerBackend
$cmd .= $this->formatPrecompiledHeaderFlag($config['precompiled_header']);
}
if ($includeCppStd && !empty($config['forced_include'])) {
$cmd .= ' -include ' . escapeshellarg($config['forced_include']);
}
return $cmd;
}

@ -96,6 +96,10 @@ class Msvc extends CompilerBackend
if (!empty($config['cxxflags'])) {
$cmd .= ' ' . $config['cxxflags'];
}
if (!empty($config['forced_include'])) {
$cmd .= ' /FI' . escapeshellarg($config['forced_include']);
}
}
$cmd .= ' /nologo';

@ -25,6 +25,7 @@ trait NativeCommandOptionsTrait
$userDefines = $this->userDefines;
if ($this->isBuildModeLib()) {
$userDefines[] = 'TYPEPHP_NO_MAIN=1';
$userDefines[] = $this->getLibraryExportsMacroName() . '=1';
}
return new CompileOptions([
@ -51,6 +52,13 @@ trait NativeCommandOptionsTrait
->with('cxxflags', $this->cxxFlags)
->with('suppressed_warnings', Constants::MSVC_SUPPRESSED_WARNINGS ?? []);
if ($this->isBuildModeLib()) {
$options = $options->with(
'forced_include',
$this->getIncludeDir() . '/php_' . $this->targetName . '_func_decl.h'
);
}
if ($this->precompiledHeader !== null) {
$options = $options->with('precompiled_header', $this->precompiledHeader);
}
@ -64,6 +72,13 @@ trait NativeCommandOptionsTrait
return $options->with('suppressed_warnings', ['4244', '4146']);
}
protected function getPrecompiledHeaderCompileCommandOptions(): CompileOptions
{
$values = $this->getCompileCommandOptions()->toArray();
unset($values['forced_include'], $values['precompiled_header']);
return new CompileOptions($values);
}
protected function getNativeCompileCommandOptions(string $language = ''): CompileOptions
{
$options = $this->getCommonCompileCommandOptions();

@ -11,7 +11,6 @@ use TypePhp\Type;
use TypePhp\Entity\ArgInfo;
use TypePhp\Entity\ArrayInitPlan;
use TypePhp\Entity\FunctionDef;
trait DefaultArgumentGenerator
{
@ -24,18 +23,19 @@ trait DefaultArgumentGenerator
return $type;
}
protected function getDefaultArgumentHelperName(FunctionDef $func, ArgInfo $argInfo): string
protected function getDefaultArgumentHelperType(ArgInfo $argInfo): string
{
return self::PREFIX . 'default_arg_' . $func->name . '_' . $argInfo->name;
return $argInfo->variadic ? Type::ARRAY : $this->getDefaultArgumentType($argInfo);
}
protected function genDefaultArgumentExpr(FunctionDef $func, ArgInfo $argInfo): string
protected function getDefaultArgumentHelperName(string $nativeName, int $argumentIndex): string
{
if (!$argInfo->arrayInitPlan || !$argInfo->arrayInitPlan->requiresRuntimeInit()) {
return $argInfo->default;
}
return self::PREFIX . $nativeName . '_arg_' . $argumentIndex . '_default_value';
}
return $this->getDefaultArgumentHelperName($func, $argInfo) . '()';
protected function genDefaultArgumentExpr(string $nativeName, int $argumentIndex): string
{
return $this->getDefaultArgumentHelperName($nativeName, $argumentIndex) . '()';
}
protected function wrapArrayInitPlan(ArrayInitPlan $plan, string $body): string
@ -43,31 +43,64 @@ trait DefaultArgumentGenerator
return "do {\n" . $plan->init . $body . $plan->clean . "} while (0);\n";
}
protected function genDefaultArgumentHelpers(): string
protected function genDefaultArgumentHelperDeclarations(string $declarationPrefix): string
{
$code = '';
foreach ($this->symbols->functions() as $func) {
foreach ($func->argInfoList as $argInfo) {
$plan = $argInfo->arrayInitPlan;
if (!$plan || !$plan->requiresRuntimeInit()) {
foreach ($this->symbols->functions() as $nativeName => $func) {
foreach ($func->argInfoList as $argumentIndex => $argInfo) {
if (!$this->shouldGenerateDefaultArgumentHelper($argInfo)) {
continue;
}
$type = $this->getDefaultArgumentHelperType($argInfo);
$helper = $this->getDefaultArgumentHelperName($nativeName, $argumentIndex);
$code .= $declarationPrefix . $type . ' ' . $helper . '();' . PHP_EOL;
}
}
return $code ? $code . PHP_EOL : '';
}
protected function genDefaultArgumentHelperDefinitions(): string
{
$code = '';
foreach ($this->symbols->functions() as $nativeName => $func) {
foreach ($func->argInfoList as $argumentIndex => $argInfo) {
if (!$this->shouldGenerateDefaultArgumentHelper($argInfo)) {
continue;
}
$type = $this->getDefaultArgumentType($argInfo);
$helper = $this->getDefaultArgumentHelperName($func, $argInfo);
$code .= 'static inline ' . $type . ' ' . $helper . "() {\n";
$code .= $plan->init;
if ($plan->clean) {
$code .= $type . ' retval = ' . $plan->expr . ';' . PHP_EOL;
$code .= $plan->clean;
$code .= 'return retval;' . PHP_EOL;
$type = $this->getDefaultArgumentHelperType($argInfo);
$helper = $this->getDefaultArgumentHelperName($nativeName, $argumentIndex);
$code .= $type . ' ' . $helper . "() {\n";
$plan = $argInfo->arrayInitPlan;
if ($plan && $plan->requiresRuntimeInit()) {
$code .= $plan->init;
if ($plan->clean) {
$code .= $type . ' retval = ' . $plan->expr . ';' . PHP_EOL;
$code .= $plan->clean;
$code .= 'return retval;' . PHP_EOL;
} else {
$code .= 'return ' . $plan->expr . ';' . PHP_EOL;
}
} else {
$code .= 'return ' . $plan->expr . ';' . PHP_EOL;
$code .= 'return ' . $argInfo->default . ';' . PHP_EOL;
}
$code .= '}' . PHP_EOL;
$code .= '}' . PHP_EOL . PHP_EOL;
}
}
return $code ? $code . PHP_EOL : '';
return $code;
}
private function shouldGenerateDefaultArgumentHelper(ArgInfo $argInfo): bool
{
if ($argInfo->variadic) {
return true;
}
return $argInfo->default !== '';
}
}

@ -788,6 +788,9 @@ CODE;
$code .= PHP_EOL;
}
$code .= "// default argument values \n";
$code .= $this->genDefaultArgumentHelperDefinitions();
$code .= "// constants \n";
foreach ($this->constants as $name => $const) {
$code .= $const->type . ' ' . $name . ";\n";
@ -1275,7 +1278,7 @@ CODE;
$this->globalHeaders,
$dependencies,
$this->getBuildDir() . '/cache/pch',
$this->getCompileCommandOptions(),
$this->getPrecompiledHeaderCompileCommandOptions(),
);
$this->precompiledHeader = [
'header' => $result['header'],
@ -1534,15 +1537,29 @@ CODE;
public function genFunctionDeclaration(string $file): void
{
$code = '#include <phpx.h>' . PHP_EOL;
$code = '#pragma once' . PHP_EOL . PHP_EOL;
$code .= '#include <phpx.h>' . PHP_EOL;
$code .= '#include <typephp_fiber_generator.h>' . PHP_EOL;
// 函数的默认值可能会使用字符串字面量,需要提前声明
if ($this->literalStrings) {
$literalStringsCount = count($this->literalStrings);
$code .= 'extern ' . Type::STR . ' ' . self::LITERAL_STRINGS . '[' . $literalStringsCount . '];' . PHP_EOL;
}
$code .= $this->genDefaultArgumentHelpers();
$declarationPrefix = 'extern ';
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);
foreach ($this->symbols->functions() as $name => $func) {
$list = [];
@ -1551,23 +1568,24 @@ CODE;
}
$argInfoList = $func->argInfoList;
if ($argInfoList) {
foreach ($argInfoList as $argInfo) {
foreach ($argInfoList as $argumentIndex => $argInfo) {
if ($argInfo->variadic) {
$arg = Type::ARRAY . ' ' . $argInfo->name . ' = {}';
$arg = Type::ARRAY . ' ' . $argInfo->name
. ' = ' . $this->genDefaultArgumentExpr($name, $argumentIndex);
} else {
$arg = $this->genArgumentDeclaration($argInfo);
if ($argInfo->default && !$this->isConstructorNativeFunction($func)) {
$arg .= ' = ' . $this->genDefaultArgumentExpr($func, $argInfo);
if ($argInfo->default !== '' && !$this->isConstructorNativeFunction($func)) {
$arg .= ' = ' . $this->genDefaultArgumentExpr($name, $argumentIndex);
}
}
$list[] = $arg;
}
}
$params = implode(', ', $list);
$code .= 'extern ' . ($func->returnsByRef ? Type::REF : $func->returnType) . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL;
$code .= $declarationPrefix . ($func->returnsByRef ? Type::REF : $func->returnType) . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL;
if ($func->hasMultiReturn()) {
$code .= 'namespace ' . self::MULTI_RETURN_NAMESPACE . ' {' . PHP_EOL;
$code .= 'extern ' . $func->getMultiReturnCppType() . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL;
$code .= $declarationPrefix . $func->getMultiReturnCppType() . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL;
$code .= '}' . PHP_EOL;
}
}
@ -1579,6 +1597,16 @@ CODE;
$this->writeFile($file, $code);
}
protected function getLibraryApiMacroName(): string
{
return 'TYPEPHP_' . strtoupper($this->targetName) . '_API';
}
protected function getLibraryExportsMacroName(): string
{
return 'TYPEPHP_' . strtoupper($this->targetName) . '_EXPORTS';
}
public function getBuildMode(): string
{
return $this->buildMode;
@ -2870,8 +2898,11 @@ CODE;
$cppCode .= '}' . PHP_EOL;
$cppCode .= $this->genExtraNamedVariadicArgs($var);
} else {
if ($argInfo->default) {
$defaultExpr = $this->genDefaultArgumentExpr($functionDef, $argInfo);
if ($argInfo->default !== '') {
$nativeName = str_starts_with($fn, self::PREFIX)
? substr($fn, strlen(self::PREFIX))
: $fn;
$defaultExpr = $this->genDefaultArgumentExpr($nativeName, $k);
if ($argInfo->byRef) {
$argExpr = 'php::getCallArgByRef(' . $k . ', ' . $defaultExpr . ')';
} else {

Loading…
Cancel
Save