feat(wasm): add WASI SDK build orchestration and numeric libraries support

- Introduce build-sdk.sh script to orchestrate complete TypePHP WASI SDK building
- Add numeric-smoke-test.cc and link-numeric-smoke-test.sh for GMP/MPFR/mpdecimal validation
- Create high-precision.php example demonstrating bigInt/bigFloat/decimal usage
- Add test-typephp-program.sh for WASI integration testing with wasmtime
- Update ABI version from v2 to v4 and expand included system libraries
- Document new SDK build process and updated extension list in WASI_BUILD.md
- Include zlib, sodium, openssl, libxml, sqlite3, zip
pull/46/head
韩天峰 3 weeks ago
parent 3d36928e8b
commit abbaa11c7c
  1. 12
      docs/WASI_BUILD.md
  2. 38
      wasm/README.md
  3. 136
      wasm/build-sdk.sh
  4. 4
      wasm/build-typephp-program.sh
  5. 15
      wasm/examples/high-precision.php
  6. 38
      wasm/link-numeric-smoke-test.sh
  7. 52
      wasm/numeric-smoke-test.cc
  8. 35
      wasm/test-typephp-program.sh

@ -65,7 +65,7 @@ sources:
PHP、PHPX、TypePHP runtime、GMP、MPFR 和 mpdecimal 由 SDK 发布阶段预编译为 WASI 静态库。应用构建只编译 TypePHP 为当前程序生成的 C++,然后链接这些 `.a`。`tpc --wasm` 不会下载源码,也不会调用 PHP、PHPX 或高精度库的构建脚本。library 模式会调用 PHPX 包内固定版本的 `wit-bindgen` 生成当前应用的 Canonical ABI 绑定;普通用户不需要从 `PATH` 安装它。
PHP/WASI 当前静态内建 `date`、`pcre`、`hash`、`json`、`lexbor`、`random`、`Reflection`、`SPL`、`standard`、`uri`、`ctype`、`calendar`、`bcmath`、`filter``tokenizer` 扩展
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` 缓存也会自动失效。
@ -174,11 +174,11 @@ WASI 产物包含 TypePHP 的三种语言级高精度类型:
- `BigFloat`:MPFR 4.2.2
- `Decimal`:mpdecimal 4.0.1
完整示例位于 [high-precision.php](../projects/php-8.5.9/wasm/examples/high-precision.php)。构建并运行:
完整示例位于 [high-precision.php](../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
php bin/tpc.php --wasm wasm/examples/high-precision.php
wasmtime -S http high-precision.wasm
```
预期输出:
@ -200,7 +200,7 @@ wasm32 使用 32 位指针,但 PHP 的 `zend_long` 保持 64 位,以维持 T
- 保留 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、PDO 等 API 暴露为“可编译但链接失败”的接口。
PHPX Facade 只是为 PHP 可选扩展生成的便捷包装,并非 TypePHP ABI 的组成部分。WASI 下整体关闭它,可以避免把 curl、socket、Swoole 等不可用 API 暴露为“可编译但链接失败”的接口;PHP/WASI 静态内建扩展本身不受 Facade 开关影响
## WASI SDK 目录
@ -221,6 +221,6 @@ phpx/wasm/wasm32-wasip2/
└── .typephp-wasi-sdk-abi
```
普通用户通过 TypePHP/PHPX 集成安装包获得该目录。TypePHP 开发者需要自行 clone 与当前版本绑定的 `php-8.5.9-wasm` 和 PHPX 源码,分别构建 PHP、PHPX、GMP、MPFR 与 mpdecimal,再按上述结构安装到 PHPX checkout。若 PHPX 不在 `vendor/swoole/phpx`,继续使用已有的 `PHPX_HOME` 指向该 checkout。
普通用户通过 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。

@ -8,6 +8,14 @@ The integrated PHPX installer places their prebuilt `wasm32-wasip2` SDK at:
├── include/
│ ├── php/ PHP installed and generated headers
│ ├── phpx/ PHPX public and TypePHP runtime headers
│ ├── zlib.h zlib API used by PHP's static zlib extension
│ ├── zconf.h
│ ├── sodium.h
│ ├── openssl/ OpenSSL crypto-only public headers
│ ├── libxml2/ libxml2 public headers
│ ├── sqlite3.h
│ ├── zip.h
│ ├── bzlib.h
│ ├── gmp.h
│ ├── gmpxx.h
│ ├── mpfr.h
@ -24,10 +32,14 @@ The integrated PHPX installer places their prebuilt `wasm32-wasip2` SDK at:
└── .typephp-wasi-sdk-abi
```
The zlib, bzip2, libsodium, libcrypto, libxml2, SQLite, and libzip objects are
embedded in `libphp.a`; the SDK intentionally does not ship or link their
dependency archives separately.
The ABI file must contain exactly:
```text
typephp-wasip2-sdk-abi-v2
typephp-wasip2-sdk-abi-v4
```
TypePHP locates PHPX through the existing `PHPX_HOME` setting, Composer's
@ -38,12 +50,36 @@ 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.
TypePHP owns complete SDK orchestration. From the compiler repository, build
and install the matching PHP and PHPX portions with:
```shell
./wasm/build-sdk.sh \
--prefix "${PHPX_HOME}/wasm/wasm32-wasip2" \
--jobs 16
```
The PHP build produces only `libphp.a` and PHP headers. The PHPX build owns
GMP, MPFR, the vendored mpdecimal, `libphpx.a`, and their headers. The
orchestrator validates the combined installation before writing
`.typephp-wasi-sdk-abi`.
Autoconf, Bison, re2c, Rust, upstream `wit-bindgen`, and the PHP/PHPX source
trees are SDK producer dependencies only. They are never searched for by an
application build. PHPX release packages include the pinned host-side
`phpx-wit-bindgen` needed for application-specific exports below
`<phpx>/wasm/bin/<host-os>-<host-arch>/`.
SDK producer and TypePHP integration checks are kept with the TypePHP WASM
backend rather than php-src:
```text
wasm/link-numeric-smoke-test.sh GMP, MPFR, and mpdecimal link check
wasm/test-typephp-program.sh TypePHP high-precision integration check
wasm/numeric-smoke-test.cc Native numeric test program
wasm/examples/high-precision.php TypePHP integration example
```
## Language-level component exports
A WASI command remains the default and defines `main()`. A callable component

