TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

13 KiB

Building TypePHP WASI Programs

TypePHP uses the stable WASI 0.2 (Preview 2) and the Component Model. The C++ generated by TypePHP, the PHPX core, a trimmed PHP 8.5 NTS, GMP, MPFR, and mpdecimal are statically linked into a single .wasm command or library component. WASI 0.1 (Preview 1) is not supported.

Environment Requirements

  • WASI SDK 33 or higher (LLVM/Clang/LLD 22 or higher)
  • PHP 8.4 or higher, used to run the TypePHP compiler
  • Wasmtime 47 or higher, used to run and test artifacts
  • Jco 1 or higher, used for the browser profile; the component profile does not need Jco
  • wit-bindgen-cli 0.60.0, used for library/WasmExport mode; command mode does not need it
  • The wasm32-wasip2 integrated SDK bound to the current TypePHP version

The WASI SDK's bin directory and Wasmtime must be added to the system PATH. The compiler does not probe or use conventional installation directories such as /opt, and does not accept dedicated tool directory configuration. WASI static libraries and headers are installed uniformly into PHPX's wasm/wasm32-wasip2/:

export PATH="<wasi-sdk-bin>:<wasmtime-bin>:$PATH"

TypePHP uses the existing PHPX locating rules: first read PHPX_HOME, then the Composer swoole/phpx installation location, and finally vendor/swoole/phpx. No WASI-specific environment variable is added.

The WASI build checks wasm32-wasip2-clang, wasm32-wasip2-clang++, llvm-ar, llvm-ranlib, llvm-nm, wasm-component-ld, and wasmtime, and confirms the target is wasm32-unknown-wasip2. The browser profile additionally checks jco, and library mode additionally checks a fixed version of wit-bindgen. All tools are found only from PATH; npm scripts automatically add the project-local node_modules/.bin to PATH.

One-command Build

The source file for command mode must provide main(): void:

<?php
function main(): void
{
    echo "Hello from TypePHP/WASI\n";
}

Run:

php bin/tpc.php --wasm hello.php

Single-file input by default generates only a hello.wasm Component in the current directory that can be executed by Wasmtime, without requiring Jco. The generated .cc uses the same build directory rules as host mode, defaulting to the TypePHP root's build/; it can be overridden with --build-dir <directory>.

A project can directly use project.yml:

name: wasm-hello
mode: bin
wasm: component
build-dir: build
output: component/wasm-hello.wasm
sources:
  - src

wasm only accepts component or browser, not a boolean value. After configuring, directly run php bin/tpc.php project.yml to enter the WASI build without repeatedly passing --wasm. WASM projects default to wasm32-wasip2 when target-platform is not configured; build-dir, output, and wasm-browser-dir are all resolved relative to the project file. See examples/wasm-hello/ for a complete browser application, which explicitly uses wasm: browser.

When you need to generate a browser module, configure wasm: browser and wasm-browser-dir, and ensure Jco is in PATH.

The command line can also explicitly select the artifact:

  • --wasm or --wasm=component: generate only a Component runnable by Wasmtime, without checking Jco.
  • --wasm=browser: generate the Component and the Jco browser module, requiring jco to be in PATH.

Detailed configuration such as paths and sources continues to live in project.yml, not passed through --wasm=.

PHP, PHPX, the TypePHP runtime, GMP, MPFR, and mpdecimal are precompiled into WASI static libraries during the SDK release stage. The application build only compiles the C++ generated by TypePHP for the current program, then links these .a. tpc --wasm does not download source code, nor does it invoke the build scripts of PHP, PHPX, or high-precision libraries. Library mode invokes wit-bindgen-cli 0.60.0 from PATH to generate the current application's Canonical ABI bindings.

PHP/WASI currently statically includes the 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, and exif extensions. OpenSSL uses a crypto-only build without the TLS stream transport; HTTP/HTTPS is still provided by the WASI HTTP Component.

