feat(wasm): add WASI program build script and HTTP fetch support

- Add build-typephp-program.sh script for compiling PHP to WASI
- Integrate HTTP fetch capability via file_get_contents() using WASI HTTP
- Move WASI builder script from projects/php-8.5.9/wasm to wasm directory
- Add PhpxLocator class to resolve PHPX directory with proper fallback logic
- Update compiler to pass PHPX directory and compiler executable to builder
- Add WASI SDK layout
pull/46/head
韩天峰 3 weeks ago
parent 07c7d4b84a
commit 9e26887c03
  1. 36
      docs/TYPEPHP_WASI_BUILD.md
  2. 6
      examples/https.php
  3. 10
      examples/wasm-hello/README.md
  4. 5
      examples/wasm-hello/index.html
  5. 4
      examples/wasm-hello/main.js
  6. 4
      examples/wasm-hello/public/fetch-demo.json
  7. 25
      examples/wasm-hello/src/WasiDemo.php
  8. 3
      examples/wasm-hello/style.css
  9. 10
      examples/wasm-hello/typephp-worker.mjs
  10. 30
      package.php
  11. 47
      phpunit/src/Build/PhpxLocatorTest.php
  12. 48
      src/Build/PhpxLocator.php
  13. 58
      src/CompilerBase.php
  14. 17
      src/compiler.php
  15. 43
      wasm/README.md
  16. 204
      wasm/build-typephp-program.sh

