From abbaa11c7ca627e0e9d1ac46d837af709f073b52 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sat, 8 Aug 2026 15:06:17 +0800 Subject: [PATCH] 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 --- docs/WASI_BUILD.md | 12 +-- wasm/README.md | 38 ++++++++- wasm/build-sdk.sh | 136 +++++++++++++++++++++++++++++++ wasm/build-typephp-program.sh | 4 +- wasm/examples/high-precision.php | 15 ++++ wasm/link-numeric-smoke-test.sh | 38 +++++++++ wasm/numeric-smoke-test.cc | 52 ++++++++++++ wasm/test-typephp-program.sh | 35 ++++++++ 8 files changed, 322 insertions(+), 8 deletions(-) create mode 100755 wasm/build-sdk.sh create mode 100644 wasm/examples/high-precision.php create mode 100755 wasm/link-numeric-smoke-test.sh create mode 100644 wasm/numeric-smoke-test.cc create mode 100755 wasm/test-typephp-program.sh diff --git a/docs/WASI_BUILD.md b/docs/WASI_BUILD.md index 3324daa0..a2ebb48f 100644 --- a/docs/WASI_BUILD.md +++ b/docs/WASI_BUILD.md @@ -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。 diff --git a/wasm/README.md b/wasm/README.md index 41f8f07b..95f5b087 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -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 `/wasm/bin/-/`. +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 diff --git a/wasm/build-sdk.sh b/wasm/build-sdk.sh new file mode 100755 index 00000000..dd275a08 --- /dev/null +++ b/wasm/build-sdk.sh @@ -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 [options] + +Options: + --prefix Required complete SDK installation prefix + --php-source PHP source tree (default: projects/php-8.5.9) + --phpx-source PHPX source tree (default: PHPX_HOME or vendor package) + --build-dir Build root (default: /tmp/typephp-wasip2-sdk-build) + --jobs 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}" diff --git a/wasm/build-typephp-program.sh b/wasm/build-typephp-program.sh index c917c8e2..7143f20e 100755 --- a/wasm/build-typephp-program.sh +++ b/wasm/build-typephp-program.sh @@ -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 diff --git a/wasm/examples/high-precision.php b/wasm/examples/high-precision.php new file mode 100644 index 00000000..e7d6668d --- /dev/null +++ b/wasm/examples/high-precision.php @@ -0,0 +1,15 @@ +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"; +} diff --git a/wasm/link-numeric-smoke-test.sh b/wasm/link-numeric-smoke-test.sh new file mode 100755 index 00000000..381293cb --- /dev/null +++ b/wasm/link-numeric-smoke-test.sh @@ -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}" diff --git a/wasm/numeric-smoke-test.cc b/wasm/numeric-smoke-test.cc new file mode 100644 index 00000000..9cf5b3a5 --- /dev/null +++ b/wasm/numeric-smoke-test.cc @@ -0,0 +1,52 @@ +#include +#include +#include + +#include +#include + +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; +} diff --git a/wasm/test-typephp-program.sh b/wasm/test-typephp-program.sh new file mode 100755 index 00000000..d80a8928 --- /dev/null +++ b/wasm/test-typephp-program.sh @@ -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"