支持 wasm

pull/46/head
韩天峰 3 weeks ago
parent c3614aa6d3
commit 8503c7c32c
  1. 1
      .gitignore
  2. 1
      docs/README.md
  3. 93
      docs/TYPEPHP_WASI_BUILD.md
  4. 65
      docs/TYPEPHP_WASM_IMPLEMENTATION_PLAN.md
  5. 3
      examples/hello.php
  6. 7
      phpunit/code/wasi-backtick.php
  7. 7
      phpunit/code/wasi-generator-arrow.php
  8. 9
      phpunit/code/wasi-generator-closure.php
  9. 91
      phpunit/src/Build/WasiToolchainTest.php
  10. 13
      phpunit/src/CompilerBaseApiTest.php
  11. 13
      phpunit/src/Generator/FiberGeneratorTest.php
  12. 13
      phpunit/src/Platform/PlatformTest.php
  13. 42
      phpunit/src/WasiUnsupportedSyntaxTest.php
  14. 3
      src/Build/SourcePipelineTrait.php
  15. 96
      src/Build/WasiToolchain.php
  16. 9
      src/CompilerBase.php
  17. 3
      src/Generator/ClosureGenerator.php
  18. 3
      src/Generator/FiberGenerator.php
  19. 50
      src/Platform/Wasi.php
  20. 41
      src/Translator.php
  21. 118
      src/compiler.php

1
.gitignore vendored

@ -21,6 +21,7 @@
*.lib
*.exp
*.class
*.wasm
/swoole_compiler
/tpc
tests/**/*.diff

@ -18,6 +18,7 @@
- [后端中立 IR](BACKEND_NEUTRAL_IR.md)
- [TypePHP WASM 技术方案与实施计划](TYPEPHP_WASM_IMPLEMENTATION_PLAN.md)
- [构建 TypePHP WASI 程序](TYPEPHP_WASI_BUILD.md)
- [核心重构计划](REFACTORING_PLAN.md)
- [构建速度研究](AOT_BUILD_SPEED_RESEARCH.md)
- [优化优先级](aot-optimization-priority.md)

@ -0,0 +1,93 @@
# 构建 TypePHP WASI 程序
TypePHP 已有一个可运行的 WASI Preview 1 原型。它将 TypePHP 生成的 C++、PHPX 核心、精简的 PHP 8.5 NTS、GMP、MPFR 和 mpdecimal 静态链接为单个 `.wasm` command 模块。
## 环境要求
- WASI SDK 33 或更高版本(LLVM/Clang/LLD 22 或更高)
- PHP 8.4 或更高版本,用于运行 TypePHP 编译器
- Autoconf、Automake、Libtool、Bison、re2c 和常规 C/C++ 构建工具
- Wasmtime 47 或更高版本,用于运行和测试产物
WASI SDK 的 `bin` 目录和 Wasmtime 必须加入系统 `PATH`。编译器不会探测或使用 `/opt` 等约定安装目录,也不接受专用的工具目录配置。可以继续通过环境变量覆盖缓存位置:
```bash
export PATH="<wasi-sdk-bin>:<wasmtime-bin>:$PATH"
export PHP_WASM_BUILD_DIR=/tmp/php-8.5.9-wasm-build
export TYPEPHP_WASM_DEPS_PREFIX=/tmp/typephp-wasm-numeric/prefix
export TYPEPHP_WASM_RUNTIME_PREFIX=/tmp/typephp-wasm-runtime
```
执行 `--wasm` 时会先检查 `clang`、`clang++`、`llvm-ar`、`llvm-ranlib`、`llvm-nm`、`wasm-ld` 和 `wasmtime` 是否都能从 `PATH` 找到,检查最低版本,并确认 `clang++` 的默认目标是 `wasm32-wasi`。检测失败时不会进入代码生成或编译阶段。
## 一条命令构建
源文件必须提供 `main(): void`
```php
<?php
function main(): void
{
echo "Hello from TypePHP/WASI\n";
}
```
执行:
```bash
php bin/tpc.php --wasm hello.php
```
输出文件默认为当前目录下的 `hello.wasm`。生成的 `.cc` 与 host 模式使用相同的 build 目录规则,默认位于 TypePHP 根目录的 `build/`;可以使用 `--build-dir <directory>` 覆盖。每个 `.o` 与对应 `.cc` 放在同一目录并在下次构建时直接覆盖,不建立 WASM 专用的深层对象目录。编译器用于传递本次源码列表的临时清单会在链接结束后自动清理。
PHP、PHPX、TypePHP runtime、GMP、MPFR 和 mpdecimal 会预编译并缓存为 WASI 静态库;正常的应用构建只编译 TypePHP 为当前程序生成的 C++,然后链接这些 `.a`。源码开发环境首次执行时会自动建立缺失的运行时缓存,发行包可直接携带预编译静态库。
每个 C/C++ 翻译单元统一使用标准 Wasm C++ exceptions 和 WASI SJLJ;链接阶段将 ABI 警告视为错误,旧的 32 位 `zend_long` 缓存也会自动失效。
运行:
```bash
wasmtime hello.wasm
```
## 高精度类型
WASI 产物包含 TypePHP 的三种语言级高精度类型:
- `BigInt`:GMP 6.3.0
- `BigFloat`:MPFR 4.2.2
- `Decimal`:mpdecimal 4.0.1
完整示例位于 [high-precision.php](../projects/php-8.5.9/wasm/examples/high-precision.php)。构建并运行:
```bash
php bin/tpc.php --wasm projects/php-8.5.9/wasm/examples/high-precision.php
wasmtime high-precision.wasm
```
预期输出:
```text
1111111101111111110111111111010
1000000000000000000000000000001
12348.14159265358979324
```
wasm32 使用 32 位指针,但 PHP 的 `zend_long` 保持 64 位,以维持 TypePHP 与 64 位 PHP 的整数语义。GMP 和 mpdecimal 使用 32 位 limb;这不改变任意精度语义,但大数吞吐量低于具有汇编优化的原生 64 位构建。
## 当前平台边界
- 仅支持 NTS、单线程。
- Fiber 和 TypePHP Generator 被禁用;编译器在发现 `yield` 时直接报致命错误。
- PHPX Facade API 在 `__wasi__` 下整体禁用。PHPX 核心类型和 `phpx_std` 仍可使用。
- 不支持动态扩展、网络 socket、进程控制和依赖操作系统服务的扩展。
- 保留 PHP stream 框架、本地文件能力以及由 WASI host 提供的时间和随机数能力。
- 当前产物是 WASI command module,适用于 Wasmtime 等 WASI 运行时,不能不经宿主适配直接放入浏览器运行。
PHPX Facade 只是为 PHP 可选扩展生成的便捷包装,并非 TypePHP ABI 的组成部分。WASI 下整体关闭它,可以避免把不存在的 curl、socket、Swoole、PDO 等 API 暴露为“可编译但链接失败”的接口。
## 内部构建层次
编译器内部会分别建立 PHP 8.5 NTS、GMP/MPFR/mpdecimal,以及 PHPX core/TypePHP runtime 的静态库缓存。内部构建脚本不是用户接口,不需要也不应由用户手动执行。发行包可直接附带目标平台对应的预编译 `.a` 文件。
用户始终通过 `php bin/tpc.php --wasm program.php` 构建最终程序,避免 PHP、PHPX 和生成代码使用不一致的 `zend_long`、异常或 SJLJ ABI。