@ -6,19 +6,18 @@ TypePHP 使用稳定的 WASI 0.2(Preview 2)和 Component Model。TypePHP 生
- WASI SDK 33 或更高版本(LLVM/Clang/LLD 22 或更高)
- PHP 8.4 或更高版本,用于运行 TypePHP 编译器
- Autoconf、Automake、Libtool、Bison、re2c 和常规 C/C++ 构建工具
- Wasmtime 47 或更高版本,用于运行和测试产物
- Jco 1 或更高版本,用于 browser profile;component profile 不需要 Jco
- 与当前 TypePHP 版本绑定的 `wasm32-wasip2` 集成 SDK
WASI SDK 的 `bin` 目录和 Wasmtime 必须加入系统 `PATH`。编译器不会探测或使用 `/opt` 等约定安装目录,也不接受专用的工具目录配置。可以继续通过环境变量覆盖缓存位置
WASI SDK 的 `bin` 目录和 Wasmtime 必须加入系统 `PATH`。编译器不会探测或使用 `/opt` 等约定安装目录,也不接受专用的工具目录配置。WASI 静态库和头文件统一安装到 PHPX 的 `wasm/wasm32-wasip2/`
```bash
export PATH="<wasi-sdk-bin>:<wasmtime-bin>:$PATH"
export PHP_WASM_BUILD_DIR=/tmp/php-8.5.9-wasip2-build
export TYPEPHP_WASM_DEPS_PREFIX=/tmp/typephp-wasip2-numeric/prefix
export TYPEPHP_WASM_RUNTIME_PREFIX=/tmp/typephp-wasip2-runtime
```
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`。所有工具只从 `PATH` 查找;npm script 会自动将项目本地的 `node_modules/.bin` 加入 `PATH`
## 一条命令构建
@ -66,7 +65,9 @@ wasm-browser-dir: generated
路径、sources 等详细配置继续放在 `project.yml`,不通过 `--wasm=` 传递。
PHP、PHPX、TypePHP runtime、GMP、MPFR 和 mpdecimal 会预编译并缓存为 WASI 静态库;正常的应用构建只编译 TypePHP 为当前程序生成的 C++,然后链接这些 `.a`。源码开发环境首次执行时会自动建立缺失的运行时缓存,发行包可直接携带预编译静态库。
PHP、PHPX、TypePHP runtime、GMP、MPFR 和 mpdecimal 由 SDK 发布阶段预编译为 WASI 静态库。应用构建只编译 TypePHP 为当前程序生成的 C++,然后链接这些 `.a`。`tpc --wasm` 不会下载源码、运行 `wit-bindgen`,也不会调用 PHP、PHPX 或高精度库的构建脚本。
PHP/WASI 当前静态内建 `date`、`pcre`、`hash`、`json`、`lexbor`、`random`、`Reflection`、`SPL`、`standard`、`uri`、`ctype`、`calendar`、`bcmath`、`filter` 和 `tokenizer` 扩展。
每个 C/C++ 翻译单元统一使用标准 Wasm C++ exceptions 和 WASI SJLJ;链接阶段将 ABI 警告视为错误,旧的 32 位 `zend_long` 缓存也会自动失效。
@ -123,8 +124,25 @@ wasm32 使用 32 位指针,但 PHP 的 `zend_long` 保持 64 位,以维持 T
PHPX Facade 只是为 PHP 可选扩展生成的便捷包装,并非 TypePHP ABI 的组成部分。WASI 下整体关闭它,可以避免把不存在的 curl、socket、Swoole、PDO 等 API 暴露为“可编译但链接失败”的接口。
## 内部构建层次
## WASI SDK 目录
集成 SDK 使用唯一、完整的前缀,位于 PHPX 根目录的 `wasm/wasm32-wasip2/`
```text
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
```
编译器内部会分别建立 PHP 8.5 NTS、GMP/MPFR/mpdecimal,以及 PHPX core/TypePHP runtime 的静态库缓存。内部构建脚本不是用户接口,不需要也不应由用户手动执行。发行包可直接附带目标平台对应的预编译 `.a` 文件。
普通用户通过 TypePHP/PHPX 集成安装包获得该目录。TypePHP 开发者需要自行 clone 与当前版本绑定的 `php-8.5.9-wasm` 和 PHPX 源码,分别构建 PHP、PHPX、GMP、MPFR 与 mpdecimal,再按上述结构安装到 PHPX checkout。若 PHPX 不在 `vendor/swoole/phpx`,继续使用已有的 `PHPX_HOME` 指向该 checkout
用户始终通过 `php bin/tpc.php --wasm program.php` 构建最终程序,避免 PHP、PHPX 和生成代码使用不一致的 `zend_long`、异常或 SJLJ ABI。
不提供单独覆盖 `libphp.a`、`libphpx.a` 或数值库的路径;所有库、头文件和 `.typephp-wasi-sdk-abi` 必须来自同一次兼容构建,避免混用不同的 `zend_long`、C++ exceptions、SJLJ 或 Component Model ABI。

@ -0,0 +1,6 @@
<?php
function main()
{
$homepage = file_get_contents('https://www.example.com/');
echo $homepage;
}

@ -8,10 +8,16 @@ Demo 展示以下已支持能力:
- WASI wall clock 和 PHP `time()` / `date()`
- 安全随机数 `random_int()` / `random_bytes()`
- 内存文件系统,以及可选的 OPFS 快照持久化
- 通过同步 `file_get_contents()` 发起 HTTP/HTTPS GET;浏览器等待期间由 JSPI 挂起 Wasm 调用栈
- PHP 8.5 runtime 信息
- TypePHP 语言级 BigInt、Decimal、BigFloat 高精度计算
网络、进程、shell、信号、Fiber 和 Generator 明确不支持。
原始 socket、进程、shell、信号、Fiber 和 Generator 明确不支持。
浏览器构建要求支持 `WebAssembly.Suspending``WebAssembly.promising`
的 JSPI 实现。HTTP 请求仍受浏览器 CORS、CSP 和 Mixed Content 策略约束。
`file_get_contents()` 当前支持 GET、`http.timeout` 和
`http.ignore_errors`;不提供 Curl API,也不会退化为忙等待。
## 构建
@ -59,3 +65,5 @@ npm run build
```
`src/` 是 TypePHP 应用,`typephp-worker.mjs` 是浏览器 WASI host,`main.js` 和 `style.css` 负责交互界面。`build/`、`component/`、`dist/`、`generated/` 均为可重新生成的输出。
WASI 运行库、PHPX 和生成的 C++ 均使用 `-O2` 编译。最终链接会移除调试与符号段,以减少浏览器下载、解析和编译 Wasm 的开销;构建过程不会自动调用系统中的 `wasm-opt`,避免旧版 Binaryen 与 WASI SDK 生成的 Wasm 异常指令不兼容。

@ -76,6 +76,9 @@
<article class="feature-card accent-green">
<div class="icon"></div><p>Filesystem</p><h3 id="filesystem-value"></h3><small id="files-value">Memory FS + OPFS snapshot</small>
</article>
<article class="feature-card accent-cyan">
<div class="icon"></div><p>HTTP fetch</p><h3 id="http-value"></h3><small id="http-detail">file_get_contents() + WASI HTTP</small>
</article>
</section>
<section class="detail-grid">
@ -106,7 +109,7 @@
<footer>
<span>TypePHP → C++17 → WASI 0.2 Component</span>
<span>运行于独立 ES module Worker · 网络能力关闭</span>
<span>运行于独立 ES module Worker · HTTP 通过 JSPI 挂起等待</span>
</footer>
</main>
<script type="module" src="/main.js"></script>