Every C/C++ translation unit uniformly uses standard Wasm C++ exceptions and WASI SJLJ; the linking stage treats ABI warnings as errors, and old 32-bit zend_long caches are automatically invalidated.

Run:

wasmtime hello.wasm

Chrome Demo:

cd examples/wasm-hello
npm ci
npm run wasm
npm run dev

The browser side always executes the Component in a dedicated Worker. The in-memory filesystem is used by default; after the startup message sent to the Worker sets persistent: true, the filesystem snapshot is restored and saved through OPFS at startup and exit. During program execution the synchronous in-memory filesystem is still used, avoiding crossing the asynchronous JS boundary on every PHP file access.

ZendVM Lifecycle for Command and Library

Command Mode

Command mode has a generated C++ main() entry. The entry calls in sequence:

typephp_<project>_runtime_init(argc, argv)
    → php_embed_init()
    → PHP/SAPI module startup and MINIT
    → PHP request startup and RINIT
    → register and start the current TypePHP application module
    → MINIT and RINIT of the current application

execute TypePHP main()

typephp_<project>_runtime_shutdown()
    → RSHUTDOWN and module cleanup of the current application
    → php_embed_shutdown()
    → PHP request/module/SAPI shutdown

The caller does not need to be aware of these steps, because the generated native main() automatically wraps the entire program lifecycle.

Library Mode Must Create the Runtime Resource First

A library component has no main() to execute automatically; merely instantiating the .wasm only completes the instantiation of the Component and the C/C++ Runtime, and does not mean the ZendVM request is already available. The Host must first call the generated WIT function:

create-runtime: func() -> result<runtime, typephp-error>;

The corresponding call in the browser is:

const component = await instantiate(null, wasi.getImportObject());
const runtime = await component.api.createRuntime();

try {
    const result = await runtime.someExportedFunction();
} finally {
    runtime[Symbol.dispose]();
}

Internally, createRuntime() calls the project-level initialization symbol through TYPEPHP_RUNTIME_INIT(<project>)(1, argv). The Host only needs to call this one stable interface, and should not directly call php_embed_init(), MINIT, RINIT, or any Zend C API.

The current initialization order is as follows:

  1. php_embed_init() initializes the Embed SAPI, the PHP core, and static extensions, and starts a PHP request; the PHP core and the already-registered static extensions complete MINIT/RINIT here.
  2. Set up PHPX's exception bridge so that PHP exceptions can safely return into the generated WIT result.
  3. Obtain the current TypePHP application's zend_module_entry, call zend_register_module_ex() and zend_startup_module_ex() to complete application module registration and MINIT.
  4. Register standard streams and set SAPI request information such as the request path.
  5. Because the Embed request and request memory pool are already started at this point, the generated code explicitly calls the current application module's request_startup_func to perform that module's RINIT; RINIT then initializes TypePHP request-level globals and class static data, and only returns the runtime resource after completion.

What is "manually" called here is the Host-visible create-runtime(), not letting the user manually assemble the ZendVM lifecycle. The specific calls of MINIT/RINIT and their order are all encapsulated in PHPX and the generated Component adapter.

Exported Calls Share the Same Request

All #[WasmExport] calls on the same runtime resource share the Zend request established by a single RINIT:

  • RINIT/RSHUTDOWN are not executed repeatedly before and after each function call.
  • The PHP request memory pool, request-level globals, and static state persist until the resource is released.
  • Currently only NTS is supported; calls on the same runtime must be serial, and the generated adapter rejects concurrent or re-entrant calls.
  • Ordinary PHP exceptions are converted into WIT result errors, and the runtime can still continue to be used.
  • A Zend bailout indicates that request state may already be corrupted, so the adapter marks the runtime as failed, and subsequent calls are rejected until the resource is released.

RSHUTDOWN Is Only Executed When the Resource Is Released

