12 KiB
构建 TypePHP WASI 程序
TypePHP 使用稳定的 WASI 0.2(Preview 2)和 Component Model。TypePHP 生成的 C++、PHPX 核心、精简的 PHP 8.5 NTS、GMP、MPFR 和 mpdecimal 会静态链接为单个 .wasm command 或 library component。WASI 0.1(Preview 1)不受支持。
环境要求
- WASI SDK 33 或更高版本(LLVM/Clang/LLD 22 或更高)
- PHP 8.4 或更高版本,用于运行 TypePHP 编译器
- Wasmtime 47 或更高版本,用于运行和测试产物
- Jco 1 或更高版本,用于 browser profile;component profile 不需要 Jco
- wit-bindgen-cli 0.60.0,用于 library/WasmExport 模式;command 模式不需要
- 与当前 TypePHP 版本绑定的
wasm32-wasip2集成 SDK
WASI SDK 的 bin 目录和 Wasmtime 必须加入系统 PATH。编译器不会探测或使用 /opt 等约定安装目录,也不接受专用的工具目录配置。WASI 静态库和头文件统一安装到 PHPX 的 wasm/wasm32-wasip2/:
export PATH="<wasi-sdk-bin>:<wasmtime-bin>:$PATH"
TypePHP 使用现有的 PHPX 定位规则:优先读取 PHPX_HOME,其次读取 Composer 的 swoole/phpx 安装位置,最后使用 vendor/swoole/phpx。不新增 WASI 专用环境变量。
WASI 构建会检查 wasm32-wasip2-clang、wasm32-wasip2-clang++、llvm-ar、llvm-ranlib、llvm-nm、wasm-component-ld 和 wasmtime,并确认目标是 wasm32-unknown-wasip2。browser profile 另外检查 jco,library 模式另外检查固定版本的 wit-bindgen。所有工具只从 PATH 查找;npm script 会自动将项目本地的 node_modules/.bin 加入 PATH。
一条命令构建
command 模式的源文件必须提供 main(): void:
<?php
function main(): void
{
echo "Hello from TypePHP/WASI\n";
}
执行:
php bin/tpc.php --wasm hello.php
单文件输入默认只生成当前目录下可由 Wasmtime 执行的 hello.wasm Component,不要求安装 Jco。生成的 .cc 与 host 模式使用相同的 build 目录规则,默认位于 TypePHP 根目录的 build/;可以使用 --build-dir <directory> 覆盖。
项目可以直接使用 project.yml:
name: wasm-hello
mode: bin
wasm: component
build-dir: build
output: component/wasm-hello.wasm
sources:
- src
wasm 只接受 component 或 browser,不接受布尔值。配置后直接执行 php bin/tpc.php project.yml 即可进入 WASI 构建,无需重复传入 --wasm。WASM 项目未配置 target-platform 时默认使用 wasm32-wasip2;build-dir、output 和 wasm-browser-dir 都相对于项目文件解析。完整浏览器应用见 examples/wasm-hello/,它显式使用 wasm: browser。
需要生成浏览器模块时,配置 wasm: browser 和 wasm-browser-dir,并确保 Jco 位于 PATH。
命令行也可以显式选择产物:
--wasm或--wasm=component:仅生成可由 Wasmtime 运行的 Component,不检测 Jco。--wasm=browser:生成 Component 和 Jco 浏览器模块,需要jco位于PATH。
路径、sources 等详细配置继续放在 project.yml,不通过 --wasm= 传递。
PHP、PHPX、TypePHP runtime、GMP、MPFR 和 mpdecimal 由 SDK 发布阶段预编译为 WASI 静态库。应用构建只编译 TypePHP 为当前程序生成的 C++,然后链接这些 .a。tpc --wasm 不会下载源码,也不会调用 PHP、PHPX 或高精度库的构建脚本。library 模式会调用 PATH 中的 wit-bindgen-cli 0.60.0 生成当前应用的 Canonical ABI 绑定。
PHP/WASI 当前静态内建 date、pcre、hash、json、lexbor、random、Reflection、SPL、standard、uri、ctype、calendar、bcmath、filter、tokenizer、mbstring、zlib、fileinfo、sodium、openssl、libxml、dom、SimpleXML、xml、xmlreader、xmlwriter、PDO、pdo_sqlite、zip、bz2 和 exif 扩展。OpenSSL 采用 crypto-only 构建,不包含 TLS stream transport;HTTP/HTTPS 仍由 WASI HTTP Component 提供。
每个 C/C++ 翻译单元统一使用标准 Wasm C++ exceptions 和 WASI SJLJ;链接阶段将 ABI 警告视为错误,旧的 32 位 zend_long 缓存也会自动失效。
运行:
wasmtime hello.wasm
Chrome Demo:
cd examples/wasm-hello
npm ci
npm run wasm
npm run dev
浏览器端始终在专用 Worker 中执行 Component。默认使用内存文件系统;发送给 Worker 的启动消息设置 persistent: true 后,会在启动和退出时通过 OPFS 恢复、保存文件系统快照。程序执行期间仍使用同步内存文件系统,避免每次 PHP 文件访问跨越异步 JS 边界。
Command 与 Library 的 ZendVM 生命周期
Command 模式
command 模式具有生成的 C++ main() 入口。入口依次调用:
typephp_runtime_init(argc, argv)
→ php_embed_init()
→ PHP/SAPI module startup 与 MINIT
→ PHP request startup 与 RINIT
→ 注册并启动当前 TypePHP 应用模块
→ 当前应用的 MINIT 与 RINIT
执行 TypePHP main()
typephp_runtime_shutdown()
→ 当前应用的 RSHUTDOWN 与模块清理
→ php_embed_shutdown()
→ PHP request/module/SAPI shutdown
调用者不需要感知这些步骤,因为生成的原生 main() 会自动包围整个程序生命周期。
Library 模式必须先创建 runtime resource
library component 没有可自动执行的 main(),单纯实例化 .wasm 只完成 Component 和 C/C++ Runtime 的实例化,不代表 ZendVM request 已经可用。Host 必须先调用生成的 WIT 函数:
create-runtime: func() -> result<runtime, typephp-error>;
浏览器中对应的调用为:
const component = await instantiate(null, wasi.getImportObject());
const runtime = await component.api.createRuntime();
try {
const result = await runtime.someExportedFunction();
} finally {
runtime[Symbol.dispose]();
}
createRuntime() 内部调用 typephp_runtime_init(1, argv)。Host 只需要调用这一层稳定接口,不应直接调用 php_embed_init()、MINIT、RINIT 或任何 Zend C API。
当前初始化顺序如下:
php_embed_init()初始化 Embed SAPI、PHP 核心和静态扩展,并启动 PHP request;PHP 核心与已经注册的静态扩展在这里完成 MINIT/RINIT。- 设置 PHPX 的异常桥接,使 PHP 异常可以安全返回到生成的 WIT
result。 - 取得当前 TypePHP 应用的
zend_module_entry,调用zend_register_module_ex()和zend_startup_module_ex(),完成应用模块注册与 MINIT。 - 注册标准流并设置请求路径等 SAPI 请求信息。
- 因为 Embed request 和请求内存池此时已经启动,生成代码会显式调用当前应用模块的
request_startup_func,补做该模块的 RINIT;RINIT 再初始化 TypePHP 请求级全局变量和类静态数据,完成后才返回runtimeresource。
这里“手动”调用的是 Host 可见的 create-runtime(),而不是让用户手动拼装 ZendVM 生命周期。MINIT/RINIT 的具体调用及其先后顺序全部封装在 PHPX 和生成的 Component adapter 中。
导出调用共享同一个 request
同一 runtime resource 上的所有 #[WasmExport] 调用共享一次 RINIT 建立的 Zend request:
- 不会在每次函数调用前后重复执行 RINIT/RSHUTDOWN。
- PHP request 内存池、请求级全局变量和静态状态会持续到 resource 被释放。
- 当前仅支持 NTS;同一个 runtime 上的调用必须串行,生成的 adapter 会拒绝并发或重入调用。
- 普通 PHP 异常会被转换为 WIT
result错误,runtime 仍然可以继续使用。 - Zend bailout 表示请求状态可能已经损坏,adapter 会将 runtime 标记为 failed,后续调用会被拒绝,直到 resource 被释放。
释放 resource 才会执行 RSHUTDOWN
释放 WIT runtime resource 会调用 typephp_runtime_shutdown():
- 调用当前 TypePHP 应用模块的 RSHUTDOWN,清理 TypePHP 请求级对象和全局数据。
- 注销并关闭当前应用模块,执行相应模块清理。
- 调用
php_embed_shutdown(),完成其余扩展的 request shutdown、module shutdown 和 SAPI shutdown。 - 最后释放 request 内存池,避免 PHP/CPP 包装对象在内存池消失后继续析构。
不要只依赖 JavaScript GC 触发 resource finalizer。浏览器和 Node Host 应在 finally 中显式调用 runtime[Symbol.dispose]();Wasmtime 或其他 Host binding 也应显式 drop resource。直接终止 Worker 或进程会回收整个 Wasm 实例,但不保证 PHP 的 RSHUTDOWN/MSHUTDOWN 回调得到执行,因此不能把必须持久化的数据只放在关闭回调中。
一个 Component 实例当前只允许同时存在一个活动的 runtime resource。释放完成后可以重新创建;初始化失败或发生 Zend bailout 时,应先释放旧 resource,而不是继续调用导出函数。
高精度类型
WASI 产物包含 TypePHP 的三种语言级高精度类型:
BigInt:GMP 6.3.0BigFloat:MPFR 4.2.2Decimal:mpdecimal 4.0.1
完整示例位于 high-precision.php。构建并运行:
php bin/tpc.php --wasm examples/high-precision.php
wasmtime -S http high-precision.wasm
预期输出:
1111111101111111110111111111010
1000000000000000000000000000001
12348.14159265358979324
wasm32 使用 32 位指针,但 PHP 的 zend_long 保持 64 位,以维持 TypePHP 与 64 位 PHP 的整数语义。GMP 和 mpdecimal 使用 32 位 limb;这不改变任意精度语义,但大数吞吐量低于具有汇编优化的原生 64 位构建。
当前平台边界
- 仅支持 NTS、单线程。
- Fiber 和 Generator 被禁用;编译器在发现
yield时直接报致命错误。 - PHPX Facade API 在
__wasi__下整体禁用。PHPX 核心类型和phpx_std仍可使用。 - 不支持动态扩展、网络 socket、进程、shell 和信号。静态可识别的调用会在编译期报致命错误。
- 保留 PHP stream 框架、本地文件能力以及由 WASI host 提供的时间和随机数能力。
- command component 可由 Wasmtime 直接运行;library component 需要 Host 按 WIT 接口调用
create-runtime()和导出函数。Chrome 使用 Jco 生成的 ESM 和examples/wasm-hello/typephp-worker.mjs中的 Worker host。
PHPX Facade 只是为 PHP 可选扩展生成的便捷包装,并非 TypePHP ABI 的组成部分。WASI 下整体关闭它,可以避免把 curl、socket、Swoole 等不可用 API 暴露为“可编译但链接失败”的接口;PHP/WASI 静态内建扩展本身不受 Facade 开关影响。
WASI SDK 目录
集成 SDK 使用唯一、完整的前缀,位于 PHPX 根目录的 wasm/wasm32-wasip2/:
phpx/wasm/wasm32-wasip2/
├── include/php/ # PHP 安装头文件
├── include/phpx/ # PHPX 和 TypePHP runtime 头文件
├── include/gmp.h ...
├── lib/libphp.a
├── lib/libphpx.a
├── lib/libgmp.a
├── lib/libgmpxx.a
├── lib/libmpfr.a
├── lib/libmpdec.a
├── lib/libmpdec++.a
└── .typephp-wasi-sdk-abi
普通用户通过 TypePHP/PHPX 集成安装包获得该目录。TypePHP 开发者需要自行 clone 与当前版本绑定的 php-8.5.9-wasm 和 PHPX 源码,并通过 wasm/build-sdk.sh 组装完整 SDK。PHP/WASI 只负责 PHP;PHPX 负责 GMP、MPFR、其专属的 mpdecimal 以及 PHPX runtime。所有产物安装到同一个 PHPX checkout。若 PHPX 不在 vendor/swoole/phpx,继续使用已有的 PHPX_HOME 指向该 checkout。
不提供单独覆盖 libphp.a、libphpx.a 或数值库的路径;所有库、头文件和 .typephp-wasi-sdk-abi 必须来自同一次兼容构建,避免混用不同的 zend_long、C++ exceptions、SJLJ 或 Component Model ABI。