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
pull/16/head
韩天峰 2 months ago
parent b4a2de7ace
commit 88139f4da7
  1. 2
      .gitignore
  2. 21
      examples/lib-demo/README.md
  3. 13
      examples/lib-demo/cpp-src/exports.cc
  4. 22
      examples/lib-demo/include/typephp_lib_demo.h
  5. 2
      examples/lib-demo/project.yml
  6. 14
      examples/lib-demo/smoke-test.cc
  7. 30
      phpunit/src/CompilerBaseApiTest.php
  8. 3
      phpunit/src/Platform/PlatformTest.php
  9. 6
      src/Backend/GccLikeBackend.php
  10. 9
      src/Platform/UnixPlatform.php
  11. 87
      src/Translator.php

2
.gitignore vendored

@ -13,6 +13,8 @@
/__pycache__
*.o
*.dll
*.so
*.dylib
*.exe
*.obj
*.pdb

@ -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.

@ -1,14 +1,11 @@
#include <php_typephp_lib_demo_func_decl.h>
#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 <phpx.h>
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};

@ -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

@ -1,4 +1,4 @@
name: typephp_lib_demo
name: demo
mode: lib
version: 0.1.0
cxx-std: c++17

@ -0,0 +1,14 @@
#include "include/typephp_lib_demo.h"
#include <iostream>
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;
}

@ -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'

@ -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

@ -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']);
}

@ -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)) {

@ -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 <mode> Compilation mode, -m bin(binary) or -m ext(extension), default: bin');
$climate->tab()->out('-m, --mode <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 <num> Number of parallel compilation jobs (default: 4)');
$climate->tab()->out('--cxx-std <ver> 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 <cstring>' . 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(支持中横线和下划线)

Loading…
Cancel
Save