@ -0,0 +1,136 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
compiler_dir=$(cd "${script_dir}/.." && pwd)
usage()
{
cat <<'EOF'
Usage: ./wasm/build-sdk.sh --prefix <wasm32-wasip2-sdk-dir> [options]
Options:
--prefix <dir> Required complete SDK installation prefix
--php-source <dir> PHP source tree (default: projects/php-8.5.9)
--phpx-source <dir> PHPX source tree (default: PHPX_HOME or vendor package)
--build-dir <dir> Build root (default: /tmp/typephp-wasip2-sdk-build)
--jobs <number> Parallel build jobs (default: 8)
-h, --help Show this help
EOF
}
prefix=
php_source=${compiler_dir}/projects/php-8.5.9
phpx_source=${PHPX_HOME:-${compiler_dir}/vendor/swoole/phpx}
build_root=${TYPEPHP_WASM_SDK_BUILD_DIR:-/tmp/typephp-wasip2-sdk-build}
jobs=${TYPEPHP_WASM_JOBS:-8}
while [[ $# -gt 0 ]]; do
case "$1" in
--prefix)
[[ $# -ge 2 ]] || { echo "--prefix requires a directory" >&2; exit 2; }
prefix=$2
shift 2
;;
--prefix=*) prefix=${1#*=}; shift ;;
--php-source)
[[ $# -ge 2 ]] || { echo "--php-source requires a directory" >&2; exit 2; }
php_source=$2
shift 2
;;
--php-source=*) php_source=${1#*=}; shift ;;
--phpx-source)
[[ $# -ge 2 ]] || { echo "--phpx-source requires a directory" >&2; exit 2; }
phpx_source=$2
shift 2
;;
--phpx-source=*) phpx_source=${1#*=}; shift ;;
--build-dir)
[[ $# -ge 2 ]] || { echo "--build-dir requires a directory" >&2; exit 2; }
build_root=$2
shift 2
;;
--build-dir=*) build_root=${1#*=}; shift ;;
--jobs|-j)
[[ $# -ge 2 ]] || { echo "$1 requires a number" >&2; exit 2; }
jobs=$2
shift 2
;;
--jobs=*|-j*) jobs=${1#*=}; jobs=${jobs#-j}; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
esac
done
if [[ -z "${prefix}" ]]; then
echo "--prefix is required" >&2
usage >&2
exit 2
fi
if [[ ! "${jobs}" =~ ^[1-9][0-9]*$ ]]; then
echo "Invalid --jobs value: ${jobs}" >&2
exit 2
fi
if [[ ! -x "${php_source}/wasm/build.sh" ]]; then
echo "PHP/WASI build entry was not found: ${php_source}/wasm/build.sh" >&2
exit 1
fi
if [[ ! -x "${phpx_source}/wasm/build.sh" ]]; then
echo "PHPX/WASI build entry was not found: ${phpx_source}/wasm/build.sh" >&2
exit 1
fi
mkdir -p "${prefix}" "${build_root}"
prefix=$(cd "${prefix}" && pwd)
php_source=$(cd "${php_source}" && pwd)
phpx_source=$(cd "${phpx_source}" && pwd)
build_root=$(cd "${build_root}" && pwd)
"${php_source}/wasm/build.sh" \
--prefix "${prefix}" \
--build-dir "${build_root}/php" \
--jobs "${jobs}"
"${phpx_source}/wasm/build.sh" \
--prefix "${prefix}" \
--build-dir "${build_root}/phpx" \
--jobs "${jobs}"
required_files=(
.typephp-wasi-php-abi
.typephp-wasi-numeric-abi
.typephp-wasi-runtime-abi
include/php/main/php.h
include/php/main/php_config.h
include/phpx/phpx.h
include/phpx/typephp_helper.h
include/zlib.h
include/zconf.h
include/sodium.h
include/openssl/evp.h
include/libxml2/libxml/parser.h
include/sqlite3.h
include/zip.h
include/bzlib.h
include/gmp.h
include/mpfr.h
include/mpdecimal.h
include/decimal.hh
lib/libphp.a
lib/libphpx.a
lib/libgmp.a
lib/libgmpxx.a
lib/libmpfr.a
lib/libmpdec.a
lib/libmpdec++.a
)
for file in "${required_files[@]}"; do
if [[ ! -f "${prefix}/${file}" ]]; then
echo "TypePHP WASI SDK is incomplete: ${prefix}/${file}" >&2
exit 1
fi
done
printf '%s\n' 'typephp-wasip2-sdk-abi-v4' > "${prefix}/.typephp-wasi-sdk-abi"
echo "Installed complete TypePHP WASI 0.2 SDK: ${prefix}"

@ -108,7 +108,7 @@ 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
|| ! grep -qx 'typephp-wasip2-sdk-abi-v4' "${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."
@ -127,6 +127,8 @@ required_headers=(
php/ext/date/lib/timelib_config.h
phpx/phpx.h
phpx/typephp_helper.h
zlib.h
zconf.h
gmp.h
mpfr.h
decimal.hh

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
use native_types;
function main(): void
{
$integer = std::bigInt("123456789012345678901234567890");
echo ($integer * 9)->toString(), "\n";
$float = std::bigFloat("1000000000000000000000000000000");
echo ($float + std::bigFloat("1"))->toString(), "\n";
$decimal = std::decimal("12345.00000000000000001");
echo ($decimal + std::decimal("3.14159265358979323"))->toString(), "\n";
}

@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
compiler_dir=$(cd "${script_dir}/.." && pwd)
phpx_home=${PHPX_HOME:-${compiler_dir}/vendor/swoole/phpx}
prefix=${TYPEPHP_WASI_SDK_DIR:-${phpx_home}/wasm/wasm32-wasip2}
output=${TYPEPHP_WASM_NUMERIC_OUTPUT:-/tmp/typephp-wasm-numeric.wasm}
wasi_cxx=${TYPEPHP_WASI_CXX:-$(command -v wasm32-wasip2-clang++ || true)}
if [[ -z "${wasi_cxx}" ]]; then
echo "Required WASI tool 'wasm32-wasip2-clang++' was not found in PATH" >&2
exit 1
fi
for library in libgmp.a libgmpxx.a libmpfr.a libmpdec.a libmpdec++.a; do
if [[ ! -f "${prefix}/lib/${library}" ]]; then
echo "WASI numeric library not found: ${prefix}/lib/${library}" >&2
exit 1
fi
done
"${wasi_cxx}" \
-O0 \
-std=c++17 \
-fwasm-exceptions \
-mllvm -wasm-enable-sjlj \
-mllvm -wasm-use-legacy-eh=false \
-I"${prefix}/include" \
"${script_dir}/numeric-smoke-test.cc" \
-L"${prefix}/lib" \
-lmpdec++ -lmpdec -lmpfr -lgmpxx -lgmp \
-lwasi-emulated-signal \
-lsetjmp -lunwind -lm \
-o "${output}"
echo "Linked numeric WASI smoke test: ${output}"

@ -0,0 +1,52 @@
#include <gmpxx.h>
#include <mpfr.h>
#include <decimal.hh>
#include <cstdio>
#include <string>
int main()
{
mpz_class integer("18446744073709551616");
integer = integer * integer + 7;
if (integer.get_str() != "340282366920938463463374607431768211463") {
return 1;
}
mpfr_t value;
mpfr_init2(value, 256);
if (mpfr_set_str(value, "2", 10, MPFR_RNDN) != 0) {
mpfr_clear(value);
return 2;
}
mpfr_sqrt(value, value, MPFR_RNDN);
char float_buffer[96];
mpfr_snprintf(float_buffer, sizeof(float_buffer), "%.40RNf", value);
mpfr_clear(value);
if (std::string(float_buffer) != "1.4142135623730950488016887242096980785697") {
return 3;
}
decimal::Decimal small_decimal("1.25");
if (small_decimal.to_sci() != "1.25") {
std::fprintf(stderr, "unexpected parsed Decimal: %s\n", small_decimal.to_sci().c_str());
return 4;
}
small_decimal *= decimal::Decimal("8");
if (small_decimal.to_sci() != "10.00") {
std::fprintf(stderr, "unexpected small Decimal result: %s\n", small_decimal.to_sci().c_str());
return 5;
}
decimal::Context decimal_context(32);
decimal::Decimal decimal_value("12345678901234567890.125");
decimal_value = decimal_value.mul(decimal::Decimal("8"), decimal_context);
const std::string decimal_string = decimal_value.to_sci();
if (decimal_string != "98765431209876543121.000") {
std::fprintf(stderr, "unexpected Decimal result: %s\n", decimal_string.c_str());
return 6;
}
std::puts("TYPEPHP_WASM_NUMERIC_OK");
return 0;
}

@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
compiler_dir=$(cd "${script_dir}/.." && pwd)
output=${TYPEPHP_WASM_TEST_OUTPUT:-/tmp/typephp-wasm-high-precision.wasm}
wasmtime_bin=${TYPEPHP_WASMTIME:-$(command -v wasmtime || true)}
if [[ -z "${wasmtime_bin}" ]]; then
echo "Required WASI tool 'wasmtime' was not found in PATH" >&2
exit 1
fi
output_dir=$(dirname "${output}")
output_name=$(basename "${output}")
(
cd "${output_dir}"
php "${compiler_dir}/bin/tpc.php" --wasm=component "${script_dir}/examples/high-precision.php"
if [[ high-precision.wasm != "${output_name}" ]]; then
mv high-precision.wasm "${output_name}"
fi
)
actual=$(XDG_CACHE_HOME=${XDG_CACHE_HOME:-/tmp/typephp-wasmtime-cache} \
"${wasmtime_bin}" -S http "${output}")
expected=$'1111111101111111110111111111010\n1000000000000000000000000000001\n12348.14159265358979324'
if [[ "${actual}" != "${expected}" ]]; then
echo "Unexpected TypePHP/WASI output:" >&2
printf '%s\n' "${actual}" >&2
exit 1
fi
printf '%s\n' "${actual}"
echo "TypePHP/WASI integration test passed"
Loading…
Cancel
Save