Skip to content

[PHP] Add PHP.wasm extension compile helper - #3567

Closed
adamziel wants to merge 1 commit into
codex/php-extension-blueprint-loadingfrom
codex/compile-extension-helper-stack
Closed

[PHP] Add PHP.wasm extension compile helper#3567
adamziel wants to merge 1 commit into
codex/php-extension-blueprint-loadingfrom
codex/compile-extension-helper-stack

Conversation

@adamziel

@adamziel adamziel commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator

What it does

Adds @php-wasm/compile-extension, stacked on top of #3568 and #3566. The CLI builds a PHP extension source directory into PHP 8.4 .so artifacts plus manifest.json.

The package includes:

  • a Docker wrapper around the existing PHP.wasm compile image
  • the canonical in-container phpize / emconfigure / emmake recipe
  • manifest writing with sha256 entries
  • docs for plain extensions, CMake/Makefile projects, Playground-provided dependencies, and vendored dependencies
  • CI coverage that builds and loads real fixtures through loadPHPExtension()

Rationale

The runtime API in #3566 and Blueprint step in #3568 do not require a build helper. Keeping this separate makes those reviews smaller while still proving that externally built artifacts can target the same manifest contract.

Extension authors still need a supported way to produce compatible side modules without re-deriving Emscripten flags, PHP headers, and async-mode settings.

Implementation

The CLI keeps the public surface flag-only for v1:

npx @php-wasm/compile-extension \
  --source ./ext-src \
  --name wp_mysql_parser \
  --php-versions 8.4 \
  --async-modes jspi,asyncify \
  --out ./dist

The Docker layer extends the existing compile image and adds the phpize toolchain. scripts/build-in-docker.sh is the source of truth for the actual build recipe.

The integration job runs only when packages/php-wasm/compile-extension/** changes. It builds and loads a hello extension, php-src ext/calendar, redis, a zlib-backed extension, and an extension backed by a vendored non-Playground library.

Testing instructions

npx nx test php-wasm-compile-extension
npx nx typecheck php-wasm-compile-extension
npx nx lint php-wasm-compile-extension
npx nx build php-wasm-compile-extension

CI runs the Docker-backed integration coverage via test-compile-extension-helper.

@adamziel
adamziel force-pushed the codex/unified-php-extension-loading branch from 8dc2bd7 to 0a01f5d Compare April 29, 2026 17:28
@adamziel
adamziel force-pushed the codex/compile-extension-helper-stack branch from 2e5916a to e8d7a6c Compare April 29, 2026 17:32
@adamziel
adamziel changed the base branch from codex/unified-php-extension-loading to codex/php-extension-blueprint-loading April 29, 2026 17:33
@adamziel
adamziel force-pushed the codex/php-extension-blueprint-loading branch from 38515ca to cdbef2d Compare April 29, 2026 17:59
@adamziel

adamziel commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #3582, which is retargeted to trunk and updates the custom-extension workflow for the current startup-time .so loading API.

@adamziel adamziel closed this May 1, 2026
adamziel added a commit that referenced this pull request May 2, 2026
…extensions (#3582)

## What it does

Adds `@php-wasm/compile-extension`, a Docker-backed helper for compiling
`phpize` extension source directories into PHP.wasm JSPI side modules.

Developers can now build a custom extension, publish the generated `.so`
files
and `manifest.json`, and load them at startup through the `extensions`
API.

For example, given a `wp_mysql_parser` extension with a normal
`config.m4`:

```bash
npx @php-wasm/compile-extension \
	--source ./wp-mysql-parser \
	--name wp_mysql_parser \
	--php-versions 8.4 \
	--out ./dist/wp_mysql_parser
```

The output is directly loadable by PHP.wasm:

```text
dist/wp_mysql_parser/
|-- manifest.json
`-- wp_mysql_parser-php8.4-jspi.so
```

```ts
import { PHP } from '@php-wasm/universal';
import { loadNodeRuntime } from '@php-wasm/node';

const php = new PHP(
	await loadNodeRuntime('8.4', {
		extensions: [
			{
				source: {
					format: 'manifest',
					manifestUrl: './dist/wp_mysql_parser/manifest.json',
				},
			},
		],
	})
);
```

Rust extensions are supported through the same workflow when the Rust
crate is
wrapped as a `phpize` extension:

```bash
RUSTFLAGS="-C panic=abort" cargo +nightly build \
	--release \
	--target wasm32-unknown-emscripten \
	-Zbuild-std=std,panic_abort

npx @php-wasm/compile-extension \
	--source ./my-rust-extension \
	--name my_rust_extension \
	--php-versions 8.4 \
	--extra-ldflags "/build/target/wasm32-unknown-emscripten/release/libmy_rust_extension.a"
```

Rust-specific caveats:

- The final extension still needs a tiny `config.m4` + C shim that
exposes the
  PHP module entry and calls Rust over C ABI.
- Custom extension builds are **JSPI-only**. Link JSPI-compatible
dependency
  archives and load the result in a JSPI runtime.
- Pass Rust `staticlib` archives through `--extra-ldflags`, not
  `PHP_ADD_LIBRARY_WITH_PATH`.
- `bindgen` needs the PHP.wasm target/sysroot/Zend defines; the helper
exports
  `BINDGEN_EXTRA_CLANG_ARGS` for builds run inside the container.
- `cc-rs` needs `-fPIC` for side modules; the helper exports
target-specific
`CFLAGS_wasm32_unknown_emscripten` and
`CXXFLAGS_wasm32_unknown_emscripten`.
- Rust `std` must be rebuilt with `panic=abort`; the precompiled
`wasm32-unknown-emscripten` `std` imports unwinding support PHP.wasm
does not
  provide.

## Rationale

Custom extensions should be a real developer workflow, not just a
runtime API.
This PR gives developers a documented path from extension source code to
loadable PHP.wasm artifacts, including manifest metadata and `sha256`
hashes.

This supersedes #3567 and retargets the work to `trunk`. The docs and
examples
match the recent `.so` loading changes: startup-time loading via
`extensions`,
PHP-version manifest selection, direct URLs, and generated `.ini` files
instead
of post-startup `dl()` or older Blueprint-oriented APIs.

## Implementation

- Adds the `@php-wasm/compile-extension` CLI, Docker build flow,
manifest
  generation, and artifact selection helpers.
- Builds custom extensions for JSPI runtimes only. The CLI no longer
exposes an
  `--async-modes` matrix, and generated manifests no longer include
  `asyncMode`.
- Documents static archive linking, Rust `staticlib` wrappers, vendored
libraries, CMake/Make dependencies, and common WebAssembly linking
failures.
- Adds unit tests, Docker integration fixtures, and CI coverage for the
helper.

## Testing instructions

```bash
npm exec nx test php-wasm-compile-extension
npm exec nx run php-wasm-compile-extension:typecheck
npm exec nx run php-wasm-compile-extension:lint
npm exec nx run php-wasm-compile-extension:build
npm exec nx run docs-site:check-orphan-pages
bash -n packages/php-wasm/compile-extension/scripts/build-in-docker.sh packages/php-wasm/compile-extension/tests/run-integration-tests.sh
git diff --check
PATH="$HOME/.nvm/versions/node/v24.14.0/bin:$PATH" npm exec nx run php-wasm-compile-extension:test-compile-extension-integration
```

The Docker integration needs a Node build with JSPI support; Node 22
reports
JSPI unavailable locally, while Node 24 matches the PR CI job.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment