diff --git a/phpunit/code/compiler_api/default_argument_abi.php b/phpunit/code/compiler_api/default_argument_abi.php new file mode 100644 index 00000000..ac0c44bd --- /dev/null +++ b/phpunit/code/compiler_api/default_argument_abi.php @@ -0,0 +1,15 @@ + '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; +} diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 401637e8..e7126409 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -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'); diff --git a/src/Backend/GccLikeBackend.php b/src/Backend/GccLikeBackend.php index 1fce1c68..0472b239 100644 --- a/src/Backend/GccLikeBackend.php +++ b/src/Backend/GccLikeBackend.php @@ -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; } diff --git a/src/Backend/Msvc.php b/src/Backend/Msvc.php index 9a294488..66beff8c 100644 --- a/src/Backend/Msvc.php +++ b/src/Backend/Msvc.php @@ -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'; diff --git a/src/Build/NativeCommandOptionsTrait.php b/src/Build/NativeCommandOptionsTrait.php index 358609fe..accee17c 100644 --- a/src/Build/NativeCommandOptionsTrait.php +++ b/src/Build/NativeCommandOptionsTrait.php @@ -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(); diff --git a/src/Generator/DefaultArgumentGenerator.php b/src/Generator/DefaultArgumentGenerator.php index 6a095ea7..3db867ef 100644 --- a/src/Generator/DefaultArgumentGenerator.php +++ b/src/Generator/DefaultArgumentGenerator.php @@ -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 !== ''; } } diff --git a/src/Translator.php b/src/Translator.php index 79aea122..1ceb1b26 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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 ' . PHP_EOL; + $code = '#pragma once' . PHP_EOL . PHP_EOL; + $code .= '#include ' . PHP_EOL; $code .= '#include ' . 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 {