@ -3,7 +3,7 @@ const elements = Object.fromEntries([
'args', 'env', 'stdin', 'persistent', 'run', 'reset', 'status', 'output',
'runtime-value', 'platform-value', 'clock-value', 'random-value', 'token-value',
'filesystem-value', 'files-value', 'argv-value', 'env-value', 'stdin-value',
'bigint-value', 'decimal-value', 'bigfloat-value',
'http-value', 'http-detail', 'bigint-value', 'decimal-value', 'bigfloat-value',
].map((id) => [id, document.getElementById(id)]));
let worker = null;
@ -41,6 +41,8 @@ function renderReport(report) {
value('token-value', report.random.token);
value('filesystem-value', `${report.filesystem.run} 次运行`);
value('files-value', report.filesystem.files.join(' · '));
value('http-value', report.http.ok ? `${report.http.bytes} bytes` : '请求失败');
value('http-detail', report.http.preview);
value('argv-value', report.input.argv.join(' '));
value('env-value', report.input.greeting);
value('stdin-value', report.input.stdin);

@ -0,0 +1,4 @@
{
"message": "Hello from browser fetch via PHP file_get_contents()",
"runtime": "WASI HTTP 0.2 + JSPI"
}

@ -35,14 +35,35 @@ final class WasiDemo
'stdin' => trim($stdin),
],
'filesystem' => self::filesystemReport(),
'http' => self::httpReport(),
'precision' => self::precisionReport(),
'capabilities' => [
'supported' => ['arguments', 'environment', 'stdin/stdout/stderr', 'clock', 'random', 'filesystem'],
'disabled' => ['network', 'process', 'signals', 'shell', 'Fiber', 'Generator'],
'supported' => ['arguments', 'environment', 'stdin/stdout/stderr', 'clock', 'random', 'filesystem', 'HTTP GET'],
'disabled' => ['raw sockets', 'process', 'signals', 'shell', 'Fiber', 'Generator'],
],
];
}
private static function httpReport(): array
{
$url = getenv('TYPEPHP_FETCH_URL');
if ($url === false || $url === '') {
return ['ok' => false, 'url' => '', 'bytes' => 0, 'preview' => 'No URL configured'];
}
$body = file_get_contents($url);
if ($body === false) {
return ['ok' => false, 'url' => $url, 'bytes' => 0, 'preview' => 'Request failed'];
}
return [
'ok' => true,
'url' => $url,
'bytes' => strlen($body),
'preview' => trim(substr($body, 0, 80)),
];
}
private static function filesystemReport(): array
{
$directory = '/workspace';

@ -20,14 +20,13 @@ h1 { margin: 0; font-size: clamp(4rem, 8vw, 7rem); font-weight: 760; letter-spac
h1 span { color: transparent; background: linear-gradient(90deg, #77eaff, #a789ff 62%, #f7a95c); background-clip: text; }
.lede { max-width: 620px; margin: 28px 0 0; color: #9fb1ca; font-size: 1.08rem; line-height: 1.8; }
.runtime-orbit { position: relative; width: 280px; height: 280px; margin: auto; display: grid; place-items: center; }
.orbit-ring { position: absolute; inset: 18px; border: 1px solid #83e9ff40; border-radius: 50%; box-shadow: inset 0 0 60px #558cff0c; animation: spin 22s linear infinite; }
.orbit-ring { position: absolute; inset: 18px; border: 1px solid #83e9ff40; border-radius: 50%; box-shadow: inset 0 0 60px #558cff0c; }
.orbit-ring::before, .orbit-ring::after { content: ""; position: absolute; inset: 34px; border: 1px dashed #a789ff38; border-radius: 50%; }
.orbit-ring::after { inset: -15px; border-style: solid; border-color: #ffffff0d; }
.runtime-core { z-index: 1; display: grid; place-items: center; width: 120px; height: 120px; border: 1px solid #79e8ff55; border-radius: 32px; background: linear-gradient(145deg, #163552, #0b1b2c); color: #87ecff; text-align: center; box-shadow: 0 26px 80px #0008, inset 0 1px #ffffff18; transform: rotate(-5deg); }
.runtime-core strong { color: #fff; font-size: 2rem; }
.satellite { position: absolute; padding: 7px 11px; border: 1px solid #ffffff18; border-radius: 99px; background: #0c1929dd; color: #aebed3; font: 700 .66rem ui-monospace, monospace; }
.satellite.one { top: 20px; left: 31px; }.satellite.two { right: 3px; top: 108px; }.satellite.three { bottom: 22px; left: 16px; }
@keyframes spin { to { transform: rotate(360deg); } }
.panel { border: 1px solid #ffffff12; border-radius: 24px; background: linear-gradient(145deg, #0d1c2dcc, #091522d9); box-shadow: 0 25px 70px #00000035, inset 0 1px #ffffff09; backdrop-filter: blur(18px); }
.control-panel { padding: 30px; }
.panel-heading, .actions { display: flex; align-items: center; justify-content: space-between; gap: 24px; }

@ -96,6 +96,10 @@ self.onmessage = async ({ data }) => {
const persistent = data.persistent === true;
const storageName = String(data.storageName || 'typephp-wasi-filesystem.json');
try {
if (typeof WebAssembly.Suspending !== 'function'
|| typeof WebAssembly.promising !== 'function') {
throw new Error('This browser does not support WebAssembly JSPI, which is required for blocking WASI I/O');
}
fileData = persistent ? await loadFileData(storageName) : { dir: {} };
_setFileData(fileData);
_setStdin(inputHandler(String(data.stdin || '')));
@ -103,12 +107,13 @@ self.onmessage = async ({ data }) => {
_setStderr(outputHandler('stderr'));
const args = ['typephp.wasm', ...(Array.isArray(data.args) ? data.args.map(String) : [])];
const env = data.env && typeof data.env === 'object' ? data.env : {};
const env = data.env && typeof data.env === 'object' ? { ...data.env } : {};
env.TYPEPHP_FETCH_URL ??= new URL('/fetch-demo.json', self.location.href).href;
const wasi = new WASIShim({
sandbox: {
args,
env,
enableNetwork: false,
enableNetwork: true,
},
});
const { instantiate } = await import('./generated/program.js');
@ -131,5 +136,6 @@ self.onmessage = async ({ data }) => {
}
}
self.postMessage({ type: 'exit', code: exitCode });
self.close();
}
};

@ -595,13 +595,34 @@ function packageUnixLike(): void
$binary = 'tpc';
$versionFile = 'version.txt';
$phpxSourceDir = getenv('PHPX_HOME');
if (!is_string($phpxSourceDir) || $phpxSourceDir === '' || !is_dir($phpxSourceDir)) {
$phpxSourceDir = 'vendor/swoole/phpx';
}
$phpxSourceDir = realpath($phpxSourceDir) ?: rtrim($phpxSourceDir, '/\\');
$wasiSdkSourceRoot = $phpxSourceDir . '/wasm/wasm32-wasip2';
$wasiSdkPackageRoot = 'vendor/swoole/phpx/wasm/wasm32-wasip2';
$requiredFiles = [
$binary,
'composer.json',
'README.md',
'LICENSE.md',
'examples/hello.php',
'wasm/build-typephp-program.sh',
"{$wasiSdkSourceRoot}/.typephp-wasi-sdk-abi",
"{$wasiSdkSourceRoot}/include/php/main/php.h",
"{$wasiSdkSourceRoot}/include/php/main/php_config.h",
"{$wasiSdkSourceRoot}/include/php/Zend/zend_config.h",
"{$wasiSdkSourceRoot}/include/php/ext/date/lib/timelib_config.h",
"{$wasiSdkSourceRoot}/include/phpx/phpx.h",
"{$wasiSdkSourceRoot}/include/phpx/typephp_helper.h",
"{$wasiSdkSourceRoot}/include/gmp.h",
"{$wasiSdkSourceRoot}/include/mpfr.h",
"{$wasiSdkSourceRoot}/include/decimal.hh",
];
foreach (['libphp.a', 'libphpx.a', 'libgmp.a', 'libgmpxx.a', 'libmpfr.a', 'libmpdec.a', 'libmpdec++.a'] as $library) {
$requiredFiles[] = "{$wasiSdkSourceRoot}/lib/{$library}";
}
foreach ($requiredFiles as $file) {
if (!is_file($file)) {
throw new RuntimeException("Required package file not found: {$file}");
@ -682,6 +703,8 @@ function packageUnixLike(): void
mustCopy('LICENSE.md', "{$topLevelDir}/LICENSE.md");
mustCreateDirectory("{$topLevelDir}/examples");
mustCopy('examples/hello.php', "{$topLevelDir}/examples/hello.php");
copyDirectory('wasm', "{$topLevelDir}/wasm");
copyDirectory("{$phpxSourceDir}/wasm", "{$topLevelDir}/vendor/swoole/phpx/wasm");
if (is_file($outputFile) && !unlink($outputFile)) {
throw new RuntimeException("Unable to remove existing archive: {$outputFile}");
@ -717,7 +740,14 @@ function packageUnixLike(): void
"{$topLevelDir}/README.md",
"{$topLevelDir}/LICENSE.md",
"{$topLevelDir}/examples/hello.php",
"{$topLevelDir}/wasm/build-typephp-program.sh",
"{$topLevelDir}/{$wasiSdkPackageRoot}/.typephp-wasi-sdk-abi",
"{$topLevelDir}/{$wasiSdkPackageRoot}/include/php/main/php.h",
"{$topLevelDir}/{$wasiSdkPackageRoot}/include/phpx/phpx.h",
];
foreach (['libphp.a', 'libphpx.a', 'libgmp.a', 'libgmpxx.a', 'libmpfr.a', 'libmpdec.a', 'libmpdec++.a'] as $library) {
$requiredEntries[] = "{$topLevelDir}/{$wasiSdkPackageRoot}/lib/{$library}";
}
$verificationZip = new ZipArchive();
if ($verificationZip->open($outputFile) !== true) {
throw new RuntimeException("Unable to verify archive: {$outputFile}");

@ -0,0 +1,47 @@
<?php
namespace TypePhpTest\Build;
use PHPUnit\Framework\TestCase;
use TypePhp\Build\PhpxLocator;
final class PhpxLocatorTest extends TestCase
{
private string|false $originalPhpxHome;
private string $phpxHome;
protected function setUp(): void
{
$this->originalPhpxHome = getenv('PHPX_HOME');
$this->phpxHome = sys_get_temp_dir() . '/typephp-phpx-locator-' . bin2hex(random_bytes(6));
mkdir($this->phpxHome, 0777, true);
}
protected function tearDown(): void
{
if ($this->originalPhpxHome === false) {
putenv('PHPX_HOME');
} else {
putenv('PHPX_HOME=' . $this->originalPhpxHome);
}
rmdir($this->phpxHome);
}
public function testPhpxHomeHasPriorityAndReturnsAnAbsolutePath(): void
{
putenv('PHPX_HOME=' . $this->phpxHome);
self::assertSame(realpath($this->phpxHome), PhpxLocator::resolve('/not-used'));
}
public function testInvalidPhpxHomeFallsBackToComposerInstallation(): void
{
putenv('PHPX_HOME=' . $this->phpxHome . '/missing');
$projectRoot = dirname(__DIR__, 3);
self::assertSame(
realpath($projectRoot . '/vendor/swoole/phpx'),
PhpxLocator::resolve($projectRoot),
);
}
}

@ -0,0 +1,48 @@
<?php
namespace TypePhp\Build;
use Composer\InstalledVersions;
use RuntimeException;
final class PhpxLocator
{
public static function resolve(string $rootPath): string
{
$phpxHome = getenv('PHPX_HOME');
if (is_string($phpxHome) && $phpxHome !== '') {
$resolved = self::existingDirectory($phpxHome);
if ($resolved !== null) {
return $resolved;
}
}
if (class_exists(InstalledVersions::class) && InstalledVersions::isInstalled('swoole/phpx')) {
$installPath = InstalledVersions::getInstallPath('swoole/phpx');
if (is_string($installPath)) {
$resolved = self::existingDirectory($installPath);
if ($resolved !== null) {
return $resolved;
}
}
}
$resolved = self::existingDirectory(rtrim($rootPath, '/\\') . '/vendor/swoole/phpx');
if ($resolved !== null) {
return $resolved;
}
throw new RuntimeException(
"phpx directory not found. Set PHPX_HOME or install swoole/phpx with Composer.",
);
}
private static function existingDirectory(string $path): ?string
{
$path = rtrim($path, '/\\');
if (!is_dir($path)) {
return null;
}
return realpath($path) ?: $path;
}
}

@ -8,6 +8,8 @@
namespace TypePhp;
use TypePhp\Build\PhpxLocator;
use League\CLImate\CLImate;
use TypePhp\Backend\CompilerBackend;
use TypePhp\Backend\CompilerFactory;
@ -527,59 +529,11 @@ class CompilerBase implements PropertyAccessContext
protected function getPhpxDir(): string
{
// 解析 phpx 目录,优先级(macOS / Windows / Linux 通用):
// 1. PHPX_HOME 环境变量指定的目录
// 2. vendor 目录(Composer 安装路径,或 TypePHP 源码检出下的 vendor/swoole/phpx)
$phpxDir = getenv('PHPX_HOME');
if ($phpxDir !== false && $phpxDir !== '') {
$phpxDir = rtrim($phpxDir, '\/');
if (is_dir($phpxDir)) {
return $this->normalizePhpxDir($phpxDir);
}
}
// Composer-installed TypePHP and PHPX are sibling packages. Ask
// Composer for the actual package path instead of assuming a nested
// vendor directory inside the TypePHP package.
if (class_exists(\Composer\InstalledVersions::class)
&& \Composer\InstalledVersions::isInstalled('swoole/phpx')) {
$composerInstallPath = \Composer\InstalledVersions::getInstallPath('swoole/phpx');
if (is_string($composerInstallPath)) {
$composerInstallPath = rtrim($composerInstallPath, '\/');
if (is_dir($composerInstallPath)) {
return $this->normalizePhpxDir($composerInstallPath);
}
}
}
// TypePHP source checkout: phpx is installed below this repository.
$composerPhpxDir = $this->rootPath . self::PHPX_VENDOR_DIR;
if (is_dir($composerPhpxDir)) {
return $this->normalizePhpxDir($composerPhpxDir);
}
if (defined('ROOT_PATH')) {
$rootPhpxDir = ROOT_PATH . self::PHPX_VENDOR_DIR;
if (is_dir($rootPhpxDir)) {
return $this->normalizePhpxDir($rootPhpxDir);
}
}
// 两个路径都不存在,报错
$this->error(
'phpx directory not found. Please either:\n' .
'1. Set PHPX_HOME environment variable to your phpx installation path\n' .
'2. Install phpx via Composer: composer require swoole/phpx'
);
try {
return PhpxLocator::resolve($this->rootPath);
} catch (\RuntimeException $exception) {
$this->error($exception->getMessage());
}
/**
* 规范化 phpx 目录路径(解析相对路径与 ..,便于错误提示和后续路径拼接)
*/
private function normalizePhpxDir(string $dir): string
{
$real = realpath($dir);
return $real !== false ? $real : $dir;
}
protected function getPlatform(): PlatformBase

@ -2,6 +2,7 @@
use TypePhp\Translator;
use TypePhp\Build\WasiToolchain;
use TypePhp\Build\WasiProjectConfig;
use TypePhp\Build\PhpxLocator;
function main(int $argc, array $argv): void
{
@ -137,7 +138,7 @@ function compileWasmProgram(array $argv): void
exit(1);
}
$builder = dirname(__DIR__) . '/projects/php-8.5.9/wasm/build-typephp-program.sh';
$builder = dirname(__DIR__) . '/wasm/build-typephp-program.sh';
if (!is_executable($builder)) {
fwrite(STDERR, "TypePHP WASI builder is not executable: {$builder}\n");
exit(1);
@ -176,14 +177,22 @@ function compileWasmProgram(array $argv): void
$environment['TYPEPHP_WASI_CLANG_VERSION'] = $tools['clang-version'];
$environment['TYPEPHP_WASMTIME_VERSION'] = $tools['wasmtime-version'];
$environment['TYPEPHP_WASM_PROGRAM_BUILD_DIR'] = $project->buildDir;
$compilerExecutable = realpath($argv[0]);
if ($compilerExecutable === false || !is_executable($compilerExecutable)) {
fwrite(STDERR, "Unable to resolve the current TypePHP compiler executable: {$argv[0]}\n");
exit(1);
}
if ($project->browserDir !== null) {
$environment['TYPEPHP_WASM_BROWSER_DIR'] = $project->browserDir;
}
$command = [$builder, $project->input];
if ($project->output !== null) {
$command[] = $project->output;
try {
$phpxDir = PhpxLocator::resolve(ROOT_PATH);
} catch (RuntimeException $exception) {
fwrite(STDERR, "Unable to locate PHPX: {$exception->getMessage()}\n");
exit(1);
}
$command = [$builder, $project->input, $project->output ?? '-', $phpxDir, $compilerExecutable];
$process = proc_open(
$command,

@ -0,0 +1,43 @@
# TypePHP WASI SDK layout
TypePHP application builds never compile PHP, PHPX, GMP, MPFR, or mpdecimal.
The integrated PHPX installer places their prebuilt `wasm32-wasip2` SDK at:
```text
<phpx>/wasm/wasm32-wasip2/
├── include/
│ ├── php/ PHP installed and generated headers
│ ├── phpx/ PHPX public and TypePHP runtime headers
│ ├── gmp.h
│ ├── gmpxx.h
│ ├── mpfr.h
│ ├── mpdecimal.h
│ └── decimal.hh
├── lib/
│ ├── libphp.a
│ ├── libphpx.a
│ ├── libgmp.a
│ ├── libgmpxx.a
│ ├── libmpfr.a
│ ├── libmpdec.a
│ └── libmpdec++.a
└── .typephp-wasi-sdk-abi
```
The ABI file must contain exactly:
```text
typephp-wasip2-sdk-abi-v2
```
TypePHP locates PHPX through the existing `PHPX_HOME` setting, Composer's
`swoole/phpx` installation metadata, or `vendor/swoole/phpx`. TypePHP developers
who independently clone and build the matching `php-8.5.9-wasm` and PHPX
repositories install the complete SDK below that PHPX checkout. There is no
additional WASI SDK environment variable and no set of per-library search
paths: all headers, archives, and the ABI marker must be installed together so
an application cannot accidentally mix incompatible builds.
`wit-bindgen`, Autoconf, Bison, re2c, and the PHP/PHPX source trees are SDK
producer dependencies only. They are never searched for or invoked by
`tpc --wasm`.

@ -0,0 +1,204 @@
#!/usr/bin/env bash
set -euo pipefail
fatal_error() {
local red=''
local reset=''
if [[ -t 2 && -z "${NO_COLOR:-}" && "${TERM:-}" != dumb ]]; then
red=$'\033[1;31m'
reset=$'\033[0m'
fi
printf '%sFatal error: %s%s\n' "${red}" "$1" "${reset}" >&2
shift
for line in "$@"; do
printf '%s %s%s\n' "${red}" "${line}" "${reset}" >&2
done
exit 1
}
if [[ $# -ne 4 ]]; then
echo "Usage: $0 <program.php> <output.wasm|-> <phpx-dir> <tpc-executable>" >&2
exit 1
fi
caller_dir=${PWD}
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
compiler_dir=$(cd "${script_dir}/.." && pwd)
phpx_dir=$3
typephp_compiler=$4
wasi_sdk_dir=${phpx_dir}/wasm/wasm32-wasip2
wasi_include_dir=${wasi_sdk_dir}/include
wasi_php_include_dir=${wasi_include_dir}/php
wasi_phpx_include_dir=${wasi_include_dir}/phpx
wasi_library_dir=${wasi_sdk_dir}/lib
wasi_cxx=${TYPEPHP_WASI_CXX:?TYPEPHP_WASI_CXX is required}
input=$1
if [[ "${input}" != /* ]]; then
input=${caller_dir}/${input}
fi
input=$(realpath "${input}")
stem=$(basename "${input}" .php)
stem=${stem//[^a-zA-Z0-9_-]/_}
build_root=${TYPEPHP_WASM_PROGRAM_BUILD_DIR:-${caller_dir}/build}
mkdir -p "${build_root}"
build_root=$(cd "${build_root}" && pwd)
generated_dir=${build_root}
generated_source_list=${build_root}/.typephp-wasm-sources
cleanup_generated_source_list() {
rm -f -- "${generated_source_list}"
}
trap cleanup_generated_source_list EXIT
if [[ $2 != - ]]; then
output=$2
if [[ "${output}" != /* ]]; then
output=${caller_dir}/${output}
fi
else
output=${caller_dir}/${stem}.wasm
fi
mkdir -p "${generated_dir}" "$(dirname "${output}")"
# Convert first so target-specific source errors are reported before validating
# and linking the separately installed WASI SDK.
TYPEPHP_WASM_INTERNAL_COMPILE=1 TYPEPHP_GENERATED_SOURCE_LIST="${generated_source_list}" "${typephp_compiler}" "${input}" \
--dry \
--target-platform wasm32-wasip2 \
--build-dir "${generated_dir}" \
--no-progress \
--no-color
if [[ ! -s "${generated_source_list}" ]]; then
echo "TypePHP did not write the generated C++ source manifest: ${generated_source_list}" >&2
exit 1
fi
mapfile -t generated_sources < "${generated_source_list}"
if [[ ${#generated_sources[@]} -eq 0 ]]; then
echo "TypePHP did not generate any C++ source files" >&2
exit 1
fi
wasi_sdk_stamp=${wasi_sdk_dir}/.typephp-wasi-sdk-abi
if [[ ! -f "${wasi_sdk_stamp}" ]] \
|| ! grep -qx 'typephp-wasip2-sdk-abi-v2' "${wasi_sdk_stamp}"; then
fatal_error \
"TypePHP WASI SDK is missing or ABI-incompatible: ${wasi_sdk_dir}" \
"Install the matching PHPX package or set PHPX_HOME to its installation directory."
fi
required_libraries=(libphp.a libphpx.a libgmp.a libgmpxx.a libmpfr.a libmpdec.a libmpdec++.a)
for library in "${required_libraries[@]}"; do
if [[ ! -f "${wasi_library_dir}/${library}" ]]; then
fatal_error "TypePHP WASI SDK library is missing: ${wasi_library_dir}/${library}"
fi
done
required_headers=(
php/main/php.h
php/main/php_config.h
php/Zend/zend_config.h
php/ext/date/lib/timelib_config.h
phpx/phpx.h
phpx/typephp_helper.h
gmp.h
mpfr.h
decimal.hh
)
for header in "${required_headers[@]}"; do
if [[ ! -f "${wasi_include_dir}/${header}" ]]; then
fatal_error "TypePHP WASI SDK header is missing: ${wasi_include_dir}/${header}"
fi
done
compile_flags=(
-std=c++17
-O2
-fwasm-exceptions
-mllvm -wasm-enable-sjlj
-mllvm -wasm-use-legacy-eh=false
-Wno-deprecated-literal-operator
)
include_flags=(
-I"${wasi_php_include_dir}"
-I"${wasi_php_include_dir}/main"
-I"${wasi_php_include_dir}/Zend"
-I"${wasi_php_include_dir}/TSRM"
-I"${wasi_php_include_dir}/ext/date/lib"
-I"${wasi_phpx_include_dir}"
-I"${wasi_include_dir}"
-I"${generated_dir}/include"
)
generated_objects=()
for source in "${generated_sources[@]}"; do
if [[ ! -f "${source}" ]]; then
echo "Generated C++ source file not found: ${source}" >&2
exit 1
fi
object=${source%.cc}.o
"${wasi_cxx}" "${compile_flags[@]}" "${include_flags[@]}" -c "${source}" -o "${object}"
generated_objects+=("${object}")
done
# Every generated object and runtime archive is already built with -O2. Keep
# the final driver invocation optimized as well, but do not let Clang discover
# an arbitrary system wasm-opt: older Binaryen releases cannot parse the Wasm
# exception-reference instructions emitted by the current WASI SDK. Stripping
# linker metadata has a much larger browser startup benefit than another slow
# whole-module optimization pass and does not change runtime semantics.
"${wasi_cxx}" \
-O2 \
--no-wasm-opt \
-std=c++17 \
-fwasm-exceptions \
"${generated_objects[@]}" \
-Wl,--whole-archive \
"${wasi_library_dir}/libphpx.a" \
-Wl,--no-whole-archive \
"${wasi_library_dir}/libphp.a" \
"${wasi_library_dir}/libmpdec++.a" \
"${wasi_library_dir}/libmpdec.a" \
"${wasi_library_dir}/libmpfr.a" \
"${wasi_library_dir}/libgmpxx.a" \
"${wasi_library_dir}/libgmp.a" \
-lwasi-emulated-signal -lsetjmp -lunwind -ldl -lm \
-Wl,--strip-all \
-Wl,--fatal-warnings \
-o "${output}"
echo "Built TypePHP/WASI program: ${output}"
if [[ "${TYPEPHP_WASM_BROWSER:-1}" == 1 ]]; then
# Chrome does not yet load components natively, so Jco lowers the same
# WASI 0.2 component to core Wasm + ESM.
jco_bin=${TYPEPHP_JCO:-jco}
browser_dir=${TYPEPHP_WASM_BROWSER_DIR:-${output%.wasm}.browser}
mkdir -p "${browser_dir}"
jco_flags=()
if "${jco_bin}" transpile --help 2>&1 | grep -q -- '--bindgen-enable-wasm-exnref'; then
jco_flags+=(--bindgen-enable-wasm-exnref)
fi
if ! "${jco_bin}" transpile --help 2>&1 | grep -q -- '--async-wasi-imports'; then
echo "Jco does not support JSPI-backed asynchronous WASI imports; upgrade Jco" >&2
exit 1
fi
jco_flags+=(--async-mode jspi --async-wasi-imports --async-wasi-exports)
"${jco_bin}" transpile "${output}" \
-o "${browser_dir}" \
--name program \
--no-nodejs-compat \
--no-namespaced-exports \
--instantiation async \
--base64-cutoff=0 \
"${jco_flags[@]}"
echo "Built TypePHP/WASI browser module: ${browser_dir}"
fi
if [[ "${TYPEPHP_WASM_RUN:-0}" == 1 ]]; then
wasmtime_bin=${TYPEPHP_WASMTIME:-wasmtime}
XDG_CACHE_HOME=${XDG_CACHE_HOME:-/tmp/typephp-wasmtime-cache} \
"${wasmtime_bin}" "${output}"
fi
Loading…
Cancel
Save