From 88139f4da7ef77e29f8266f5d826a1faed8e13fa Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 10 Jul 2026 12:02:56 +0800 Subject: [PATCH] feat(build): add library build mode with proper target naming - Add support for 'lib' build mode that creates shared libraries with correct extensions - Implement automatic 'lib' prefix for library targets on Unix systems unless explicit output is set - Add build mode aliases normalization ('library', 'extension', 'cli' map to standard modes) - Update target filename generation to handle explicit output paths with custom extensions - Add platform-specific target extensions: .so for Linux, .dylib for macOS, .dll for Windows - Include smoke test for validating library compilation and linking - Update documentation with library build instructions and usage examples - Add linker flag -Wl,-z,defs for Linux library builds to ensure self-contained libraries - Modify command line help to document new build modes and clarify existing options - Add C API header generation for exported --- .gitignore | 2 + examples/lib-demo/README.md | 21 +++++ examples/lib-demo/cpp-src/exports.cc | 13 ++- examples/lib-demo/include/typephp_lib_demo.h | 22 +++++ examples/lib-demo/project.yml | 2 +- examples/lib-demo/smoke-test.cc | 14 ++++ phpunit/src/CompilerBaseApiTest.php | 30 +++++++ phpunit/src/Platform/PlatformTest.php | 3 + src/Backend/GccLikeBackend.php | 6 ++ src/Platform/UnixPlatform.php | 9 ++ src/Translator.php | 87 +++++++++++++++----- 11 files changed, 179 insertions(+), 30 deletions(-) create mode 100644 examples/lib-demo/README.md create mode 100644 examples/lib-demo/include/typephp_lib_demo.h create mode 100644 examples/lib-demo/smoke-test.cc diff --git a/.gitignore b/.gitignore index 55aba248..ede6074d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ /__pycache__ *.o *.dll +*.so +*.dylib *.exe *.obj *.pdb diff --git a/examples/lib-demo/README.md b/examples/lib-demo/README.md new file mode 100644 index 00000000..4064b02e --- /dev/null +++ b/examples/lib-demo/README.md @@ -0,0 +1,21 @@ +# lib-demo + +This example compiles TypePHP code into a shared library and exposes a stable C ABI. + +Build it from the repository root: + +```sh +php bin/compiler.php examples/lib-demo/project.yml --no-progress +``` + +The resulting library uses the configured project name: currently `libdemo.so` on Linux, `libdemo.dylib` on macOS, and `demo.dll` on Windows. Set `output: demo.so` in `project.yml` to use an exact output filename. Its public API is declared in `include/typephp_lib_demo.h`. + +On Linux, validate the library can be linked and called: + +```sh +g++ -std=c++17 examples/lib-demo/smoke-test.cc -Iexamples/lib-demo \ + ./libdemo.so -Wl,-rpath,'$ORIGIN' -o lib-demo-smoke +./lib-demo-smoke +``` + +`typephp_lib_demo_add()` initializes the embedded PHP runtime on its first call. The runtime remains process-global for the lifetime of the library; do not call it concurrently with the first invocation. diff --git a/examples/lib-demo/cpp-src/exports.cc b/examples/lib-demo/cpp-src/exports.cc index 52128136..0c0f6a96 100644 --- a/examples/lib-demo/cpp-src/exports.cc +++ b/examples/lib-demo/cpp-src/exports.cc @@ -1,14 +1,11 @@ -#include - -#ifdef _WIN32 -#define TYPEPHP_API extern "C" __declspec(dllexport) -#else -#define TYPEPHP_API extern "C" __attribute__((visibility("default"))) -#endif +#define TYPEPHP_LIB_DEMO_BUILD +#include "../include/typephp_lib_demo.h" +#include extern "C" int php_aot_runtime_init(int argc, char **argv); +extern php::Int php_demo_add(php::Int a, php::Int b); -TYPEPHP_API int typephp_lib_demo_add(int a, int b) +extern "C" TYPEPHP_LIB_DEMO_API int typephp_lib_demo_add(int a, int b) { char app_name[] = "typephp_lib_demo"; char *argv[] = {app_name, nullptr}; diff --git a/examples/lib-demo/include/typephp_lib_demo.h b/examples/lib-demo/include/typephp_lib_demo.h new file mode 100644 index 00000000..77265cd9 --- /dev/null +++ b/examples/lib-demo/include/typephp_lib_demo.h @@ -0,0 +1,22 @@ +#ifndef TYPEPHP_LIB_DEMO_H +#define TYPEPHP_LIB_DEMO_H + +#if defined(_WIN32) && defined(TYPEPHP_LIB_DEMO_BUILD) +#define TYPEPHP_LIB_DEMO_API __declspec(dllexport) +#elif defined(_WIN32) +#define TYPEPHP_LIB_DEMO_API __declspec(dllimport) +#else +#define TYPEPHP_LIB_DEMO_API +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +TYPEPHP_LIB_DEMO_API int typephp_lib_demo_add(int a, int b); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/examples/lib-demo/project.yml b/examples/lib-demo/project.yml index ecbc89fa..16283ba4 100644 --- a/examples/lib-demo/project.yml +++ b/examples/lib-demo/project.yml @@ -1,4 +1,4 @@ -name: typephp_lib_demo +name: demo mode: lib version: 0.1.0 cxx-std: c++17 diff --git a/examples/lib-demo/smoke-test.cc b/examples/lib-demo/smoke-test.cc new file mode 100644 index 00000000..1a6d16a9 --- /dev/null +++ b/examples/lib-demo/smoke-test.cc @@ -0,0 +1,14 @@ +#include "include/typephp_lib_demo.h" + +#include + +int main() +{ + const int first = typephp_lib_demo_add(20, 22); + const int second = typephp_lib_demo_add(-10, 7); + if (first != 42 || second != -3) { + std::cerr << "unexpected results: " << first << ", " << second << '\n'; + return 1; + } + return 0; +} diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 2b4ba653..5b63fdb7 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -103,6 +103,18 @@ class CompilerBaseApiTest extends TestCase chmod($binDir . '/clang-format', 0755); } + public function testBuildModeAliasesAreNormalized(): void + { + $this->compiler->setBuildMode('library'); + $this->assertSame(CompilerBase::BUILD_MODE_LIB, $this->compiler->getBuildMode()); + + $this->compiler->setBuildMode('extension'); + $this->assertSame(CompilerBase::BUILD_MODE_EXT, $this->compiler->getBuildMode()); + + $this->compiler->setBuildMode('cli'); + $this->assertSame(CompilerBase::BUILD_MODE_BIN, $this->compiler->getBuildMode()); + } + // ======================================================================== // getTypeFromZendType // ======================================================================== @@ -367,6 +379,24 @@ YAML, 'myproject.yml', 'examples/tetris-sdl'); $this->assertSame('tetris', $this->invokeMethod('getTargetFileName')); } + public function testLibModeUsesUnixLibraryPrefixUnlessOutputIsExplicit(): void + { + $this->compiler->setBuildMode(CompilerBase::BUILD_MODE_LIB); + $this->compiler->setTargetName('demo'); + $this->assertSame('libdemo.so', $this->invokeMethod('getTargetFileName')); + + $this->compiler->setOutputPath('demo.so'); + $this->assertSame('demo.so', $this->invokeMethod('getTargetFileName')); + } + + public function testExtensionModeDoesNotUseUnixLibraryPrefix(): void + { + $this->compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT); + $this->compiler->setTargetName('demo'); + + $this->assertSame('demo.so', $this->invokeMethod('getTargetFileName')); + } + public function testParseProjectYamlResolvesRelativePathOptionsAgainstYamlDirectory(): void { $projectFile = $this->createProjectFile(<<<'YAML' diff --git a/phpunit/src/Platform/PlatformTest.php b/phpunit/src/Platform/PlatformTest.php index beb2b9c5..16c9d25d 100644 --- a/phpunit/src/Platform/PlatformTest.php +++ b/phpunit/src/Platform/PlatformTest.php @@ -95,10 +95,13 @@ class PlatformTest extends TestCase $this->assertSame('.exe', $windows->getTargetExtension('bin')); $this->assertSame('.dll', $windows->getTargetExtension('ext')); + $this->assertSame('.dll', $windows->getTargetExtension('lib')); $this->assertSame('', $linux->getTargetExtension('bin')); $this->assertSame('.so', $linux->getTargetExtension('ext')); + $this->assertSame('.so', $linux->getTargetExtension('lib')); $this->assertSame('', $macos->getTargetExtension('bin')); $this->assertSame('.so', $macos->getTargetExtension('ext')); + $this->assertSame('.dylib', $macos->getTargetExtension('lib')); } public function testPlatformPathPrefixRemoval(): void diff --git a/src/Backend/GccLikeBackend.php b/src/Backend/GccLikeBackend.php index 7d63f00d..6ebe5a8f 100644 --- a/src/Backend/GccLikeBackend.php +++ b/src/Backend/GccLikeBackend.php @@ -124,6 +124,12 @@ abstract class GccLikeBackend extends CompilerBackend if ((!empty($config['build_mode']) && ($config['build_mode'] === 'ext' || $config['build_mode'] === 'lib')) || !empty($config['shared'])) { $flags .= ' ' . $this->platform->getSharedLinkFlag(); + // Shared libraries must be self-contained. Executables can defer symbols + // to their host, but a lib-mode artifact must be loadable via dlopen(). + if (($config['build_mode'] ?? null) === 'lib' && !($this->platform instanceof \TypePhp\Platform\Macos)) { + $flags .= ' -Wl,-z,defs'; + } + if ($this->platform instanceof \TypePhp\Platform\Macos && !empty($config['install_name'])) { $flags .= ' ' . $this->platform->getCurrentInstallNameOption($config['install_name']); } diff --git a/src/Platform/UnixPlatform.php b/src/Platform/UnixPlatform.php index fed6e235..e611a6db 100644 --- a/src/Platform/UnixPlatform.php +++ b/src/Platform/UnixPlatform.php @@ -8,6 +8,15 @@ namespace TypePhp\Platform; */ abstract class UnixPlatform extends PlatformBase { + public function getTargetExtension(string $buildMode): string + { + if ($buildMode === 'lib') { + return $this->getSharedLibraryExtension(); + } + + return parent::getTargetExtension($buildMode); + } + public function getIncludeFlags(array $includePaths): string { if (empty($includePaths)) { diff --git a/src/Translator.php b/src/Translator.php index dd30d901..d7ba6eda 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -41,6 +41,8 @@ class Translator extends Preprocessor public const string VERSION = '0.3.0'; public const string APP_NAME = 'TypePHP Compiler (AOT)'; protected string $targetName = 'app'; + protected bool $hasExplicitOutput = false; + protected ?string $explicitOutputExtension = null; protected array $sourceDirs = []; protected bool $verbose = false; protected array $phpSrcFiles = []; @@ -248,7 +250,7 @@ class Translator extends Preprocessor $climate->tab()->out('-v, --version Show version'); $climate->tab()->out('-h, --help Show this help message'); $climate->tab()->out('-f, --force Force recompile phpx misc files (ignore cache)'); - $climate->tab()->out('-m, --mode Compilation mode, -m bin(binary) or -m ext(extension), default: bin'); + $climate->tab()->out('-m, --mode Compilation mode: bin (binary), lib (shared library), or ext (PHP extension); default: bin'); $climate->tab()->out('-r, --run Run the compiled binary after build'); $climate->tab()->out('-j, --job Number of parallel compilation jobs (default: 4)'); $climate->tab()->out('--cxx-std C++ standard version (c++17, c++20, etc., default: c++17)'); @@ -282,7 +284,7 @@ class Translator extends Preprocessor // 构建模式 if ($this->climate->arguments->defined('mode')) { - $this->buildMode = $this->climate->arguments->get('mode'); + $this->setBuildMode($this->climate->arguments->get('mode')); } // 调试行号 @@ -346,7 +348,7 @@ class Translator extends Preprocessor // 输出文件名/路径 if ($this->climate->arguments->defined('output')) { - $this->setTargetName($this->climate->arguments->get('output')); + $this->setOutputPath($this->climate->arguments->get('output')); } // 构建目录 @@ -570,6 +572,18 @@ class Translator extends Preprocessor public function setBuildMode(string $mode): void { + $mode = strtolower(trim($mode)); + $mode = match ($mode) { + 'binary', 'cli' => self::BUILD_MODE_BIN, + 'extension' => self::BUILD_MODE_EXT, + 'library', 'shared', 'dll', 'dylib', 'so' => self::BUILD_MODE_LIB, + default => $mode, + }; + + if (!in_array($mode, [self::BUILD_MODE_BIN, self::BUILD_MODE_EXT, self::BUILD_MODE_LIB], true)) { + $this->error("Invalid build mode `{$mode}`. Expected bin, lib, or ext."); + } + $this->buildMode = $mode; } @@ -593,6 +607,47 @@ class Translator extends Preprocessor $this->targetName = $name; } + /** + * Set an explicit output path without using its extension in generated symbols. + */ + public function setOutputPath(string $path): void + { + if (str_contains($path, '/') || str_contains($path, '\\')) { + $this->outputDir = dirname($path); + $path = basename($path); + } + + $extension = pathinfo($path, PATHINFO_EXTENSION); + if ($extension !== '') { + $this->explicitOutputExtension = '.' . $extension; + $path = substr($path, 0, -strlen($this->explicitOutputExtension)); + } else { + $this->explicitOutputExtension = null; + } + + $this->hasExplicitOutput = true; + $this->setTargetName($path); + } + + protected function getTargetFileName(): string + { + $targetFile = $this->targetName; + if ($this->isBuildModeLib() && !$this->isWindows() && !$this->hasExplicitOutput) { + $targetFile = 'lib' . $targetFile; + } + + $extension = $this->explicitOutputExtension ?? $this->getPlatform()->getTargetExtension($this->buildMode); + if ($extension !== '' && !str_ends_with($targetFile, $extension)) { + $targetFile .= $extension; + } + + if ($this->outputDir !== '') { + $targetFile = rtrim($this->outputDir, '/\\') . '/' . $targetFile; + } + + return $targetFile; + } + public function addFiles(array $files): void { $this->sourceDirs = array_merge($this->sourceDirs, $files); @@ -834,6 +889,11 @@ class Translator extends Preprocessor $code = '#include ' . PHP_EOL; $code .= $this->genIncludeHeaderFiles(); + if ($this->isBuildModeLib() && !$this->isWindows()) { + // PHPX's embedded runtime references this CLI-only symbol even when main() is disabled. + $code .= 'extern "C" void save_ps_args(int, char **) {}' . PHP_EOL; + } + if ($this->isBuildModeBin()) { $cliHeaders = [ '#include "php_cli_process_title.h"', @@ -1731,7 +1791,7 @@ CODE; public function run(string $targetFile): never { if ($this->buildMode !== self::BUILD_MODE_BIN) { - $this->climate->error('--run is only supported in binary mode (-m bin), not extension mode (-m ext)'); + $this->climate->error('--run is only supported in binary mode (-m bin), not library or extension mode'); exit(1); } @@ -2230,7 +2290,7 @@ CODE; // 读取 output/name。name 只表示目标名,不能按 YAML 目录解析成输出路径。 $output = $cfg['output'] ?? null; if (!empty($output)) { - $this->setTargetName($this->resolvePath((string) $output, $projectDir, 'Output path')); + $this->setOutputPath($this->resolvePath((string) $output, $projectDir, 'Output path')); } elseif (!empty($cfg['name'])) { $this->setTargetName((string) $cfg['name']); } @@ -2244,22 +2304,7 @@ CODE; // 读取 mode/type/build-mode(支持 CLI/YAML 两套命名) $buildMode = $cfg['mode'] ?? $cfg['build-mode'] ?? $cfg['type'] ?? null; if (!empty($buildMode)) { - // 映射常见的类型名称到内部 buildMode - $modeMap = [ - 'extension' => self::BUILD_MODE_EXT, - 'ext' => self::BUILD_MODE_EXT, - 'library' => self::BUILD_MODE_LIB, - 'lib' => self::BUILD_MODE_LIB, - 'shared' => self::BUILD_MODE_LIB, - 'dll' => self::BUILD_MODE_LIB, - 'dylib' => self::BUILD_MODE_LIB, - 'so' => self::BUILD_MODE_LIB, - 'binary' => self::BUILD_MODE_BIN, - 'bin' => self::BUILD_MODE_BIN, - 'cli' => self::BUILD_MODE_BIN, - ]; - $mappedMode = $modeMap[strtolower($buildMode)] ?? $buildMode; - $this->setBuildMode($mappedMode); + $this->setBuildMode((string) $buildMode); } // 读取 ignore(支持中横线和下划线)