ext-php-rs: writing PHP extensions in Rust through the Zend API
Bindings for the Zend API to build PHP extensions natively in Rust.
At a glance
- What is it?
- ext-php-rs wraps the Zend extension API in Rust types and procedural macros so a PHP extension can be written in Rust instead of C. The ergonomics are good; the build system, the MSVC linker caveat on Windows and the stub generation gap are where the real cost sits.
- Who is it for?
- Adopt ext-php-rs if you want a PHP extension written in Rust and your deployment targets PHP 8.1 or later on Linux or macOS with Clang 5.0 or later available. Do not adopt it for a Windows build that needs IDE stubs, because the README states that cargo-php stub generation does not work on Windows, or for PHP versions below 8.1, which the project says have no support planned.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The php_function and php_module macros replace hand-written glue
The project describes itself as bindings and abstractions for the Zend API to build PHP extensions natively in Rust, and the mechanism that matters is a pair of procedural macros. The example in the README exports a single PHP function and registers it in a module. The function body is ordinary Rust, taking a String and returning a String. The doc comment carries the PHP type information, with @param and @return tags, and the parameter types come from the Rust signature rather than from a hand-written conversion. That is the core value proposition: the README says the built-in macros abstract away the need to interact with the Zend API, with Rust-type function parameters abstracting away interaction with Zend values. A second macro marks the module entry point and registers the function with a ModuleBuilder. The crate pulls in a separate proc-macro crate, ext-php-rs-derive, pinned to an exact version so the macro crate and the runtime crate cannot drift apart.
The escape hatch is writing your own glue code
Two design claims in the feature list are worth separating, because they pull in opposite directions. The library calls itself lightweight on the grounds that you do not have to use the built-in helper macros and it is possible to write your own glue code around your own functions. And it calls itself extensible because you can implement IntoZval and FromZval for your own custom types, which then work as function parameters and return types. This matters for anything with a domain type in its signature, a money value or a validated identifier, where writing a conversion for a whole struct is more work than a string. The stated motivation is that abstracting the raw Zend APIs allows extensions to be developed faster and lets the library support future, and potentially past, PHP versions without significant changes to extension code. The limits of that abstraction are visible in the repository itself: the build dependency list includes bindgen, which means the Zend structures are generated from the PHP headers at build time, and there are separate unix_build.rs and windows_build.rs files, so platform differences are handled outside the macros.
cargo-php handles stubs, installation and the php -m check
The README leans on a separate tool, cargo-php, for the build-and-install loop, and it shows the whole cycle. You install cargo-php first, then generate IDE stubs to standard output, then install the extension in release mode, then confirm the module is loaded with php -m, where the extension name appears in the PHP Modules list. The stub output is a PHP file containing the function signature, which is what gives an editor completion and static analysis something to read. The commands are:
$ cargo install cargo-php --locked$ cargo php stubs --stdout
$ cargo php install --release
$ php -mOnce the extension is registered, calling it from PHP is unremarkable: a call to hello_world with a name argument returns the greeting string, and the README shows the var_dump output as string(13) "Hello, David!".
The stub step is the part with a real limitation, and the README is explicit about it: cargo-php stub generation does not work on Windows, and rewriting that functionality to be cross-platform is on the roadmap. A Windows team therefore gets a working extension but no generated IDE stubs, unless they write the PHP-side declaration by hand.
Windows needs nightly Rust, rust-lld and a debug PHP SDK
The Windows requirements are the most demanding part of the project and the place to look first if you build there. Rust nightly is required, because the vectorcall calling convention used by some PHP functions on Windows exists only as an unstable feature. The crate source shows this in the example, which carries a conditional attribute for the windows target enabling the abi_vectorcall feature. The README suggests the rust-lld linker, and explains why: the MSVC linker is supported, but you may hit issues if its version does not match your PHP installation. The configuration is a target section in a config file:
# Replace target triple if you have a different architecture than x86_64
[target.x86_64-pc-windows-msvc]
linker = "rust-lld"Two more constraints follow. The cc crate requires cl.exe, which usually ships with Visual Studio, and building in DEBUG mode on Windows requires a PHP SDK built with the DEBUG option and a PHP_LIB pointing at the lib files, with the README giving an example path. Extensions can also only be compiled for PHP installations sourced from windows.php.net, with support for other installations planned. On Windows you are therefore building against a specific, officially sourced PHP build, with a nightly toolchain.
Clang, PHP 8.1 and the musl story on Alpine
The baseline requirements are modest and explicit: Linux, macOS or Windows, PHP 8.1 or later, Rust, and Clang 5.0 or later. The Clang requirement is not incidental, because bindgen needs libclang to parse the PHP headers, and the build dependency list carries bindgen with default features off plus the logging feature. On minimum supported PHP, the README states plainly that no support is planned for earlier versions, and that PHP versions no longer receiving security updates will no longer be supported, though they might still work with no guarantees. There is no MSRV guarantee either, with lib.rs suggesting Rust 1.57 at the time of writing. Alpine Linux with musl is supported through the runtime feature flag, which the README says is on by default, and the dependency install is short:
# Install dependencies and cargo-php
apk add clang16-libclang
cargo install cargo-phpFor a musl build you then need a rustflags override in your project config:
[target.'cfg(target_env = "musl")']
rustflags = ["-C", "target-feature=-crt-static"]If you prefer to link libclang statically, you swap runtime for _static and install clang16-static instead of clang16-libclang. The two are mutually exclusive, so default-features = false is required.
Cargo feature flags are off by default, with one contradiction
The Cargo Features section says all features are disabled by default, and then lists closure, which returns Rust closures to PHP by creating a RustClosure class type, anyhow, which implements Into<PhpException> for anyhow::Error so anyhow results can be returned from PHP functions, and supports anyhow v1.x, plus static, embed, smartstring, indexmap, enum and observer. The actual manifest says something different. In Cargo.toml the default feature list is enum and runtime, and the comment in the file explains that clang-sys refuses to build the two together, making them the one non-additive pair in the manifest. So the README text and the manifest disagree about what you get out of the box, and the manifest is what cargo will act on. If you depend on the exact default set, read the features table in Cargo.toml rather than the README paragraph. The observer feature appears elsewhere too, in the Dockerfile, where the docsrs_bindings_builder stage runs cargo build --features observer to produce the bindings.rs file that is then copied and formatted.
MIT or Apache-2.0, with releases tracked under two crate names
The licence is dual, MIT OR Apache-2.0, with LICENSE_APACHE and LICENSE_MIT both present at the repository root and the same string in the manifest. Apache-2.0 is the convenient choice for organisations that require a patent grant, and MIT is the simpler one to fold into an existing policy. This is not legal advice, but the practical point is that you can pick either without asking the maintainers. The project is not archived and the last push was on 2026-10-20. The release history shows two crates moving in lockstep, ext-php-rs and ext-php-rs-derive, with ext-php-rs-v0.16.3 and ext-php-rs-derive-v0.12.3 published on the same day, and ext-php-rs-v0.16.2 six days earlier. Because the derive crate is pinned with an exact version requirement in the manifest, upgrading the two together is the expected path, and the repository includes a CHANGELOG.md and a.release-plz.toml, which is the tool that drives those releases. The guide lives in the guide/ directory and the API reference is published on docs.rs, with the README noting that the project is documented in-line so the cargo documentation is currently the best resource.
Editorial conclusion
Adopt ext-php-rs if you want a PHP extension written in Rust and your deployment targets PHP 8.1 or later on Linux or macOS with Clang 5.0 or later available. Do not adopt it for a Windows build that needs IDE stubs, because the README states that cargo-php stub generation does not work on Windows, or for PHP versions below 8.1, which the project says have no support planned. Before you commit, install cargo-php, run cargo php stubs --stdout against your own function to confirm the generated signature matches what you expect, and decide explicitly whether to use the built-in macros or to write your own glue code around the raw Zend calls.
Frequently asked questions
What is ext-php-rs?
It is a set of bindings and abstractions for the Zend API that let you build PHP extensions natively in Rust. The project is written in Rust, published on crates.io as ext-php-rs, and licensed MIT OR Apache-2.0.
Which PHP versions does ext-php-rs support?
PHP 8.1 or later is required, and the README states that no support is planned for earlier versions. PHP versions that no longer receive security updates will no longer be supported, though they might still work with no guarantees.
How do I install a PHP extension built with ext-php-rs?
The README uses cargo-php for the loop. You run cargo php stubs --stdout to generate IDE stubs, then cargo php install --release to build and install the extension, then check that the extension appears in the output of php -m.
Does cargo-php work on Windows?
The stub generation feature of cargo-php does not work on Windows, and the README says rewriting it to be cross-platform is on the roadmap. Windows builds also require Rust nightly, because the vectorcall calling convention is only available as an unstable feature.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/extphprs-ext-php-rs)Was this useful?