@ -1,14 +1,14 @@
# TypePHP WASM 技术方案与实施计划
> 状态:技术预研结论,供后续实现使用
> 状态:WASI 命令行原型已实现,浏览器阶段待实现
> 调研日期:2026-08-07
> 首期目标:浏览器中的 Emscripten/WASM,NTS,单线程
> 当前目标:WASI Preview 1,NTS,单线程;后续适配浏览器
## 1. 文档目的
本文记录 TypePHP 支持 WebAssembly 的技术决策、功能边界、运行时架构、主要风险、验证方法和分阶段实施计划。
本文不是当前功能说明。TypePHP 尚未承诺已经支持 WASM,后续实现应以本文作为设计起点,并根据原型验证结果更新决策
2026-08-07 的实现验证已经证明:精简 PHP 8.5、PHPX 核心、TypePHP 生成代码、GMP、MPFR 和 mpdecimal 可以通过 WASI SDK 静态链接为单个模块,并在 Wasmtime 中运行。可复现构建方法见 [构建 TypePHP WASI 程序](TYPEPHP_WASI_BUILD.md)。本文余下内容同时保留浏览器阶段的设计目标
## 2. 核心结论
@ -18,28 +18,28 @@
PHP 源码
-> TypePHP 编译器
-> TypePHP 生成的 C++
-> Emscripten 编译和静态链接
-> WASI SDK 编译和静态链接
+ PHP NTS
+ PHPX
+ TypePHP runtime
+ GMP / MPFR / mpdecimal
+ 最小 TypePHP WASM SAPI
-> typephp.wasm + 薄 JavaScript loader
+ PHP embed/WASI 运行时
-> typephp.wasm(WASI command module)
```
具体决策如下:
1. 第一版复用当前 C++/Zend 后端,不直接生成 WAT/WASM,也不重新实现 PHP 运行时。
2. 使用 Emscripten,而不是把 WASI 作为首期目标
2. 先使用 WASI SDK 建立最小、可测试的命令行目标;浏览器阶段再选择 Emscripten 或 WASI adapter
3. PHP、PHPX、TypePHP 生成代码和高精度库全部静态链接到一个 `.wasm` 模块。
4. 浏览器侧保留一个很薄的 JavaScript loader,用于实例化模块和提供必要的宿主能力。
4. 当前由 WASI host 提供 stdout、clocks、随机数和受控文件系统;浏览器阶段也保持 WASI 接口,由浏览器中的 WASI host/adapter 实现这些能力。
5. 仅支持 PHP NTS,不支持线程。
6. 禁用 Fiber 和 TypePHP Generator。
7. 必须支持 C++ 异常以及 Zend bailout 所需的 `setjmp/longjmp`
8. 保留 PHP stream 框架和本地 stream,禁用网络 transport 和依赖操作系统进程能力的功能。
9. WordPress Playground 和其他 PHP-WASM 项目只作为补丁与移植经验来源,不作为 TypePHP 的依赖或代码基础。
本文描述的是最短可落地路径。长期的后端中立方案参见 [BACKEND_NEUTRAL_IR.md](BACKEND_NEUTRAL_IR.md)。在首期原型成功前,不应为了 WASM 重写 TypePHP 前端和语义层。
本文描述的是最短可落地路径。长期的后端中立方案参见 [BACKEND_NEUTRAL_IR.md](BACKEND_NEUTRAL_IR.md)。WASI 原型证明无需为了 WASM 重写 TypePHP 前端和语义层。
## 3. 为什么不采用 WordPress Playground
@ -67,20 +67,20 @@ TypePHP 无法直接复用 Playground 发布的 PHP-WASM 二进制,因为 Type
## 4. 目标与非目标
### 4.1 首期目标
### 4.1 当前 WASI 目标
- 在主流浏览器中加载 TypePHP 编译产物。
- 在 Wasmtime 等 WASI runtime 中加载 TypePHP 编译产物。
- 执行静态编译的 TypePHP 应用入口。
- 保持 TypePHP 当前基于 Zend 和 PHPX 的主要语言语义。
- 正确处理 PHP request 生命周期、C++ 异常和 Zend bailout。
- 支持 GMP、MPFR 和 mpdecimal 高精度类型。
- 支持内存文件系统和必要的本地 PHP stream。
- 支持 WASI 文件系统和必要的本地 PHP stream。
- 对不支持的功能给出确定、可测试的错误,而不是链接失败或运行时崩溃。
- 构建过程可复现,php-src 和 Emscripten 版本固定。
- 构建过程可复现,php-src、WASI SDK 和数值库版本固定。
### 4.2 首期非目标
- WASI 独立运行时
- 无宿主适配的浏览器直接运行
- pthread、Web Worker 并行 PHP 或共享内存。
- Fiber 和 TypePHP Generator。
- 动态扩展加载。
@ -95,31 +95,26 @@ TypePHP 无法直接复用 Playground 发布的 PHP-WASM 二进制,因为 Type
## 5. 目标平台选择
### 5.1 首期使用 Emscripten
### 5.1 当前使用 WASI SDK
首期目标是浏览器,且 PHP 依赖 `setjmp/longjmp`,PHPX 和 TypePHP 依赖 C++ 异常。Emscripten 已经提供:
当前先建立命令行可验证基线。WASI SDK 已经验证可以同时提供:
- C/C++ 到 WebAssembly 的完整工具链;
- C++ exception handling;
- `setjmp/longjmp` 支持;
- 浏览器文件系统;
- JavaScript import/export 和模块加载;
- libc、时间、随机数和常见 POSIX 接口的兼容层。
- 标准 Wasm C++ exception handling;
- Zend bailout 所需的 SJLJ;
- capability-based 文件系统;
- libc、时间和随机数接口。
这些能力可以显著减少 PHP 移植工作
PHP、PHPX 和所有 TypePHP C++ 翻译单元必须使用一致的 Wasm EH/SJLJ 参数。链接器必须将函数签名不一致视为致命错误
### 5.2 暂不以 WASI 为目标
### 5.2 浏览器阶段
WASI 更适合非浏览器运行时和能力安全模型,但首期采用 WASI 会同时引入以下变量
WASI command module 不能不经适配直接在浏览器运行。后续浏览器阶段需要在以下两种路线中选择
- PHP 和 Zend bailout 的兼容性;
- C++ 异常实现;
- 浏览器侧 WASI shim;
- 文件系统和异步 I/O;
- GMP、MPFR、mpdecimal 的 WASI 构建;
- 不同 WASI runtime 的实现差异。
- 使用 Emscripten 重建同一套静态运行时;
- 为已验证的 WASI 模块提供浏览器 WASI adapter/component host。
在 Emscripten 原型稳定前,不同时维护第二套平台。后续如需支持 Wasmtime、Wasmer 或边缘运行时,应建立独立的 WASI 可行性项目,而不是把兼容层混入首期实现
选择应以异常语义、文件系统、模块体积和 JavaScript 宿主接口的实测结果为依据。浏览器适配不得改变 TypePHP、PHPX 或高精度类型的语言语义。
## 6. 产物和运行模型
@ -133,12 +128,12 @@ dist/
└── typephp-wasm.mjs
```
所有 C/C++ 代码进入 `typephp.wasm`。`typephp-wasm.mjs` 只负责:
所有 C/C++ 代码进入 `typephp.wasm`浏览器自身不会自动提供 WASI imports;`typephp-wasm.mjs` 作为 WASI host/adapter 的装载入口,只负责:
- 获取和实例化 `.wasm`
- 提供 stdout/stderr;
- 初始化内存文件系统;
- 注入时间、随机数等宿主能力;
- 实现或接入 WASI clocks、随机数等宿主能力;
- 调用导出的 TypePHP 生命周期接口;
- 把状态码和错误信息转换为 JavaScript 结果。
@ -232,8 +227,8 @@ PHP 标准库大量依赖 stream。完全关闭 stream 会破坏文件读写、`
| 文件系统 | MEMFS;可选只读预加载文件 |
| 当前目录和路径 | 虚拟根目录,禁止泄漏宿主路径 |
| 环境变量 | loader 注入白名单 |
| 时间 | Emscripten/JavaScript host clock |
| 随机数 | 浏览器安全随机源,不使用弱伪随机替代 |
| 时间 | WASI clocks;浏览器宿主使用浏览器时钟实现该接口 |
| 随机数 | WASI random;浏览器宿主使用安全随机源实现,不使用弱伪随机替代 |
| DNS、socket | 不支持 |
| 进程、shell | 不支持 |
| 信号 | 不支持 |

@ -7,4 +7,7 @@ function main(): void
var_dump(php_uname());
global $argv;
var_dump($argv);
$date = date('Y-m-d H:i:s', time());
var_dump($date);
}

@ -0,0 +1,7 @@
<?php
function main(): void
{
$output = `echo unsupported`;
echo $output;
}

@ -0,0 +1,7 @@
<?php
function main(): void
{
$generator = fn (): iterable => yield 1;
var_dump($generator);
}

@ -0,0 +1,9 @@
<?php
function main(): void
{
$generator = function (): iterable {
yield 1;
};
var_dump($generator);
}

@ -0,0 +1,91 @@
<?php
namespace TypePhp\Tests\Build;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use TypePhp\Build\WasiToolchain;
final class WasiToolchainTest extends TestCase
{
private string $directory;
private string|false $originalPath;
protected function setUp(): void
{
$this->directory = sys_get_temp_dir() . '/typephp-wasi-tools-' . bin2hex(random_bytes(6));
mkdir($this->directory, 0777, true);
$this->originalPath = getenv('PATH');
}
protected function tearDown(): void
{
putenv('PATH=' . ($this->originalPath !== false ? $this->originalPath : ''));
foreach (glob($this->directory . '/*') ?: [] as $file) {
unlink($file);
}
rmdir($this->directory);
}
public function testDetectsSupportedToolsOnlyFromPath(): void
{
$this->installFakeTools(22, 47, 'wasm32-unknown-wasip1');
putenv('PATH=' . $this->directory);
$tools = (new WasiToolchain())->detect();
$this->assertSame($this->directory . '/clang++', $tools['clang++']);
$this->assertSame($this->directory . '/wasmtime', $tools['wasmtime']);
$this->assertSame('wasm32-unknown-wasip1', $tools['target']);
$this->assertSame('22.0.0', $tools['clang-version']);
$this->assertSame('47.0.0', $tools['wasmtime-version']);
}
public function testRejectsMissingTool(): void
{
putenv('PATH=' . $this->directory);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('`clang` was not found in PATH');
(new WasiToolchain())->detect();
}
public function testRejectsOldLlvm(): void
{
$this->installFakeTools(21, 47, 'wasm32-unknown-wasip1');
putenv('PATH=' . $this->directory);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('`clang` 21 is too old');
(new WasiToolchain())->detect();
}
public function testRejectsNonWasiClangTarget(): void
{
$this->installFakeTools(22, 47, 'x86_64-unknown-linux-gnu');
putenv('PATH=' . $this->directory);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('not configured for wasm32-wasi');
(new WasiToolchain())->detect();
}
private function installFakeTools(int $llvmMajor, int $wasmtimeMajor, string $target): void
{
foreach (['clang', 'llvm-ar', 'llvm-ranlib', 'llvm-nm', 'wasm-ld'] as $tool) {
$this->writeExecutable($tool, "#!/bin/sh\necho 'LLVM version {$llvmMajor}.0.0'\n");
}
$this->writeExecutable(
'clang++',
"#!/bin/sh\nif [ \"\$1\" = '--print-target-triple' ]; then echo '{$target}'; else echo 'clang version {$llvmMajor}.0.0'; fi\n",
);
$this->writeExecutable('wasmtime', "#!/bin/sh\necho 'wasmtime {$wasmtimeMajor}.0.0'\n");
}
private function writeExecutable(string $name, string $contents): void
{
$path = $this->directory . '/' . $name;
file_put_contents($path, $contents);
chmod($path, 0755);
}
}

@ -99,6 +99,19 @@ class CompilerBaseApiTest extends TestCase
return $projectFile;
}
public function testWasiTargetDetection(): void
{
foreach (['wasm32-wasi', 'wasm32-wasip1', 'wasm32-wasip1-threads'] as $target) {
$this->setPropertyValue('targetPlatform', $target);
$this->assertTrue($this->compiler->isWasiTarget(), $target);
}
foreach (['', 'wasm32-unknown-unknown', 'aarch64-linux-gnu'] as $target) {
$this->setPropertyValue('targetPlatform', $target);
$this->assertFalse($this->compiler->isWasiTarget(), $target);
}
}
private function createFakeClangFormat(string $binDir, string $logFile): void
{
mkdir($binDir, 0777, true);

@ -8,6 +8,19 @@ use TypePhp\Exception\TestError;
class FiberGeneratorTest extends TestCase
{
public function testWasiTargetRejectsGeneratorDuringPreparation(): void
{
$compiler = CompilerTest::create(ROOT_PATH);
$reflection = new \ReflectionClass($compiler);
$reflection->getProperty('targetPlatform')->setValue($compiler, 'wasm32-wasi');
$file = __DIR__ . '/../../code/generator-conversion-error.php';
$compiler->addFiles([$file]);
$this->expectException(TestError::class);
$this->expectExceptionMessage('Fiber and Generator are not supported by the WASI target');
$compiler->prepareFile($file);
}
public function testCompilerStateIsRestoredAfterGeneratorConversionError(): void
{
$compiler = CompilerTest::create(ROOT_PATH);

@ -6,9 +6,22 @@ use PHPUnit\Framework\TestCase;
use TypePhp\Platform\Windows;
use TypePhp\Platform\Linux;
use TypePhp\Platform\Macos;
use TypePhp\Platform\Wasi;
class PlatformTest extends TestCase
{
public function testWasiTargetProperties(): void
{
$platform = new Wasi('wasm32-unknown-wasip1');
$this->assertSame('WASI SDK (wasm32-unknown-wasip1)', $platform->getName());
$this->assertSame('.o', $platform->getObjectExtension());
$this->assertSame('.wasm', $platform->getExecutableExtension());
$this->assertSame('.a', $platform->getSharedLibraryExtension());
$this->assertSame('LL', $platform->getIntegerLiteralSuffix());
$this->assertSame([], $platform->getBuildLibraryWarnings('', '', 'bin'));
}
/**
* 测试 Windows 平台基本功能
*/

@ -0,0 +1,42 @@
<?php
namespace TypePhp\Tests;
use PHPUnit\Framework\TestCase;
use TypePhp\CompilerTest;
use TypePhp\Exception\TestError;
class WasiUnsupportedSyntaxTest extends TestCase
{
/** @dataProvider unsupportedConversionSyntaxProvider */
public function testUnsupportedSyntaxFailsDuringWasiConversion(string $file, string $message): void
{
$compiler = CompilerTest::create(ROOT_PATH);
(new \ReflectionClass($compiler))->getProperty('targetPlatform')->setValue($compiler, 'wasm32-wasi');
$source = __DIR__ . '/../code/' . $file;
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$this->expectException(TestError::class);
$this->expectExceptionMessage($message);
$compiler->convertFile($source);
}
public static function unsupportedConversionSyntaxProvider(): array
{
return [
'backtick shell execution' => [
'wasi-backtick.php',
'Backtick shell execution is not supported by the WASI target',
],
'generator closure' => [
'wasi-generator-closure.php',
'Fiber and Generator are not supported by the WASI target',
],
'generator arrow function' => [
'wasi-generator-arrow.php',
'Fiber and Generator are not supported by the WASI target',
],
];
}
}

@ -14,6 +14,7 @@ use TypePhp\Exception\Unsupported;
use TypePhp\Installer\LibPhpInstaller;
use TypePhp\Installer\LibPhpxInstaller;
use TypePhp\Platform\Linux;
use TypePhp\Platform\Wasi;
use TypePhp\Platform\Windows;
trait SourcePipelineTrait
@ -98,7 +99,7 @@ trait SourcePipelineTrait
// 仅在 PHP 脚本入口(bin/tpc.php)前置检测 phpx 库:缺少库立即 fatal,
// 避免继续向下执行到文件处理/编译阶段才报错。已编译的 tpc 可执行文件
// 在进入 main() 前就由动态链接器加载 libphpx,无需(也无法)在此检测。
if (defined('TYPEPHP_PHP_SCRIPT_ENTRY')) {
if (defined('TYPEPHP_PHP_SCRIPT_ENTRY') && !($this->getPlatform() instanceof Wasi)) {
$this->validatePhpxLibrary();
}

@ -0,0 +1,96 @@
<?php
namespace TypePhp\Build;
use RuntimeException;
final class WasiToolchain
{
public const MIN_LLVM_MAJOR = 22;
public const MIN_WASMTIME_MAJOR = 47;
/** @return array<string, string> */
public function detect(): array
{
$tools = [];
foreach (['clang', 'clang++', 'llvm-ar', 'llvm-ranlib', 'llvm-nm', 'wasm-ld', 'wasmtime'] as $name) {
$tools[$name] = $this->findExecutable($name);
}
$versions = [];
foreach (['clang', 'clang++', 'llvm-ar', 'llvm-ranlib', 'llvm-nm', 'wasm-ld'] as $name) {
$versions[$name] = $this->requireVersion($name, $tools[$name], self::MIN_LLVM_MAJOR);
}
$versions['wasmtime'] = $this->requireVersion('wasmtime', $tools['wasmtime'], self::MIN_WASMTIME_MAJOR);
[$exitCode, $target, $error] = $this->run([$tools['clang++'], '--print-target-triple']);
$target = trim($target);
if ($exitCode !== 0 || preg_match('/^wasm32-(?:unknown-)?wasi(?:p1)?$/', $target) !== 1) {
$detail = trim($error) !== '' ? ': ' . trim($error) : '';
throw new RuntimeException(
"clang++ from PATH is not configured for wasm32-wasi (reported target: "
. ($target !== '' ? $target : 'unknown') . "){$detail}",
);
}
$tools['target'] = $target;
$tools['clang-version'] = $versions['clang++'];
$tools['wasmtime-version'] = $versions['wasmtime'];
return $tools;
}
private function findExecutable(string $name): string
{
$path = getenv('PATH');
foreach (explode(PATH_SEPARATOR, is_string($path) ? $path : '') as $directory) {
if ($directory === '') {
continue;
}
$candidate = rtrim($directory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name;
if (is_file($candidate) && is_executable($candidate)) {
// Preserve the PATH entry instead of resolving symlinks. LLVM
// multicall binaries select their driver mode and adjacent
// .cfg file from argv[0] (notably clang++ and wasm-ld).
return $candidate;
}
}
throw new RuntimeException("Required WASI tool `{$name}` was not found in PATH");
}
private function requireVersion(string $name, string $executable, int $minimumMajor): string
{
[$exitCode, $output, $error] = $this->run([$executable, '--version']);
$versionText = trim($output . "\n" . $error);
if ($exitCode !== 0 || preg_match('/(?:version|wasmtime|LLD)\s+((\d+)(?:\.\d+)+)/i', $versionText, $match) !== 1) {
throw new RuntimeException("Unable to determine the version of WASI tool `{$name}` from PATH");
}
$major = (int) $match[2];
if ($major < $minimumMajor) {
throw new RuntimeException(
"WASI tool `{$name}` {$major} is too old; version {$minimumMajor} or newer is required",
);
}
return $match[1];
}
/** @return array{int, string, string} */
private function run(array $command): array
{
$process = proc_open(
$command,
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes,
);
if (!is_resource($process)) {
return [127, '', 'failed to start process'];
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
return [proc_close($process), $stdout, $stderr];
}
}

@ -581,6 +581,12 @@ class CompilerBase implements PropertyAccessContext
return $this->getPlatform() instanceof Macos;
}
public function isWasiTarget(): bool
{
$target = strtolower($this->targetPlatform);
return str_starts_with($target, 'wasm32-wasi') || str_starts_with($target, 'wasm32-wasip1');
}
public function isBuildModeBin(): bool
{
return $this->buildMode === self::BUILD_MODE_BIN;
@ -3804,6 +3810,9 @@ class CompilerBase implements PropertyAccessContext
protected function parseShellExec(Expr\ShellExec $expr): string
{
if ($this->isWasiTarget()) {
$this->fatalError($expr, 'Backtick shell execution is not supported by the WASI target');
}
$list = [];
foreach ($expr->parts as $part) {
$list[] = $this->identifierToStr($part);

@ -247,6 +247,9 @@ trait ClosureGenerator
protected function validateGeneratorClosure(Expr\ArrowFunction|Expr\Closure $expr, array $params): void
{
if ($this->isWasiTarget()) {
$this->fatalError($expr, 'Fiber and Generator are not supported by the WASI target');
}
if ($expr->byRef) {
$this->fatalError($expr, 'Generator closures returning by reference are not supported yet');
}

@ -61,6 +61,9 @@ trait FiberGenerator
protected function prepareGeneratorFunction(Function_|ClassMethod $v, FunctionDef $functionDef): void
{
if ($this->isWasiTarget()) {
$this->fatalError($v, 'Fiber and Generator are not supported by the WASI target');
}
if ($v->byRef) {
$this->fatalError($v, 'Generators returning by reference are not supported yet');
}

@ -0,0 +1,50 @@
<?php
namespace TypePhp\Platform;
final class Wasi extends UnixPlatform
{
public function __construct(private readonly string $target = 'wasm32-wasi')
{
}
public function getName(): string
{
return "WASI SDK ({$this->target})";
}
public function isCurrent(): bool
{
return false;
}
public function getSharedLibraryExtension(): string
{
return '.a';
}
public function getExecutableExtension(): string
{
return '.wasm';
}
public function getDefaultCompiler(): string
{
$compiler = getenv('TYPEPHP_WASI_CXX');
return is_string($compiler) && $compiler !== '' ? $compiler : 'clang++';
}
public function getBuildLibraryWarnings(
string $phpDir,
string $phpxDir,
string $buildMode,
bool $checkPhpxRuntime = true,
): array {
return [];
}
public function getIntegerLiteralSuffix(): string
{
return 'LL';
}
}

@ -37,6 +37,7 @@ use TypePhp\Generator\LibraryImportStubGenerator;
use TypePhp\Generator\Symbol;
use TypePhp\Metadata\Constants;
use TypePhp\Platform\PlatformFactory;
use TypePhp\Platform\Wasi;
use TypePhp\Platform\Windows;
use TypePhp\Resolver\Reflection;
use TypePhp\Resolver\ClassConstantValueTrait;
@ -164,8 +165,20 @@ class Translator extends Preprocessor
protected function detectPlatform(): void
{
try {
$this->platform = PlatformFactory::create();
$this->cppCompiler = CompilerFactory::detectCompilerName($this->platform);
$targetPlatform = $this->climate->arguments->defined('target-platform')
? (string) $this->climate->arguments->get('target-platform')
: '';
if (str_starts_with($targetPlatform, 'wasm32-wasi') || str_starts_with($targetPlatform, 'wasm32-wasip1')) {
$detectedTarget = getenv('TYPEPHP_WASI_TARGET');
$this->platform = new Wasi(
is_string($detectedTarget) && $detectedTarget !== '' ? $detectedTarget : $targetPlatform,
);
} else {
$this->platform = PlatformFactory::create();
}
$this->cppCompiler = $this->platform instanceof Wasi
? $this->platform->getDefaultCompiler()
: CompilerFactory::detectCompilerName($this->platform);
if ($this->platform instanceof Windows) {
$libInfo = $this->platform->detectPhpLibs($this->getPhpDir());
@ -181,8 +194,15 @@ class Translator extends Preprocessor
}
$this->compilerBackend = CompilerFactory::createByName($this->cppCompiler, $this->platform);
$backendName = $this->compilerBackend->getName();
if ($this->platform instanceof Wasi) {
$clangVersion = getenv('TYPEPHP_WASI_CLANG_VERSION');
$backendName = 'LLVM Clang'
. (is_string($clangVersion) && $clangVersion !== '' ? " {$clangVersion}" : '');
}
$label = $this->platform instanceof Wasi ? 'Initialized target/toolchain' : 'Initialized platform/backend';
$this->climate->info(
"Initialized platform/backend: {$this->platform->getName()} + {$this->compilerBackend->getName()} ({$this->compilerBackend->getCompilerCommand()})"
"{$label}: {$this->platform->getName()} + {$backendName} ({$this->compilerBackend->getCompilerCommand()})"
);
} catch (\Throwable $e) {
$this->error($e->getMessage());
@ -244,6 +264,7 @@ class Translator extends Preprocessor
$climate->tab()->out('--cxx-std <ver> C++ standard version (c++17, c++20, etc., default: c++17)');
$climate->tab()->out('--march <arch> Target CPU instruction set (e.g. native, x86-64-v3, armv8-a)');
$climate->tab()->out('--target-platform <triple> Cross-compilation target triple (e.g. aarch64-linux-gnu)');
$climate->tab()->out('--wasm Build a self-contained WASI command module from one PHP file');
$climate->tab()->out('--lto Enable Link Time Optimization (-flto)');
$climate->tab()->out('--no-literal-strings Disable literal strings optimization');
$climate->tab()->out('--php-version <ver> PHP language version to accept (8.2-8.5, default: 8.5)');
@ -754,7 +775,7 @@ class Translator extends Preprocessor
$code .= 'extern "C" void save_ps_args(int, char **) {}' . PHP_EOL;
}
if ($this->isBuildModeBin()) {
if ($this->isBuildModeBin() && !$this->isWasiTarget()) {
$cliHeaders = [
'#include "php_cli_process_title.h"',
'#include "php_cli_process_title_arginfo.h"',
@ -856,7 +877,7 @@ CODE;
$code .= "// clang-format off\n";
$code .= "static const zend_function_entry ext_functions[] = {\n";
if ($this->isBuildModeBin()) {
if ($this->isBuildModeBin() && !$this->isWasiTarget()) {
$code .= $this->getIndent() . "PHP_FE(cli_set_process_title, arginfo_cli_set_process_title)\n";
$code .= $this->getIndent() . "PHP_FE(cli_get_process_title, arginfo_cli_get_process_title)\n";
}
@ -885,7 +906,9 @@ CODE;
$code .= 'PHP_MINIT_FUNCTION(' . $this->getModuleName() . ') {' . PHP_EOL;
$code .= 'zend_try {' . PHP_EOL;
$code .= '// class/interface class entries' . PHP_EOL;
$code .= 'typephp_register_fiber_generator_class();' . PHP_EOL;
if (!$this->isWasiTarget()) {
$code .= 'typephp_register_fiber_generator_class();' . PHP_EOL;
}
$code .= 'if (typephp_install_reflection_attribute_handlers() != SUCCESS) {' . PHP_EOL;
$code .= $this->getIndent() . 'return FAILURE;' . PHP_EOL;
$code .= '}' . PHP_EOL;
@ -1289,7 +1312,9 @@ CODE;
{
$job = $this->maxJob;
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/typephp_fiber_generator.cc';
if (!$this->isWasiTarget()) {
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/typephp_fiber_generator.cc';
}
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/typephp_helper.cc';
// embed 需要 main 函数,以及 cli 的内置函数定义
@ -1297,7 +1322,7 @@ CODE;
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/typephp_main.cc';
}
if ($this->isBuildModeBin()) {
if ($this->isBuildModeBin() && !$this->isWasiTarget()) {
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/php_cli_process_title.c';
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/ps_title.c';
}

@ -1,5 +1,6 @@
<?php
use TypePhp\Translator;
use TypePhp\Build\WasiToolchain;
function main(int $argc, array $argv): void
{
@ -11,6 +12,11 @@ function main(int $argc, array $argv): void
define("ROOT_PATH", getcwd());
}
if (in_array('--wasm', $argv, true)) {
compileWasmProgram($argv);
return;
}
// .prof 文件分析模式:./tpc app.prof
if ($argc >= 2 && str_ends_with($argv[1], '.prof')) {
profileAnalyze($argc, $argv);
@ -31,6 +37,16 @@ function main(int $argc, array $argv): void
if ($translator->isDryRun()) {
$buildDir = $translator->getBuildDir();
$count = count($sourceFiles);
$sourceListFile = getenv('TYPEPHP_GENERATED_SOURCE_LIST');
if (is_string($sourceListFile) && $sourceListFile !== '') {
$sourceListDir = dirname($sourceListFile);
if (!is_dir($sourceListDir) && !mkdir($sourceListDir, 0777, true) && !is_dir($sourceListDir)) {
throw new RuntimeException("Unable to create generated source manifest directory: {$sourceListDir}");
}
if (file_put_contents($sourceListFile, implode(PHP_EOL, $sourceFiles) . PHP_EOL) === false) {
throw new RuntimeException("Unable to write generated source manifest: {$sourceListFile}");
}
}
$translator->output("Dry run completed: {$count} C++ source file(s) generated in {$buildDir}", 'lightBlue');
return;
}
@ -45,6 +61,108 @@ function main(int $argc, array $argv): void
}
}
/**
* Build a self-contained WASI command module through the compiler's public CLI.
* The lower-level build scripts are implementation details and are not part of
* the user-facing workflow.
*/
function compileWasmProgram(array $argv): void
{
$input = null;
$buildDir = null;
$arguments = array_slice($argv, 1);
for ($i = 0, $count = count($arguments); $i < $count; $i++) {
$argument = $arguments[$i];
if ($argument === '--wasm') {
continue;
}
if ($argument === '--build-dir') {
if (!isset($arguments[$i + 1]) || $arguments[$i + 1] === '') {
fwrite(STDERR, "Option --build-dir requires a directory\n");
exit(1);
}
$buildDir = $arguments[++$i];
continue;
}
if (str_starts_with($argument, '--build-dir=')) {
$buildDir = substr($argument, strlen('--build-dir='));
if ($buildDir === '') {
fwrite(STDERR, "Option --build-dir requires a directory\n");
exit(1);
}
continue;
}
if (str_starts_with($argument, '-')) {
fwrite(STDERR, "Unsupported option in --wasm mode: {$argument}\n");
exit(1);
}
if ($input !== null) {
fwrite(STDERR, "The --wasm mode accepts exactly one PHP input file\n");
exit(1);
}
$input = $argument;
}
if ($input === null) {
fwrite(STDERR, "Usage: php bin/tpc.php <program.php> --wasm [--build-dir <directory>]\n");
exit(1);
}
$workingDirectory = getcwd();
$buildDir ??= ROOT_PATH . DIRECTORY_SEPARATOR . 'build';
if (!str_starts_with($buildDir, DIRECTORY_SEPARATOR)
&& preg_match('/^[A-Za-z]:[\\\\\/]/', $buildDir) !== 1) {
$buildDir = $workingDirectory . DIRECTORY_SEPARATOR . $buildDir;
}
$builder = dirname(__DIR__) . '/projects/php-8.5.9/wasm/build-typephp-program.sh';
if (!is_executable($builder)) {
fwrite(STDERR, "TypePHP WASI builder is not executable: {$builder}\n");
exit(1);
}
try {
$tools = (new WasiToolchain())->detect();
} catch (RuntimeException $exception) {
fwrite(STDERR, "WASI toolchain check failed: {$exception->getMessage()}\n");
fwrite(STDERR, "Add WASI SDK and Wasmtime bin directories to PATH, then try again.\n");
exit(1);
}
$environment = getenv();
if (!is_array($environment)) {
$environment = [];
}
$environment['TYPEPHP_WASI_CC'] = $tools['clang'];
$environment['TYPEPHP_WASI_CXX'] = $tools['clang++'];
$environment['TYPEPHP_WASI_AR'] = $tools['llvm-ar'];
$environment['TYPEPHP_WASI_RANLIB'] = $tools['llvm-ranlib'];
$environment['TYPEPHP_WASI_NM'] = $tools['llvm-nm'];
$environment['TYPEPHP_WASI_LD'] = $tools['wasm-ld'];
$environment['TYPEPHP_WASMTIME'] = $tools['wasmtime'];
$environment['TYPEPHP_WASI_TARGET'] = $tools['target'];
$environment['TYPEPHP_WASI_CLANG_VERSION'] = $tools['clang-version'];
$environment['TYPEPHP_WASMTIME_VERSION'] = $tools['wasmtime-version'];
$environment['TYPEPHP_WASM_PROGRAM_BUILD_DIR'] = $buildDir;
$process = proc_open(
[$builder, $input],
[STDIN, STDOUT, STDERR],
$pipes,
getcwd(),
$environment,
);
if (!is_resource($process)) {
fwrite(STDERR, "Failed to start the TypePHP WASI builder\n");
exit(1);
}
$exitCode = proc_close($process);
if ($exitCode !== 0) {
exit($exitCode);
}
}
function profileAnalyze(int $argc, array $argv): void
{
$profFile = $argv[1];

Loading…
Cancel
Save