Releasing the WIT runtime resource calls the project-level shutdown symbol through TYPEPHP_RUNTIME_SHUTDOWN(<project>)():

  1. Call the current TypePHP application module's RSHUTDOWN to clean up TypePHP request-level objects and global data.
  2. Unregister and shut down the current application module, performing the corresponding module cleanup.
  3. Call php_embed_shutdown() to complete request shutdown, module shutdown, and SAPI shutdown of the remaining extensions.
  4. Finally release the request memory pool to avoid PHP/CPP wrapper objects continuing to destruct after the memory pool disappears.

Do not rely only on JavaScript GC to trigger the resource finalizer. Browser and Node Hosts should explicitly call runtime[Symbol.dispose]() in a finally block; Wasmtime or other Host bindings should also explicitly drop the resource. Directly terminating a Worker or process reclaims the entire Wasm instance, but does not guarantee that PHP's RSHUTDOWN/MSHUTDOWN callbacks are executed, so data that must be persisted should not be placed only in shutdown callbacks.

A Component instance currently allows only one active runtime resource at a time. After release it can be recreated; when initialization fails or a Zend bailout occurs, the old resource should be released first, rather than continuing to call exported functions.

High-precision Types

WASI artifacts include TypePHP's three language-level high-precision types:

  • BigInt: GMP 6.3.0
  • BigFloat: MPFR 4.2.2
  • Decimal: mpdecimal 4.0.1

A complete example is at high-precision.php. Build and run:

php bin/tpc.php --wasm examples/high-precision.php
wasmtime -S http high-precision.wasm

Expected output:

1111111101111111110111111111010
1000000000000000000000000000001
12348.14159265358979324

wasm32 uses 32-bit pointers, but PHP's zend_long remains 64-bit to maintain TypePHP's integer semantics with 64-bit PHP. GMP and mpdecimal use 32-bit limbs; this does not change arbitrary-precision semantics, but large-number throughput is lower than a native 64-bit build with assembly optimization.

Current Platform Boundaries

  • Only NTS, single-threaded is supported.
  • Fiber and Generator are disabled; the compiler reports a fatal error directly when it finds yield.
  • The PHPX Facade API is entirely disabled under __wasi__. PHPX core types and phpx_std are still usable.
  • Dynamic extensions, network sockets, processes, shells, and signals are not supported. Statically recognizable calls report a fatal error at compile time.
  • The PHP stream framework, local file capability, and time and random number capabilities provided by the WASI host are retained.
  • A command component can be run directly by Wasmtime; a library component requires the Host to call create-runtime() and exported functions according to the WIT interface. Chrome uses the ESM generated by Jco and the Worker host in examples/wasm-hello/typephp-worker.mjs.

The PHPX Facade is only a convenience wrapper generated for PHP optional extensions, not part of the TypePHP ABI. Disabling it entirely under WASI avoids exposing unavailable APIs such as curl, socket, and Swoole as interfaces that "compile but fail to link"; the PHP/WASI statically built-in extensions themselves are not affected by the Facade switch.

WASI SDK Directory

The integrated SDK uses a single, complete prefix located at wasm/wasm32-wasip2/ in the PHPX root directory:

phpx/wasm/wasm32-wasip2/
├── include/php/             # PHP installed headers
├── include/phpx/            # PHPX and TypePHP runtime headers
├── 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

Ordinary users obtain this directory through the TypePHP/PHPX integrated installation package. TypePHP developers need to clone the php-8.5.9-wasm and PHPX source bound to the current version, and assemble the full SDK through wasm/build-sdk.sh. PHP/WASI is only responsible for PHP; PHPX is responsible for GMP, MPFR, its dedicated mpdecimal, and the PHPX runtime. All artifacts are installed into the same PHPX checkout. If PHPX is not at vendor/swoole/phpx, continue using the existing PHPX_HOME to point to that checkout.

No path to separately override libphp.a, libphpx.a, or the numeric libraries is provided; all libraries, headers, and .typephp-wasi-sdk-abi must come from the same compatible build, avoiding mixing different zend_long, C++ exceptions, SJLJ, or Component Model ABIs.