Introduction
ext-php-rs is a Rust library containing bindings and abstractions for the PHP
extension API, which allows users to build extensions natively in Rust.
Features
- Easy to use: The built-in macros can abstract away the need to interact with the Zend API, such as Rust-type function parameter abstracting away interacting with Zend values.
- Lightweight: You don’t have to use the built-in helper macros. It’s possible to write your own glue code around your own functions.
- Extensible: Implement
IntoZvalandFromZvalfor your own custom types, allowing the type to be used as function parameters and return types.
Goals
Our main goal is to make extension development easier.
- Writing extensions in C can be tedious, and with the Zend APIs limited documentation can be intimidating.
- Rust’s modern language features and feature-full standard library are big improvements on C.
- Abstracting away the raw Zend APIs allows extensions to be developed faster and with more confidence.
- Abstractions also allow us to support future (and potentially past) versions of PHP without significant changes to extension code.
Versioning
ext-php-rs follows semantic versioning, however, no backwards compatibility is
guaranteed while we are at major version 0, which is for the foreseeable
future. It’s recommended to lock the version at the patch level.
When introducing breaking changes a migration guide will be provided in this guide.
Documentation
- This guide!
- Rust docs
Installation
To get started using ext-php-rs you will need both a Rust toolchain
and a PHP development environment. We’ll cover each of these below.
Rust toolchain
First, make sure you have rust installed on your system.
If you haven’t already done so you can do so by following the instructions here.
ext-php-rs runs on both the stable and nightly versions so you can choose whichever one fits you best.
PHP development environment
In order to develop PHP extensions, you’ll need the following installed on your system:
- The PHP CLI executable itself
- The PHP development headers
- The
php-configbinary
While the easiest way to get started is to use the packages provided by your distribution, we recommend building PHP from source.
NB: To use ext-php-rs you’ll need at least PHP 8.0.
Using a package manager
# Debian and derivatives
apt install php-dev
# Arch Linux
pacman -S php
# Fedora
dnf install php-devel
# Homebrew
brew install php
Compiling PHP from source
Please refer to this PHP internals book chapter for an in-depth guide on how to build PHP from source.
TL;DR; use the following commands to build a minimal development version with debug symbols enabled.
# clone the php-src repository
git clone https://github.com/php/php-src.git
cd php-src
# by default you will be on the master branch, which is the current
# development version. You can check out a stable branch instead:
git checkout PHP-8.1
./buildconf
PREFIX="${HOME}/build/php"
./configure --prefix="${PREFIX}" \
--enable-debug \
--disable-all --disable-cgi
make -j "$(nproc)"
make install
The PHP CLI binary should now be located at ${PREFIX}/bin/php
and the php-config binary at ${PREFIX}/bin/php-config.
Next steps
Now that we have our development environment in place, let’s go build an extension !
Hello World
Project Setup
We will start by creating a new Rust library crate:
$ cargo new hello_world --lib
$ cd hello_world
Cargo.toml
Let’s set up our crate by adding ext-php-rs as a dependency and setting the
crate type to cdylib. Update the Cargo.toml to look something like so:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
ext-php-rs = "*"
[profile.release]
strip = "debuginfo"
.cargo/config.toml
When compiling for Linux and macOS, we do not link directly to PHP, rather PHP will dynamically load the library. We need to tell the linker it’s ok to have undefined symbols (as they will be resolved when loaded by PHP).
On Windows, we also need to switch to using the rust-lld linker.
Microsoft Visual C++’s
link.exeis supported, however you may run into issues if your linker is not compatible with the linker used to compile PHP.
We do this by creating a Cargo config file in .cargo/config.toml with the
following contents:
[target.'cfg(not(target_os = "windows"))']
rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"]
[target.x86_64-pc-windows-msvc]
linker = "rust-lld"
[target.i686-pc-windows-msvc]
linker = "rust-lld"
[target.'cfg(target_env = "musl")']
rustflags = ["-C", "target-feature=-crt-static"]
Writing our extension
src/lib.rs
Let’s actually write the extension code now. We start by importing the
ext-php-rs prelude, which contains most of the imports required to make a
basic extension. We will then write our basic hello_world function, which will
take a string argument for the callers name, and we will return another string.
Finally, we write a get_module function which is used by PHP to find out about
your module. We must provide the defined function to the given ModuleBuilder
and then return the same object.
We also need to enable the abi_vectorcall feature when compiling for Windows
(the first line). This is a nightly-only feature so it is recommended to use
the #[cfg_attr] macro to not enable the feature on other operating systems.
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_function]
pub fn hello_world(name: &str) -> String {
format!("Hello, {}!", name)
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(hello_world))
}
fn main() {}
Building the extension
Now let’s build our extension.
This is done through cargo like any other Rust crate.
If you installed php using a package manager in the previous chapter
(or if the php and php-config binaries are already in your $PATH),
then you can just run
cargo build
If you have multiple PHP versions in your PATH, or your installation resides in a custom location, you can use the following environment variables:
# explicitly specifies the path to the PHP executable:
export PHP=/path/to/php
# explicitly specifies the path to the php-config executable:
export PHP_CONFIG=/path/to/php-config
As an alternative, if you compiled PHP from source and installed it under
it’s own prefix (configure --prefix=/my/prefix), you can just put
this prefix in front of your PATH:
export PATH="/my/prefix:${PATH}"
Once you’ve setup these variables, you can just run
cargo build
Cargo will track changes to these environment variables and rebuild the library accordingly.
Testing our extension
The extension we just built is stored inside the cargo target directory:
target/debug if you did a debug build, target/release for release builds.
The extension file name is OS-dependent. The naming works as follows:
- let
Sbe the empty string - append to
Sthe value of std::env::consts::DLL_PREFIX (empty on windows,libon unixes) - append to
Sthe lower-snake-case version of your crate name - append to
Sthe value of std::env::consts::DLL_SUFFIX (.dllon windows,.dylibon macOS,.soon other unixes). - set the filename to the value of
S
Which in our case would give us:
- linux:
libhello_world.so - macOS:
libhello_world.dylib - windows:
hello_world.dll
Now we need a way to tell the PHP CLI binary to load our extension.
There are several ways to do that.
For now we’ll simply pass the -d extension=/path/to/extension option to the PHP CLI binary.
Let’s make a test script:
test.php
<?php
var_dump(hello_world("David"));
And run it:
$ php -d extension=./target/debug/libhello_world.so test.php
string(13) "Hello, David!"
cargo php
ext-php-rs comes with a cargo subcommand called cargo-php. When called in
the manifest directory of an extension, it allows you to do the following:
- Generate IDE stub files
- Install the extension
- Remove the extension
System Requirements
The subcommand has been tested on the following systems and architectures. Note these are not requirements, but simply platforms that the application have been tested on. YMMV.
- macOS 12.0 (AArch64, x86_64 builds but untested)
- Linux 5.15.1 (AArch64, x86_64 builds but untested)
Windows is not currently supported by ext-php-rs.
macOS Note
When installing your extension multiple times without uninstalling on macOS, you
may run into PHP exiting with SIGKILL. You can see the exact cause of the exit
in Console, however, generally this is due to a invalid code signature.
Uninstalling the extension and then reinstalling generally fixes this problem.
Installation
The subcommand is installed through composer like any other Rust CLI application:
$ cargo install cargo-php --locked
You can then call the application via cargo php (assuming the cargo
installation directory is in your PATH):
$ cargo php --help
cargo-php 0.1.0
David Cole <david.cole1340@gmail.com>
Installs extensions and generates stub files for PHP extensions generated with `ext-php-rs`.
USAGE:
cargo-php <SUBCOMMAND>
OPTIONS:
-h, --help
Print help information
-V, --version
Print version information
SUBCOMMANDS:
help
Print this message or the help of the given subcommand(s)
install
Installs the extension in the current PHP installation
remove
Removes the extension in the current PHP installation
stubs
Generates stub PHP files for the extension
The command should always be executed from within your extensions manifest
directory (the directory with your Cargo.toml).
Stubs
Stub files are used by your IDEs language server to know the signature of methods, classes and constants in your PHP extension, similar to how a C header file works.
One of the largest collection of PHP standard library and non-standard extension stub files is provided by JetBrains: phpstorm-stubs. This collection is used by JetBrains PhpStorm and the PHP Intelephense language server (which I personally recommend for use in Visual Studio Code).
Usage
$ cargo php stubs --help
cargo-php-stubs
Generates stub PHP files for the extension.
These stub files can be used in IDEs to provide typehinting for extension classes, functions and
constants.
USAGE:
cargo-php stubs [OPTIONS] [EXT]
ARGS:
<EXT>
Path to extension to generate stubs for. Defaults for searching the directory the
executable is located in
OPTIONS:
-h, --help
Print help information
--manifest <MANIFEST>
Path to the Cargo manifest of the extension. Defaults to the manifest in the directory
the command is called.
This cannot be provided alongside the `ext` option, as that option provides a direct
path to the extension shared library.
-o, --out <OUT>
Path used to store generated stub file. Defaults to writing to `<ext-name>.stubs.php` in
the current directory
--stdout
Print stubs to stdout rather than write to file. Cannot be used with `out`
Custom Allocators
If your extension uses a custom global allocator (e.g., mimalloc, jemalloc),
you may encounter a crash when running cargo php stubs with an error like
“pointer being freed was not allocated”.
This happens because cargo php stubs builds your extension in debug mode and
loads it to extract type information. Custom allocators can conflict with this
process.
Workaround: Conditionally enable your custom allocator only in release builds:
#[cfg(not(debug_assertions))]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
See issue #523 for more details.
Extension Installation
When PHP is in your PATH, the application can automatically build and copy your
extension into PHP. This requires php-config to be installed alongside PHP.
It is recommended to backup your php.ini before installing the extension
so you are able to restore if you run into any issues.
Usage
$ cargo php install --help
cargo-php-install
Installs the extension in the current PHP installation.
This copies the extension to the PHP installation and adds the extension to a PHP configuration
file.
Note that this uses the `php-config` executable installed alongside PHP to locate your `php.ini`
file and extension directory. If you want to use a different `php-config`, the application will read
the `PHP_CONFIG` variable (if it is set), and will use this as the path to the executable instead.
USAGE:
cargo-php install [OPTIONS]
OPTIONS:
--disable
Installs the extension but doesn't enable the extension in the `php.ini` file
-h, --help
Print help information
--ini-path <INI_PATH>
Path to the `php.ini` file to update with the new extension
--install-dir <INSTALL_DIR>
Changes the path that the extension is copied to. This will not activate the extension
unless `ini_path` is also passed
--manifest <MANIFEST>
Path to the Cargo manifest of the extension. Defaults to the manifest in the directory
the command is called
--release
Whether to install the release version of the extension
--yes
Bypasses the confirmation prompt
Extension Removal
Removes the extension from your PHPs extension directory, and removes the entry
from your php.ini if present.
Usage
$ cargo php remove --help
cargo-php-remove
Removes the extension in the current PHP installation.
This deletes the extension from the PHP installation and also removes it from the main PHP
configuration file.
Note that this uses the `php-config` executable installed alongside PHP to locate your `php.ini`
file and extension directory. If you want to use a different `php-config`, the application will read
the `PHP_CONFIG` variable (if it is set), and will use this as the path to the executable instead.
USAGE:
cargo-php remove [OPTIONS]
OPTIONS:
-h, --help
Print help information
--ini-path <INI_PATH>
Path to the `php.ini` file to remove the extension from
--install-dir <INSTALL_DIR>
Changes the path that the extension will be removed from. This will not remove the
extension from a configuration file unless `ini_path` is also passed
--manifest <MANIFEST>
Path to the Cargo manifest of the extension. Defaults to the manifest in the directory
the command is called
--yes
Bypasses the confirmation prompt
Types
In PHP, data is stored in containers called zvals (zend values). Internally,
these are effectively tagged unions (enums in Rust) without the safety that Rust
introduces. Passing data between Rust and PHP requires the data to become a
zval. This is done through two traits: FromZval and IntoZval. These traits
have been implemented on most regular Rust types:
- Primitive integers (
i8,i16,i32,i64,u8,u16,u32,u64,usize,isize). - Double and single-precision floating point numbers (
f32,f64). - Booleans.
- Strings (
Stringand&str) Vec<T>where T implementsIntoZvaland/orFromZval.HashMap<String, T>where T implementsIntoZvaland/orFromZval.Binary<T>where T implementsPack, used for transferring binary string data.BinarySlice<T>where T implementsPack, used for exposing PHP binary strings as read-only slices.- A PHP callable closure or function wrapped with
Callable. Option<T>where T implementsIntoZvaland/orFromZval, and whereNoneis converted to a PHPnull.
Return types can also include:
- Any class type which implements
RegisteredClass(i.e. any struct you have registered with PHP). - An immutable reference to
selfwhen used in a method, through theClassReftype. - A Rust closure wrapped with
Closure. Result<T, E>, whereT: IntoZvalandE: Into<PhpException>. When the error variant is encountered, it is converted into aPhpExceptionand thrown as an exception.
For a type to be returnable, it must implement IntoZval, while for it to be
valid as a parameter, it must implement FromZval.
Primitive Numbers
Primitive integers include i8, i16, i32, i64, u8, u16, u32, u64,
isize, usize, f32 and f64.
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| Yes | No | Yes | No | i32 on 32-bit platforms, i64 on 64-bit platforms, f64 platform-independent |
Note that internally, PHP treats all of these integers the same (a ‘long’), and therefore it must be converted into a long to be stored inside the zval. A long is always signed, and the size will be 32-bits on 32-bit platforms and 64-bits on 64-bit platforms.
Floating point numbers are always stored in a double type (f64), regardless
of platform. Note that converting a zval into a f32 will lose accuracy.
This means that converting i64, u32, u64, isize and usize can fail
depending on the value and the platform, which is why all zval conversions are
fallible.
Rust example
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_function]
pub fn test_numbers(a: i32, b: u32, c: f32) -> u8 {
println!("a {} b {} c {}", a, b, c);
0
}
fn main() {}
PHP example
<?php
test_numbers(5, 10, 12.5); // a 5 b 10 c 12.5
String
When a String type is encountered, the zend string content is copied to/from a
Rust String object. If the zval does not contain a string, it will attempt to
read a double from the zval and convert it into a String object.
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| Yes | No | Yes | No | zend_string (C-string) |
Internally, PHP stores strings in zend_string objects, which is a refcounted C
struct containing the string length with the content of the string appended to
the end of the struct based on how long the string is. Since the string is
NUL-terminated, you cannot have any NUL bytes in your string, and an error will
be thrown if one is encountered while converting a String to a zval.
Rust example
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_function]
pub fn str_example(input: String) -> String {
format!("Hello {}", input)
}
fn main() {}
PHP example
<?php
var_dump(str_example("World")); // string(11) "Hello World"
var_dump(str_example(5)); // string(7) "Hello 5"
&str
A borrowed string. When this type is encountered, you are given a reference to
the actual zend string memory, rather than copying the contents like if you were
taking an owned String argument.
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| No | Yes | No | Yes | zend_string (C-string) |
Note that you cannot expect the function to operate the same by swapping out
String and &str - since the zend string memory is read directly, this
library does not attempt to parse double types as strings.
See the String for a deeper dive into the internal structure of
PHP strings.
Rust example
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_function]
pub fn str_example(input: &str) -> String {
format!("Hello {}", input)
}
#[php_function]
pub fn str_return_example() -> &'static str {
"Hello from Rust"
}
fn main() {}
PHP example
<?php
var_dump(str_example("World")); // string(11) "Hello World"
var_dump(str_example(5)); // Invalid
var_dump(str_return_example());
bool
A boolean. Not much else to say here.
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| Yes | No | Yes | No | Union flag |
Booleans are not actually stored inside the zval. Instead, they are treated as two different union types (the zval can be in a true or false state). An equivalent structure in Rust would look like:
enum Zval {
True,
False,
String(&mut ZendString),
Long(i64),
// ...
}
Rust example
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_function]
pub fn test_bool(input: bool) -> String {
if input {
"Yes!".into()
} else {
"No!".into()
}
}
fn main() {}
PHP example
<?php
var_dump(test_bool(true)); // string(4) "Yes!"
var_dump(test_bool(false)); // string(3) "No!"
Rust example, taking by reference
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::types;
#[php_function]
pub fn test_bool(input: &mut types::Zval) {
input.reference_mut().unwrap().set_bool(false);
}
fn main() {}
Vec
Vectors can contain any type that can be represented as a zval. Note that the data contained in the array will be copied into Rust types and stored inside the vector. The internal representation of a PHP array is discussed below.
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| Yes | No | Yes | No | ZendHashTable |
Internally, PHP arrays are hash tables where the key can be an unsigned long or a string. Zvals are contained inside arrays therefore the data does not have to contain only one type.
When converting into a vector, all values are converted from zvals into the given generic type. If any of the conversions fail, the whole conversion will fail.
Rust example
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_function]
pub fn test_vec(vec: Vec<String>) -> String {
vec.join(" ")
}
fn main() {}
PHP example
<?php
var_dump(test_vec(['hello', 'world', 5])); // string(13) "hello world 5"
HashSet
HashSet is an unordered collection of unique values. When converting to a PHP array,
values are stored with sequential integer keys (0, 1, 2, …).
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| Yes | No | Yes | No | ZendHashTable |
Rust example
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use std::collections::HashSet;
#[php_function]
pub fn test_hashset(set: HashSet<String>) -> HashSet<String> {
// Duplicates are automatically removed
set
}
fn main() {}
PHP example
<?php
// Duplicates are removed, order is not preserved
$result = test_hashset(['a', 'b', 'a', 'c']);
var_dump($result); // array with 3 unique values
BTreeSet
BTreeSet is a sorted collection of unique values. Elements are ordered by their Ord
implementation. When converting to a PHP array, values are stored with sequential integer
keys (0, 1, 2, …) in sorted order.
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| Yes | No | Yes | No | ZendHashTable |
Rust example
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use std::collections::BTreeSet;
#[php_function]
pub fn test_btreeset(set: BTreeSet<String>) -> BTreeSet<String> {
// Duplicates removed, values sorted
set
}
fn main() {}
PHP example
<?php
// Values are sorted and deduplicated
$result = test_btreeset(['z', 'a', 'm', 'a']);
foreach ($result as $value) {
echo "$value\n";
}
// a
// m
// z
IndexSet
IndexSet is like HashSet but preserves insertion order. This requires the indexmap
feature.
[dependencies]
ext-php-rs = { version = "...", features = ["indexmap"] }
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| Yes | No | Yes | No | ZendHashTable |
Rust example
use ext_php_rs::prelude::*;
use indexmap::IndexSet;
#[php_function]
pub fn test_indexset(set: IndexSet<String>) -> IndexSet<String> {
// Order is preserved, duplicates removed
for v in set.iter() {
println!("v: {}", v);
}
set
}
PHP example
<?php
// Order is preserved, duplicates removed
$result = test_indexset(['z', 'a', 'm', 'a']);
foreach ($result as $value) {
echo "$value\n";
}
// z
// a
// m
HashMap
HashMaps are represented as associative arrays in PHP.
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| Yes | No | Yes | No | ZendHashTable |
Converting from a zval to a HashMap is valid when the key is a String, and
the value implements FromZval. The key and values are copied into Rust types
before being inserted into the HashMap. If one of the key-value pairs has a
numeric key, the key is represented as a string before being inserted.
Converting from a HashMap to a zval is valid when the key implements
AsRef<str>, and the value implements IntoZval.
When using `HashMap` the order of the elements is not preserved.
HashMaps are unordered collections, so the order of elements may not be the same
when converting from PHP to Rust and back.
If you need to preserve the order of elements, consider using `IndexMap` (requires
the `indexmap` feature) or `Vec<(K, V)>`.
Rust example
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use std::collections::HashMap;
#[php_function]
pub fn test_hashmap(hm: HashMap<String, String>) -> Vec<String> {
for (k, v) in hm.iter() {
println!("k: {} v: {}", k, v);
}
hm.into_iter()
.map(|(_, v)| v)
.collect::<Vec<_>>()
}
fn main() {}
PHP example
<?php
var_dump(test_hashmap([
'hello' => 'world',
'rust' => 'php',
'okk',
]));
Output:
k: rust v: php
k: hello v: world
k: 0 v: okk
array(3) {
[0] => string(3) "php",
[1] => string(5) "world",
[2] => string(3) "okk"
}
IndexMap (Order-Preserving)
IndexMap is like HashMap but preserves insertion order, making it ideal for
working with PHP arrays where key order matters. This requires the indexmap
feature.
[dependencies]
ext-php-rs = { version = "...", features = ["indexmap"] }
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| Yes | No | Yes | No | ZendHashTable |
Rust example
use ext_php_rs::prelude::*;
use indexmap::IndexMap;
#[php_function]
pub fn test_indexmap(map: IndexMap<String, String>) -> IndexMap<String, i32> {
// Order is preserved when iterating
for (k, v) in map.iter() {
println!("k: {} v: {}", k, v);
}
// Return an ordered map
let mut result = IndexMap::new();
result.insert("first".to_string(), 1);
result.insert("second".to_string(), 2);
result.insert("third".to_string(), 3);
result // Order preserved: first, second, third
}
PHP example
<?php
// Keys maintain their order
$result = test_indexmap([
'z' => 'last',
'a' => 'first',
'm' => 'middle',
]);
// Output preserves insertion order
foreach ($result as $key => $value) {
echo "$key => $value\n";
}
// first => 1
// second => 2
// third => 3
Vec<(K, V)> and Vec<ArrayKey, V>
Vec<(K, V)> and Vec<ArrayKey, V> are used to represent associative arrays in PHP
where the keys can be strings or integers.
If using String or &str as the key type, only string keys will be accepted.
For i64 keys, string keys that can be parsed as integers will be accepted, and
converted to i64.
If you need to accept both string and integer keys, use ArrayKey as the key type.
Rust example
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::types::ArrayKey;
#[php_function]
pub fn test_vec_kv(vec: Vec<(String, String)>) -> Vec<String> {
for (k, v) in vec.iter() {
println!("k: {} v: {}", k, v);
}
vec.into_iter()
.map(|(_, v)| v)
.collect::<Vec<_>>()
}
#[php_function]
pub fn test_vec_arraykey(vec: Vec<(ArrayKey, String)>) -> Vec<String> {
for (k, v) in vec.iter() {
println!("k: {} v: {}", k, v);
}
vec.into_iter()
.map(|(_, v)| v)
.collect::<Vec<_>>()
}
fn main() {}
PHP example
<?php
declare(strict_types=1);
var_dump(test_vec_kv([
['hello', 'world'],
['rust', 'php'],
['okk', 'okk'],
]));
var_dump(test_vec_arraykey([
['hello', 'world'],
[1, 'php'],
["2", 'okk'],
]));
Output:
k: hello v: world
k: rust v: php
k: okk v: okk
array(3) {
[0] => string(5) "world",
[1] => string(3) "php",
[2] => string(3) "okk"
}
k: hello v: world
k: 1 v: php
k: 2 v: okk
array(3) {
[0] => string(5) "world",
[1] => string(3) "php",
[2] => string(3) "okk"
}
ZendHashTable
ZendHashTable is the internal representation of PHP arrays. While you can use
Vec and HashMap for most use cases (which are converted to/from
ZendHashTable automatically), working directly with ZendHashTable gives you
more control and avoids copying data when you need to manipulate PHP arrays
in-place.
When to use ZendHashTable directly
- When you need to modify a PHP array in place without copying
- When working with arrays passed by reference
- When you need fine-grained control over array operations
- When implementing custom iterators or data structures
Basic Operations
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::types::ZendHashTable;
use ext_php_rs::boxed::ZBox;
#[php_function]
pub fn create_array() -> ZBox<ZendHashTable> {
let mut ht = ZendHashTable::new();
// Push values (auto-incrementing numeric keys)
ht.push("first").unwrap();
ht.push("second").unwrap();
// Insert with string keys
ht.insert("name", "John").unwrap();
ht.insert("age", 30i64).unwrap();
// Insert at specific numeric index
ht.insert_at_index(100, "at index 100").unwrap();
ht
}
#[php_function]
pub fn read_array(arr: &ZendHashTable) {
// Get by string key
if let Some(name) = arr.get("name") {
println!("Name: {:?}", name.str());
}
// Get by numeric index
if let Some(first) = arr.get_index(0) {
println!("First: {:?}", first.str());
}
// Check length
println!("Length: {}", arr.len());
println!("Is empty: {}", arr.is_empty());
// Iterate over key-value pairs
for (key, value) in arr.iter() {
println!("{}: {:?}", key, value);
}
}
fn main() {}
Entry API
The Entry API provides an ergonomic way to handle hash table operations where
you need to conditionally insert or update values based on whether a key already
exists. This is similar to Rust’s std::collections::hash_map::Entry API.
Basic Usage
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::types::ZendHashTable;
use ext_php_rs::boxed::ZBox;
#[php_function]
pub fn entry_example() -> ZBox<ZendHashTable> {
let mut ht = ZendHashTable::new();
// Insert a default value if the key doesn't exist
ht.entry("counter").or_insert(0i64).unwrap();
// Modify the value if it exists, using and_modify
ht.entry("counter")
.and_modify(|v| {
if let Some(n) = v.long() {
v.set_long(n + 1);
}
})
.or_insert(0i64)
.unwrap();
// Use or_insert_with for lazy initialization
ht.entry("computed")
.or_insert_with(|| "computed value")
.unwrap();
// Works with numeric keys too
ht.entry(42i64).or_insert("value at index 42").unwrap();
ht
}
fn main() {}
Entry Variants
The entry() method returns an Entry enum with two variants:
Entry::Occupied- The key exists in the hash tableEntry::Vacant- The key does not exist
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::types::{ZendHashTable, Entry, Zval};
use ext_php_rs::boxed::ZBox;
#[php_function]
pub fn match_entry() -> ZBox<ZendHashTable> {
let mut ht = ZendHashTable::new();
ht.insert("existing", "value").unwrap();
// Pattern match on the entry
match ht.entry("existing") {
Entry::Occupied(entry) => {
println!("Key {:?} exists with value {:?}",
entry.key(),
entry.get().and_then(Zval::str),
);
}
Entry::Vacant(entry) => {
println!("Key {:?} is vacant", entry.key());
entry.insert("new value").unwrap();
}
}
ht
}
fn main() {}
Common Patterns
Counting occurrences
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::types::ZendHashTable;
use ext_php_rs::boxed::ZBox;
#[php_function]
pub fn count_words(words: Vec<String>) -> ZBox<ZendHashTable> {
let mut counts = ZendHashTable::new();
for word in words {
counts.entry(word.as_str())
.and_modify(|v| {
if let Some(n) = v.long() {
v.set_long(n + 1);
}
})
.or_insert(1i64)
.unwrap();
}
counts
}
fn main() {}
Caching computed values
This example demonstrates using or_insert_with_key for lazy computation:
extern crate ext_php_rs;
use ext_php_rs::types::ZendHashTable;
fn expensive_computation(key: &str) -> String {
format!("computed_{}", key)
}
fn get_or_compute(cache: &mut ZendHashTable, key: &str) -> String {
let value = cache.entry(key)
.or_insert_with_key(|k| expensive_computation(&k.to_string()))
.unwrap();
value.str().unwrap_or_default().to_string()
}
fn main() {}
Updating existing values
This example shows how to conditionally update a value only if the key exists:
extern crate ext_php_rs;
use ext_php_rs::types::{ZendHashTable, Entry};
fn update_if_exists(ht: &mut ZendHashTable, key: &str, new_value: &str) -> bool {
match ht.entry(key) {
Entry::Occupied(mut entry) => {
entry.insert(new_value).unwrap();
true
}
Entry::Vacant(_) => false,
}
}
fn main() {}
Entry Methods Reference
Entry methods
| Method | Description |
|---|---|
or_insert(default) | Insert default if vacant, return &mut Zval |
or_insert_with(f) | Insert result of f() if vacant |
or_insert_with_key(f) | Insert result of f(&key) if vacant |
or_default() | Insert default Zval (null) if vacant |
key() | Get reference to the key |
and_modify(f) | Modify value in place if occupied |
OccupiedEntry methods
| Method | Description |
|---|---|
key() | Get reference to key |
get() | Get reference to value |
get_mut() | Get mutable reference to value |
into_mut() | Convert to mutable reference with entry’s lifetime |
insert(value) | Replace value, returning old value |
remove() | Remove and return value |
remove_entry() | Remove and return key-value pair |
VacantEntry methods
| Method | Description |
|---|---|
key() | Get reference to key |
into_key() | Take ownership of key |
insert(value) | Insert value and return &mut Zval |
PHP Example
<?php
// Using the create_array function
$arr = create_array();
var_dump($arr);
// array(5) {
// [0]=> string(5) "first"
// [1]=> string(6) "second"
// ["name"]=> string(4) "John"
// ["age"]=> int(30)
// [100]=> string(12) "at index 100"
// }
// Count words
$counts = count_words(['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']);
var_dump($counts);
// array(3) {
// ["apple"]=> int(3)
// ["banana"]=> int(2)
// ["cherry"]=> int(1)
// }
Binary
Binary data is represented as a string in PHP. The most common source of this
data is from the pack and unpack functions. It allows you to transfer
arbitrary binary data between Rust and PHP.
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| Yes | No | Yes | No | zend_string |
The binary type is represented as a string in PHP. Although not encoded, the data is converted into an array and then the pointer to the data is set as the string pointer, with the length of the array being the length of the string.
Binary<T> is valid when T implements Pack. This is currently implemented
on most primitive numbers (i8, i16, i32, i64, u8, u16, u32, u64, isize, usize,
f32, f64).
Rust Usage
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::binary::Binary;
#[php_function]
pub fn test_binary(input: Binary<u32>) -> Binary<u32> {
for i in input.iter() {
println!("{}", i);
}
vec![5, 4, 3, 2, 1]
.into_iter()
.collect::<Binary<_>>()
}
fn main() {}
PHP Usage
<?php
$data = pack('L*', 1, 2, 3, 4, 5);
$output = unpack('L*', test_binary($data));
var_dump($output); // array(5) { [0] => 5, [1] => 4, [2] => 3, [3] => 2, [4] => 1 }
Binary Slices
Binary data is represented as a string in PHP. The most common source of this
data is from the pack and unpack functions. It allows you to use a PHP
string as a read-only slice in Rust.
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| Yes | No | No | No | zend_string |
The binary type is represented as a string in PHP. Although not encoded, the data is converted into a slice and then the pointer to the data is set as the string pointer, with the length of the array being the length of the string.
BinarySlice<T> is valid when T implements PackSlice. This is currently
implemented on most primitive numbers (i8, i16, i32, i64, u8, u16, u32, u64,
isize, usize, f32, f64).
Rust Usage
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::binary_slice::BinarySlice;
#[php_function]
pub fn test_binary_slice(input: BinarySlice<u8>) -> u8 {
let mut sum = 0;
for i in input.iter() {
sum += i;
}
sum
}
fn main() {}
PHP Usage
<?php
$data = pack('C*', 1, 2, 3, 4, 5);
$output = test_binary_slice($data);
var_dump($output); // 15
Option<T>
Options are used for optional and nullable parameters, as well as null returns.
It is valid to be converted to/from a zval as long as the underlying T generic
is also able to be converted to/from a zval.
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| Yes | No | Yes | No | Depends on T, null for None. |
Using Option<T> as a parameter indicates that the parameter is nullable. If
null is passed, a None value will be supplied. It is also used in the place of
optional parameters. If the parameter is not given, a None value will also be
supplied.
Returning Option<T> is a nullable return type. Returning None will return
null to PHP.
Rust example
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_function]
pub fn test_option_null(input: Option<String>) -> Option<String> {
input.map(|input| format!("Hello {}", input).into())
}
fn main() {}
PHP example
<?php
var_dump(test_option_null("World")); // string(11) "Hello World"
var_dump(test_option_null()); // null
Object
An object is any object type in PHP. This can include a PHP class and PHP
stdClass. A Rust struct registered as a PHP class is a class object, which
contains an object.
Objects are valid as parameters but only as an immutable or mutable reference. You cannot take ownership of an object as objects are reference counted, and multiple zvals can point to the same object. You can return a boxed owned object.
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| No | Yes | ZBox<ZendObject> | Yes, mutable only | Zend object. |
Examples
Calling a method
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{prelude::*, types::ZendObject};
// Take an object reference and also return it.
#[php_function]
pub fn take_obj(obj: &mut ZendObject) -> () {
let _ = obj.try_call_method("hello", vec![&"arg1", &"arg2"]);
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(take_obj))
}
fn main() {}
Taking an object reference
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{prelude::*, types::ZendObject};
// Take an object reference and also return it.
#[php_function]
pub fn take_obj(obj: &mut ZendObject) -> &mut ZendObject {
let _ = obj.set_property("hello", 5);
dbg!(obj)
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(take_obj))
}
fn main() {}
Creating a new object
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{prelude::*, types::ZendObject, boxed::ZBox};
// Create a new `stdClass` and return it.
#[php_function]
pub fn make_object() -> ZBox<ZendObject> {
let mut obj = ZendObject::new_stdclass();
let _ = obj.set_property("hello", 5);
obj
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(make_object))
}
fn main() {}
Lazy Objects (PHP 8.4+)
PHP 8.4 introduced lazy objects, which defer their initialization until their properties are first accessed. ext-php-rs provides APIs to introspect and create lazy objects from Rust.
Lazy Object Types
There are two types of lazy objects:
-
Lazy Ghosts: The ghost object itself becomes the real instance when initialized. After initialization, the ghost is indistinguishable from a regular object.
-
Lazy Proxies: A proxy wraps a real instance that is created when first accessed. The proxy and real instance have different identities. After initialization, the proxy still reports as lazy.
Introspection APIs
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{prelude::*, types::ZendObject};
#[php_function]
pub fn check_lazy(obj: &ZendObject) -> String {
if obj.is_lazy() {
if obj.is_lazy_ghost() {
if obj.is_lazy_initialized() {
"Initialized lazy ghost".into()
} else {
"Uninitialized lazy ghost".into()
}
} else if obj.is_lazy_proxy() {
if obj.is_lazy_initialized() {
"Initialized lazy proxy".into()
} else {
"Uninitialized lazy proxy".into()
}
} else {
"Unknown lazy type".into()
}
} else {
"Not a lazy object".into()
}
}
fn main() {}
Available introspection methods:
| Method | Description |
|---|---|
is_lazy() | Returns true if the object is lazy (ghost or proxy) |
is_lazy_ghost() | Returns true if the object is a lazy ghost |
is_lazy_proxy() | Returns true if the object is a lazy proxy |
is_lazy_initialized() | Returns true if the lazy object has been initialized |
lazy_init() | Triggers initialization of a lazy object |
lazy_get_instance() | For proxies, returns Option<&mut Self> with the real instance after initialization |
You can also check if a class supports lazy objects using ClassEntry::can_be_lazy():
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{prelude::*, zend::ClassEntry};
#[php_function]
pub fn can_class_be_lazy(class_name: &str) -> bool {
ClassEntry::try_find(class_name)
.map(|ce| ce.can_be_lazy())
.unwrap_or(false)
}
fn main() {}
Creating Lazy Objects from Rust
You can create lazy objects from Rust using make_lazy_ghost() and
make_lazy_proxy(). These methods require the closure feature:
[dependencies]
ext-php-rs = { version = "0.15", features = ["closure"] }
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{prelude::*, types::ZendObject, boxed::ZBox};
#[php_function]
pub fn create_lazy_ghost(obj: &mut ZendObject) -> PhpResult<()> {
let init_value = "initialized".to_string();
obj.make_lazy_ghost(Box::new(move || {
// Initialization logic - use captured state
println!("Initializing with: {}", init_value);
}) as Box<dyn Fn()>)?;
Ok(())
}
#[php_function]
pub fn create_lazy_proxy(obj: &mut ZendObject) -> PhpResult<()> {
obj.make_lazy_proxy(Box::new(|| {
// Return the real instance
Some(ZendObject::new_stdclass())
}) as Box<dyn Fn() -> Option<ZBox<ZendObject>>>)?;
Ok(())
}
fn main() {}
Creating Lazy Objects from PHP
For full control over lazy object creation, use PHP’s Reflection API:
<?php
// Create a lazy ghost
$reflector = new ReflectionClass(MyClass::class);
$ghost = $reflector->newLazyGhost(function ($obj) {
$obj->__construct('initialized');
});
// Create a lazy proxy
$proxy = $reflector->newLazyProxy(function ($obj) {
return new MyClass('initialized');
});
Limitations
-
PHP 8.4+ only: Lazy objects are a PHP 8.4 feature and not available in earlier versions.
-
closurefeature required: Themake_lazy_ghost()andmake_lazy_proxy()methods require theclosurefeature to be enabled. -
User-defined classes only: PHP lazy objects only work with user-defined PHP classes, not internal classes. Since Rust-defined classes (using
#[php_class]) are registered as internal classes, they cannot be made lazy. -
Closure parameter access: Due to Rust trait system limitations, the
make_lazy_ghost()andmake_lazy_proxy()closures don’t receive the object being initialized as a parameter. Capture any needed initialization state in the closure itself.
Class Object
A class object is an instance of a Rust struct (which has been registered as a PHP class) that has been allocated alongside an object. You can think of a class object as a superset of an object, as a class object contains a Zend object.
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
| No | &ZendClassObject<T> | Yes | &mut ZendClassObject<T> | Zend object and a Rust struct. |
Examples
Returning a reference to self
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{prelude::*, types::ZendClassObject};
#[php_class]
pub struct Example {
foo: i32,
bar: i32
}
#[php_impl]
impl Example {
// ext-php-rs treats the method as associated due to the `self_` argument.
// The argument _must_ be called `self_`.
pub fn builder_pattern(
self_: &mut ZendClassObject<Example>,
) -> &mut ZendClassObject<Example> {
// do something with `self_`
self_
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.class::<Example>()
}
fn main() {}
Creating a new class instance
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_class]
pub struct Example {
foo: i32,
bar: i32
}
#[php_impl]
impl Example {
pub fn make_new(foo: i32, bar: i32) -> Example {
Example { foo, bar }
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.class::<Example>()
}
fn main() {}
Closure
Rust closures can be passed to PHP through a wrapper class PhpClosure. The
Rust closure must be static (i.e. can only reference things with a 'static
lifetime, so not self in methods), and can take up to 8 parameters, all of
which must implement FromZval. The return type must implement IntoZval.
Passing closures from Rust to PHP is feature-gated behind the closure feature.
Enable it in your Cargo.toml:
ext-php-rs = { version = "...", features = ["closure"] }
PHP callables (which includes closures) can be passed to Rust through the
Callable type. When calling a callable, you must provide it with a Vec of
arguments, all of which must implement IntoZval and Clone.
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
Callable | No | Closure, Callablefor PHP functions | No | Callables are implemented in PHP, closures are represented as an instance of PhpClosure. |
Internally, when you enable the closure feature, a class PhpClosure is
registered alongside your other classes:
<?php
class PhpClosure
{
public function __invoke(..$args): mixed;
}
This class cannot be instantiated from PHP. When the class is invoked, the underlying Rust closure is called. There are three types of closures in Rust:
Fn and FnMut
These closures can be called multiple times. FnMut differs from Fn in the
fact that it can modify variables in its scope.
Example
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_function]
pub fn closure_get_string() -> Closure {
// Return a closure which takes two integers and returns a string
Closure::wrap(Box::new(|a, b| {
format!("A: {} B: {}", a, b)
}) as Box<dyn Fn(i32, i32) -> String>)
}
#[php_function]
pub fn closure_count() -> Closure {
let mut count = 0i32;
// Return a closure which takes an integer, adds it to a persistent integer,
// and returns the updated value.
Closure::wrap(Box::new(move |a: i32| {
count += a;
count
}) as Box<dyn FnMut(i32) -> i32>)
}
fn main() {}
FnOnce
Closures that implement FnOnce can only be called once. They consume some sort
of value. Calling these closures more than once will cause them to throw an
exception. They must be wrapped using the wrap_once function instead of
wrap.
Internally, the FnOnce closure is wrapped again by an FnMut closure, which
owns the FnOnce closure until it is called. If the FnMut closure is called
again, the FnOnce closure would have already been consumed, and an exception
will be thrown.
Example
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_function]
pub fn closure_return_string() -> Closure {
let example: String = "Hello, world!".into();
// This closure consumes `example` and therefore cannot be called more than once.
Closure::wrap_once(Box::new(move || {
example
}) as Box<dyn FnOnce() -> String>)
}
fn main() {}
Closures must be boxed as PHP classes cannot support generics, therefore trait objects must be used. These must be boxed to have a compile time size.
Callable
Callables are simply represented as zvals. You can attempt to get a callable
function by its name, or as a parameter. They can be called through the
try_call method implemented on Callable, which returns a zval in a result.
Callable parameter
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_function]
pub fn callable_parameter(call: ZendCallable) {
let val = call.try_call(vec![&0, &1, &"Hello"]).expect("Failed to call function");
dbg!(val);
}
fn main() {}
Functions & methods
PHP functions and methods are represented by the Function struct.
You can use the try_from_function and try_from_method methods to obtain a Function struct corresponding to the passed function or static method name.
It’s heavily recommended you reuse returned Function objects, to avoid the overhead of looking up the function/method name.
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::zend::Function;
#[php_function]
pub fn test_function() -> () {
let var_dump = Function::try_from_function("var_dump").unwrap();
let _ = var_dump.try_call(vec![&"abc"]);
}
#[php_function]
pub fn test_method() -> () {
let f = Function::try_from_method("ClassName", "staticMethod").unwrap();
let _ = f.try_call(vec![&"abc"]);
}
fn main() {}
Macros
ext-php-rs comes with a set of macros that are used to annotate types which
are to be exported to PHP. This allows you to write Rust-like APIs that can be
used from PHP without fiddling around with zvals.
php_module- Defines the function used by PHP to retrieve your extension.- [
php_startup] - Defines the extension startup function used by PHP to initialize your extension. php_function- Used to export a Rust function to PHP.php_class- Used to export a Rust struct or enum as a PHP class.php_impl- Used to export a Rustimplblock to PHP, including all methods and constants.php_const- Used to export a Rust constant to PHP as a global constant.php_extern- Attribute used to annotateexternblocks which are deemed as PHP functions.php_interface- Attribute used to export Rust Trait as PHP interfacephp- Used to modify the default behavior of the above macros. This is a generic attribute that can be used on most of the above macros.
#[php_module] Attribute
The module macro is used to annotate the get_module function, which is used by
the PHP interpreter to retrieve information about your extension, including the
name, version, functions and extra initialization functions. Regardless if you
use this macro, your extension requires a extern "C" fn get_module() so that
PHP can get this information.
The function is renamed to get_module if you have used another name. The
function is passed an instance of ModuleBuilder which allows you to register
the following (if required):
- Functions, classes, and constants
- Extension and request startup and shutdown functions.
- Read more about the PHP extension lifecycle here.
- PHP extension information function
- Used by the
phpinfo()function to get information about your extension.
- Used by the
Classes and constants are not registered with PHP in the get_module function. These are
registered inside the extension startup function.
Usage
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{
prelude::*,
zend::ModuleEntry,
info_table_start,
info_table_row,
info_table_end
};
#[php_const]
pub const MY_CUSTOM_CONST: &'static str = "Hello, world!";
#[php_class]
pub struct Test {
a: i32,
b: i32
}
#[php_function]
pub fn hello_world() -> &'static str {
"Hello, world!"
}
/// Used by the `phpinfo()` function and when you run `php -i`.
/// This will probably be simplified with another macro eventually!
pub extern "C" fn php_module_info(_module: *mut ModuleEntry) {
info_table_start!();
info_table_row!("my extension", "enabled");
info_table_end!();
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.constant(wrap_constant!(MY_CUSTOM_CONST))
.class::<Test>()
.function(wrap_function!(hello_world))
.info_function(php_module_info)
}
fn main() {}
#[php_function] Attribute
Used to annotate functions which should be exported to PHP. Note that this
should not be used on class methods - see the #[php_impl] macro for that.
See the list of types that are valid as parameter and return types.
Optional parameters
Optional parameters can be used by setting the Rust parameter type to a variant
of Option<T>. The macro will then figure out which parameters are optional by
using the last consecutive arguments that are a variant of Option<T> or have a
default value.
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_function]
pub fn greet(name: String, age: Option<i32>) -> String {
let mut greeting = format!("Hello, {}!", name);
if let Some(age) = age {
greeting += &format!(" You are {} years old.", age);
}
greeting
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(greet))
}
fn main() {}
Default parameter values can also be set for optional parameters. This is done
through the #[php(defaults)] attribute option. When an optional parameter has a
default, it does not need to be a variant of Option:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_function]
#[php(defaults(offset = 0))]
pub fn rusty_strpos(haystack: &str, needle: &str, offset: i64) -> Option<usize> {
let haystack: String = haystack.chars().skip(offset as usize).collect();
haystack.find(needle)
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(rusty_strpos))
}
fn main() {}
Note that if there is a non-optional argument after an argument that is a
variant of Option<T>, the Option<T> argument will be deemed a nullable
argument rather than an optional argument.
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
/// `age` will be deemed required and nullable rather than optional.
#[php_function]
pub fn greet(name: String, age: Option<i32>, description: String) -> String {
let mut greeting = format!("Hello, {}!", name);
if let Some(age) = age {
greeting += &format!(" You are {} years old.", age);
}
greeting += &format!(" {}.", description);
greeting
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(greet))
}
fn main() {}
You can also specify the optional arguments if you want to have nullable arguments before optional arguments. This is done through an attribute parameter:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
/// `age` will be deemed required and nullable rather than optional,
/// while description will be optional.
#[php_function]
#[php(optional = "description")]
pub fn greet(name: String, age: Option<i32>, description: Option<String>) -> String {
let mut greeting = format!("Hello, {}!", name);
if let Some(age) = age {
greeting += &format!(" You are {} years old.", age);
}
if let Some(description) = description {
greeting += &format!(" {}.", description);
}
greeting
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(greet))
}
fn main() {}
Variadic Functions
Variadic functions can be implemented by specifying the last argument in the Rust
function to the type &[&Zval]. This is the equivalent of a PHP function using
the ...$args syntax.
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{prelude::*, types::Zval};
/// This can be called from PHP as `add(1, 2, 3, 4, 5)`
#[php_function]
pub fn add(number: u32, numbers:&[&Zval]) -> u32 {
// numbers is a slice of 4 Zvals all of type long
number
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(add))
}
fn main() {}
Returning Result<T, E>
You can also return a Result from the function. The error variant will be
translated into an exception and thrown. See the section on
exceptions for more details.
#[php_interface] Attribute
You can export a Trait block to PHP. This exports all methods as well as
constants to PHP on the interface. Trait method SHOULD NOT contain default
implementations, as these are not supported in PHP interfaces.
Options
By default all constants are renamed to UPPER_CASE and all methods are renamed to
camelCase. This can be changed by passing the change_method_case and
change_constant_case as #[php] attributes on the impl block. The options are:
#[php(change_method_case = "snake_case")]- Renames the method to snake case.#[php(change_constant_case = "snake_case")]- Renames the constant to snake case.
See the name and change_case section for a list of all
available cases.
Methods
See the php_impl
Constants
See the php_impl
Example
Define an example trait with methods and constant:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{prelude::*, types::ZendClassObject};
#[php_interface]
#[php(name = "Rust\\TestInterface")]
trait Test {
const TEST: &'static str = "TEST";
fn co();
#[php(defaults(value = 0))]
fn set_value(&mut self, value: i32);
}
#[php_module]
pub fn module(module: ModuleBuilder) -> ModuleBuilder {
module
.interface::<PhpInterfaceTest>()
}
fn main() {}
Using our newly created interface in PHP:
<?php
assert(interface_exists("Rust\TestInterface"));
class B implements Rust\TestInterface {
public static function co() {}
public function setValue(?int $value = 0) {
}
}
Interface Inheritance
PHP interfaces can extend other interfaces. You can achieve this in two ways:
Using #[php(extends(...))]
Use the extends attribute to extend a built-in PHP interface or another Rust-defined interface.
For built-in PHP interfaces, use the explicit form:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::zend::ce;
#[php_interface]
#[php(extends(ce = ce::throwable, stub = "\\Throwable"))]
#[php(name = "MyException")]
trait MyExceptionInterface {
fn get_error_code(&self) -> i32;
}
fn main() {}
For Rust-defined interfaces, you can use the simpler type syntax:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_interface]
trait BaseInterface {
fn base_method(&self) -> i32;
}
#[php_interface]
#[php(extends(BaseInterface))]
trait ExtendedInterface {
fn extended_method(&self) -> String;
}
fn main() {}
Using Rust Trait Bounds
You can also use Rust’s trait bound syntax. When a trait marked with #[php_interface]
has supertraits, the PHP interface will automatically extend those parent interfaces:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_interface]
#[php(name = "Rust\\ParentInterface")]
trait ParentInterface {
fn parent_method(&self) -> String;
}
// ChildInterface extends ParentInterface in PHP
#[php_interface]
#[php(name = "Rust\\ChildInterface")]
trait ChildInterface: ParentInterface {
fn child_method(&self) -> String;
}
#[php_module]
pub fn module(module: ModuleBuilder) -> ModuleBuilder {
module
.interface::<PhpInterfaceParentInterface>()
.interface::<PhpInterfaceChildInterface>()
}
fn main() {}
In PHP:
<?php
// ChildInterface extends ParentInterface
assert(is_a('Rust\ChildInterface', 'Rust\ParentInterface', true));
#[php_impl_interface] Attribute
The #[php_impl_interface] attribute allows a Rust class to implement a custom PHP
interface defined with #[php_interface]. This creates a relationship where PHP’s
instanceof and is_a() recognize the implementation.
Key feature: The macro automatically registers the trait methods as PHP methods
on the class. You don’t need to duplicate them in a separate #[php_impl] block.
Example
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
// Define a custom interface
#[php_interface]
#[php(name = "Rust\\Greetable")]
trait Greetable {
fn greet(&self) -> String;
}
// Define a class
#[php_class]
#[php(name = "Rust\\Greeter")]
pub struct Greeter {
name: String,
}
#[php_impl]
impl Greeter {
pub fn __construct(name: String) -> Self {
Self { name }
}
// Note: No need to add greet() here - it's automatically
// registered by #[php_impl_interface] below
}
// Implement the interface for the class
// This automatically registers greet() as a PHP method
#[php_impl_interface]
impl Greetable for Greeter {
fn greet(&self) -> String {
format!("Hello, {}!", self.name)
}
}
#[php_module]
pub fn module(module: ModuleBuilder) -> ModuleBuilder {
module
.interface::<PhpInterfaceGreetable>()
.class::<Greeter>()
}
fn main() {}
Using in PHP:
<?php
$greeter = new Rust\Greeter("World");
// instanceof works
assert($greeter instanceof Rust\Greetable);
// is_a() works
assert(is_a($greeter, 'Rust\Greetable'));
// The greet() method is available (registered by #[php_impl_interface])
echo $greeter->greet(); // Output: Hello, World!
// Can be used as type hint
function greet(Rust\Greetable $obj): void {
echo $obj->greet();
}
greet($greeter);
When to Use
- Use
#[php_impl_interface]for custom interfaces you define with#[php_interface] - Use
#[php(implements(ce = ...))]on#[php_class]for built-in PHP interfaces likeIterator,ArrayAccess,Countable, etc.
See the Classes documentation for examples of implementing built-in interfaces.
Cross-Crate Support
The #[php_impl_interface] macro supports cross-crate interface discovery via the
inventory crate. This means you can define
an interface in one crate and implement it in another crate, and the implementation
will be automatically discovered at link time.
Example: Defining an Interface in a Library Crate
First, create a library crate that defines the interface:
# my-interfaces/Cargo.toml
[package]
name = "my-interfaces"
version = "0.1.0"
[dependencies]
ext-php-rs = "0.15"
// my-interfaces/src/lib.rs
use ext_php_rs::prelude::*;
/// A serializable interface that can convert objects to JSON.
#[php_interface]
#[php(name = "MyInterfaces\\Serializable")]
pub trait Serializable {
fn to_json(&self) -> String;
}
// Re-export the generated PHP interface struct for consumers
pub use PhpInterfaceSerializable;
Example: Implementing the Interface in Another Crate
Now create your extension crate that implements the interface:
# my-extension/Cargo.toml
[package]
name = "my-extension"
version = "0.1.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
ext-php-rs = "0.15"
my-interfaces = { path = "../my-interfaces" }
// my-extension/src/lib.rs
use ext_php_rs::prelude::*;
use my_interfaces::Serializable;
#[php_class]
#[php(name = "MyExtension\\User")]
pub struct User {
name: String,
email: String,
}
#[php_impl]
impl User {
pub fn __construct(name: String, email: String) -> Self {
Self { name, email }
}
// Note: No need to add to_json() here - it's automatically
// registered by #[php_impl_interface] below
}
// Register the interface implementation
// This automatically registers to_json() as a PHP method
#[php_impl_interface]
impl Serializable for User {
fn to_json(&self) -> String {
format!(r#"{{"name":"{}","email":"{}"}}"#, self.name, self.email)
}
}
#[php_module]
pub fn module(module: ModuleBuilder) -> ModuleBuilder {
module
// Register the interface from the library crate
.interface::<my_interfaces::PhpInterfaceSerializable>()
.class::<User>()
}
Using in PHP
<?php
use MyExtension\User;
use MyInterfaces\Serializable;
$user = new User("John", "john@example.com");
// instanceof works across crates
assert($user instanceof Serializable);
// Type hints work
function serialize_object(Serializable $obj): string {
return $obj->toJson();
}
echo serialize_object($user);
// Output: {"name":"John","email":"john@example.com"}
Important Notes
-
Automatic method registration: The
#[php_impl_interface]macro automatically registers all trait methods as PHP methods on the class. You don’t need to duplicate them in a#[php_impl]block. -
Interface registration: The interface must be registered in the
#[php_module]function using.interface::<PhpInterfaceName>(). -
Link-time discovery: The
inventorycrate uses link-time registration for interface discovery, so all implementations are automatically discovered when the final binary is linked.
#[php_class] Attribute
Structs can be exported to PHP as classes with the #[php_class] attribute
macro. This attribute derives the RegisteredClass trait on your struct, as
well as registering the class to be registered with the #[php_module] macro.
Options
There are additional macros that modify the class. These macros must be
placed underneath the #[php_class] attribute.
name- Changes the name of the class when exported to PHP. The Rust struct name is kept the same. If no name is given, the name of the struct is used. Useful for namespacing classes.change_case- Changes the case of the class name when exported to PHP.readonly- Marks the class as readonly (PHP 8.2+). All properties in a readonly class are implicitly readonly.flags- Sets class flags usingClassFlags, e.g.#[php(flags = ClassFlags::Final)]for a final class.#[php(extends(...))]- Sets the parent class of the class. Can only be used once. Two forms are supported:- Simple type form:
#[php(extends(MyBaseClass))]- For Rust-defined classes that implementRegisteredClass. - Explicit form:
#[php(extends(ce = ce_fn, stub = "ParentClass"))]- For built-in PHP classes.ce_fnmust be a function with the signaturefn() -> &'static ClassEntry.
- Simple type form:
#[php(implements(...))]- Implements the given interface on the class. Can be used multiple times. Two forms are supported:- Simple type form:
#[php(implements(MyInterface))]— For Rust-defined interfaces that implementRegisteredClass. - Explicit form:
#[php(implements(ce = ce_fn, stub = "InterfaceName"))]— For built-in PHP interfaces.ce_fnmust be a valid function with the signaturefn() -> &'static ClassEntry.
- Simple type form:
You may also use the #[php(prop)] attribute on a struct field to use the field as a
PHP property. By default, the field will be accessible from PHP publicly with
the same name as the field. Property types must implement IntoZval and
FromZval.
You can customize properties with these options:
name- Allows you to rename the property, e.g.#[php(prop, name = "new_name")]change_case- Allows you to rename the property using rename rules, e.g.#[php(prop, change_case = PascalCase)]static- Makes the property static (shared across all instances), e.g.#[php(prop, static)]flags- Sets property visibility flags, e.g.#[php(prop, flags = ext_php_rs::flags::PropertyFlags::Private)]
Restrictions
No lifetime parameters
Rust lifetimes are used by the Rust compiler to reason about a program’s memory safety. They are a compile-time only concept; there is no way to access Rust lifetimes at runtime from a dynamic language like PHP.
As soon as Rust data is exposed to PHP,
there is no guarantee which the Rust compiler can make on how long the data will live.
PHP is a reference-counted language and those references can be held
for an arbitrarily long time, which is untraceable by the Rust compiler.
The only possible way to express this correctly is to require that any #[php_class]
does not borrow data for any lifetime shorter than the 'static lifetime,
i.e. the #[php_class] cannot have any lifetime parameters.
When you need to share ownership of data between PHP and Rust, instead of using borrowed references with lifetimes, consider using reference-counted smart pointers such as Arc.
No generic parameters
A Rust struct Foo<T> with a generic parameter T generates new compiled implementations
each time it is used with a different concrete type for T.
These new implementations are generated by the compiler at each usage site.
This is incompatible with wrapping Foo in PHP,
where there needs to be a single compiled implementation of Foo which is integrated with the PHP interpreter.
Example
This example creates a PHP class Human, adding a PHP property address.
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_class]
pub struct Human {
name: String,
age: i32,
#[php(prop)]
address: String,
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.class::<Human>()
}
fn main() {}
Create a custom exception RedisException, which extends Exception, and put
it in the Redis\Exception namespace:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{
prelude::*,
exception::PhpException,
zend::ce
};
#[php_class]
#[php(name = "Redis\\Exception\\RedisException")]
#[php(extends(ce = ce::exception, stub = "\\Exception"))]
#[derive(Default)]
pub struct RedisException;
// Throw our newly created exception
#[php_function]
pub fn throw_exception() -> PhpResult<i32> {
Err(PhpException::from_class::<RedisException>("Not good!".into()))
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.class::<RedisException>()
.function(wrap_function!(throw_exception))
}
fn main() {}
Extending a Rust-defined Class
When extending another Rust-defined class, you can use the simpler type syntax:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_class]
#[derive(Default)]
pub struct Animal;
#[php_impl]
impl Animal {
pub fn speak(&self) -> &'static str {
"..."
}
}
#[php_class]
#[php(extends(Animal))]
#[derive(Default)]
pub struct Dog;
#[php_impl]
impl Dog {
pub fn speak(&self) -> &'static str {
"Woof!"
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.class::<Animal>()
.class::<Dog>()
}
fn main() {}
Sharing Methods Between Parent and Child Classes
When both parent and child are Rust-defined classes, methods defined only in the parent won’t automatically work when called on a child instance. This is because each Rust type has its own object handlers.
The recommended workaround is to use a Rust trait for shared behavior:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
/// Trait for shared behavior
trait AnimalBehavior {
fn speak(&self) -> &'static str {
"..."
}
}
#[php_class]
#[derive(Default)]
pub struct Animal;
impl AnimalBehavior for Animal {}
#[php_impl]
impl Animal {
pub fn speak(&self) -> &'static str {
AnimalBehavior::speak(self)
}
}
#[php_class]
#[php(extends(Animal))]
#[derive(Default)]
pub struct Dog;
impl AnimalBehavior for Dog {
fn speak(&self) -> &'static str {
"Woof!"
}
}
#[php_impl]
impl Dog {
// Re-export the method so it works on Dog instances
pub fn speak(&self) -> &'static str {
AnimalBehavior::speak(self)
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.class::<Animal>()
.class::<Dog>()
}
fn main() {}
This pattern ensures that:
$animal->speak()returns"..."$dog->speak()returns"Woof!"$dog instanceof Animalistrue
Implementing an Interface
To implement an interface, use #[php(implements(...))]. For built-in PHP interfaces, use the explicit form with ce and stub. For Rust-defined interfaces, you can use the simple type form.
The following example implements ArrayAccess:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{
prelude::*,
exception::PhpResult,
types::Zval,
zend::ce,
};
#[php_class]
#[php(implements(ce = ce::arrayaccess, stub = "\\ArrayAccess"))]
#[derive(Default)]
pub struct EvenNumbersArray;
/// Returns `true` if the array offset is an even number.
/// Usage:
/// ```php
/// $arr = new EvenNumbersArray();
/// var_dump($arr[0]); // true
/// var_dump($arr[1]); // false
/// var_dump($arr[2]); // true
/// var_dump($arr[3]); // false
/// var_dump($arr[4]); // true
/// var_dump($arr[5] = true); // Fatal error: Uncaught Exception: Setting values is not supported
/// ```
#[php_impl]
impl EvenNumbersArray {
pub fn __construct() -> EvenNumbersArray {
EvenNumbersArray {}
}
// We need to use `Zval` because ArrayAccess needs $offset to be a `mixed`
pub fn offset_exists(&self, offset: &'_ Zval) -> bool {
offset.is_long()
}
pub fn offset_get(&self, offset: &'_ Zval) -> PhpResult<bool> {
let integer_offset = offset.long().ok_or("Expected integer offset")?;
Ok(integer_offset % 2 == 0)
}
pub fn offset_set(&mut self, _offset: &'_ Zval, _value: &'_ Zval) -> PhpResult {
Err("Setting values is not supported".into())
}
pub fn offset_unset(&mut self, _offset: &'_ Zval) -> PhpResult {
Err("Setting values is not supported".into())
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.class::<EvenNumbersArray>()
}
fn main() {}
Static Properties
Static properties are shared across all instances of a class. Use #[php(prop, static)]
to declare a static property. Unlike instance properties, static properties are managed
entirely by PHP and do not use Rust property handlers.
You can specify a default value using the default attribute:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::class::RegisteredClass;
#[php_class]
pub struct Counter {
#[php(prop)]
pub instance_value: i32,
#[php(prop, static, default = 0)]
pub count: i32,
#[php(prop, static, flags = ext_php_rs::flags::PropertyFlags::Private)]
pub internal_state: String,
}
#[php_impl]
impl Counter {
pub fn __construct(value: i32) -> Self {
Self {
instance_value: value,
count: 0,
internal_state: String::new(),
}
}
/// Increment the static counter from Rust
pub fn increment() {
let ce = Self::get_metadata().ce();
let current: i64 = ce.get_static_property("count").unwrap_or(0);
ce.set_static_property("count", current + 1).unwrap();
}
/// Get the current count
pub fn get_count() -> i64 {
let ce = Self::get_metadata().ce();
ce.get_static_property("count").unwrap_or(0)
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.class::<Counter>()
}
fn main() {}
From PHP, you can access static properties directly on the class:
// No need to initialize - count already has default value of 0
Counter::increment();
Counter::increment();
echo Counter::$count; // 2
echo Counter::getCount(); // 2
Abstract Classes
Abstract classes cannot be instantiated directly and may contain abstract methods
that must be implemented by subclasses. Use #[php(flags = ClassFlags::Abstract)]
to declare an abstract class:
use ext_php_rs::prelude::*;
use ext_php_rs::flags::ClassFlags;
#[php_class]
#[php(flags = ClassFlags::Abstract)]
pub struct AbstractAnimal;
#[php_impl]
impl AbstractAnimal {
// Protected constructor for subclasses
#[php(vis = "protected")]
pub fn __construct() -> Self {
Self
}
// Abstract method - must be implemented by subclasses.
// Body is never called; use unimplemented!() as a placeholder.
#[php(abstract)]
pub fn speak(&self) -> String {
unimplemented!()
}
// Concrete method - inherited by subclasses
pub fn breathe(&self) {
println!("Breathing...");
}
}
From PHP, you can extend this abstract class:
class Dog extends AbstractAnimal {
public function __construct() {
parent::__construct();
}
public function speak(): string {
return "Woof!";
}
}
$dog = new Dog();
echo $dog->speak(); // "Woof!"
$dog->breathe(); // "Breathing..."
// This would cause an error:
// $animal = new AbstractAnimal(); // Cannot instantiate abstract class
See the impl documentation for more details on abstract methods.
Final Classes
Final classes cannot be extended. Use #[php(flags = ClassFlags::Final)] to
declare a final class:
use ext_php_rs::prelude::*;
use ext_php_rs::flags::ClassFlags;
#[php_class]
#[php(flags = ClassFlags::Final)]
pub struct FinalClass;
Readonly Classes (PHP 8.2+)
PHP 8.2 introduced readonly classes,
where all properties are implicitly readonly. You can create a readonly class using
the #[php(readonly)] attribute:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_class]
#[php(readonly)]
pub struct ImmutablePoint {
x: f64,
y: f64,
}
#[php_impl]
impl ImmutablePoint {
pub fn __construct(x: f64, y: f64) -> Self {
Self { x, y }
}
pub fn get_x(&self) -> f64 {
self.x
}
pub fn get_y(&self) -> f64 {
self.y
}
pub fn distance_from_origin(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.class::<ImmutablePoint>()
}
fn main() {}
From PHP:
$point = new ImmutablePoint(3.0, 4.0);
echo $point->getX(); // 3.0
echo $point->getY(); // 4.0
echo $point->distanceFromOrigin(); // 5.0
// On PHP 8.2+, you can verify the class is readonly:
$reflection = new ReflectionClass(ImmutablePoint::class);
var_dump($reflection->isReadOnly()); // true
The readonly attribute is compatible with other class attributes:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::flags::ClassFlags;
// Readonly + Final class
#[php_class]
#[php(readonly)]
#[php(flags = ClassFlags::Final)]
pub struct FinalImmutableData {
value: String,
}
fn main() {}
Note: The readonly attribute requires PHP 8.2 or later. Using it when
compiling against an earlier PHP version will result in a compile error.
Conditional Compilation for Multi-Version Support
If your extension needs to support both PHP 8.1 and PHP 8.2+, you can use conditional compilation to only enable readonly on supported versions.
First, add ext-php-rs-build as a build dependency in your Cargo.toml:
[build-dependencies]
ext-php-rs-build = "0.1"
anyhow = "1"
Then create a build.rs that detects the PHP version and emits cfg flags:
use ext_php_rs_build::{find_php, PHPInfo, ApiVersion, emit_php_cfg_flags, emit_check_cfg};
fn main() -> anyhow::Result<()> {
let php = find_php()?;
let info = PHPInfo::get(&php)?;
let version: ApiVersion = info.zend_version()?.try_into()?;
emit_check_cfg();
emit_php_cfg_flags(version);
Ok(())
}
Now you can use #[cfg(php82)] to conditionally apply the readonly attribute:
#[php_class]
#[cfg_attr(php82, php(readonly))]
pub struct MaybeReadonlyClass {
value: String,
}
The ext-php-rs-build crate provides several useful utilities:
find_php()- Locates the PHP executable (respects thePHPenv var)PHPInfo::get()- Runsphp -iand parses the outputApiVersion- Enum representing PHP versions (Php80, Php81, Php82, etc.)emit_php_cfg_flags()- Emitscargo:rustc-cfg=phpXXfor all supported versionsemit_check_cfg()- Emits check-cfg to avoid unknown cfg warnings
This is optional - if your extension only targets PHP 8.2+, you can use
#[php(readonly)] directly without any build script setup.
Implementing Iterator
To make a Rust class usable with PHP’s foreach loop, implement the
Iterator interface.
This requires implementing five methods: current(), key(), next(), rewind(), and valid().
The following example creates a RangeIterator that iterates over a range of integers:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{prelude::*, zend::ce};
#[php_class]
#[php(implements(ce = ce::iterator, stub = "\\Iterator"))]
pub struct RangeIterator {
start: i64,
end: i64,
current: i64,
index: i64,
}
#[php_impl]
impl RangeIterator {
/// Create a new range iterator from start to end (inclusive).
pub fn __construct(start: i64, end: i64) -> Self {
Self {
start,
end,
current: start,
index: 0,
}
}
/// Return the current element.
pub fn current(&self) -> i64 {
self.current
}
/// Return the key of the current element.
pub fn key(&self) -> i64 {
self.index
}
/// Move forward to next element.
pub fn next(&mut self) {
self.current += 1;
self.index += 1;
}
/// Rewind the Iterator to the first element.
pub fn rewind(&mut self) {
self.current = self.start;
self.index = 0;
}
/// Checks if current position is valid.
pub fn valid(&self) -> bool {
self.current <= self.end
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.class::<RangeIterator>()
}
fn main() {}
Using the iterator in PHP:
<?php
$range = new RangeIterator(1, 5);
// Use with foreach
foreach ($range as $key => $value) {
echo "$key => $value\n";
}
// Output:
// 0 => 1
// 1 => 2
// 2 => 3
// 3 => 4
// 4 => 5
// Works with iterator functions
$arr = iterator_to_array(new RangeIterator(10, 12));
// [0 => 10, 1 => 11, 2 => 12]
$count = iterator_count(new RangeIterator(1, 100));
// 100
Iterator with Mixed Types
You can return different types for keys and values. The following example uses string keys:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{prelude::*, zend::ce};
#[php_class]
#[php(implements(ce = ce::iterator, stub = "\\Iterator"))]
pub struct MapIterator {
keys: Vec<String>,
values: Vec<String>,
index: usize,
}
#[php_impl]
impl MapIterator {
pub fn __construct() -> Self {
Self {
keys: vec!["first".into(), "second".into(), "third".into()],
values: vec!["one".into(), "two".into(), "three".into()],
index: 0,
}
}
pub fn current(&self) -> Option<String> {
self.values.get(self.index).cloned()
}
pub fn key(&self) -> Option<String> {
self.keys.get(self.index).cloned()
}
pub fn next(&mut self) {
self.index += 1;
}
pub fn rewind(&mut self) {
self.index = 0;
}
pub fn valid(&self) -> bool {
self.index < self.keys.len()
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.class::<MapIterator>()
}
fn main() {}
<?php
$map = new MapIterator();
foreach ($map as $key => $value) {
echo "$key => $value\n";
}
// Output:
// first => one
// second => two
// third => three
#[php_impl] Attribute
You can export an entire impl block to PHP. This exports all methods as well
as constants to PHP on the class that it is implemented on. This requires the
#[php_class] macro to already be used on the underlying struct. Trait
implementations cannot be exported to PHP. Only one impl block can be exported
per class.
If you do not want a function exported to PHP, you should place it in a separate
impl block.
If you want to use async Rust, use #[php_async_impl], instead: see here » for more info.
Options
By default all constants are renamed to UPPER_CASE and all methods are renamed to
camelCase. This can be changed by passing the change_method_case and
change_constant_case as #[php] attributes on the impl block. The options are:
#[php(change_method_case = "snake_case")]- Renames the method to snake case.#[php(change_constant_case = "snake_case")]- Renames the constant to snake case.
See the name and change_case section for a list of all
available cases.
Methods
Methods basically follow the same rules as functions, so read about the
[php_function] macro first. The primary difference between functions and
methods is they are bounded by their class object.
Class methods can take a &self or &mut self parameter. They cannot take a
consuming self parameter. Static methods can omit this self parameter.
To access the underlying Zend object, you can take a reference to a
ZendClassObject<T> in place of the self parameter, where the parameter must
be named self_. This can also be used to return a reference to $this.
The rest of the options are passed as separate attributes:
#[php(defaults(i = 5, b = "hello"))]- Sets the default value for parameter(s).#[php(optional = i)]- Sets the first optional parameter. Note that this also sets the remaining parameters as optional, so all optional parameters must be a variant ofOption<T>.#[php(vis = "public")],#[php(vis = "protected")]and#[php(vis = "private")]- Sets the visibility of the method.#[php(name = "method_name")]- Renames the PHP method to a different identifier, without renaming the Rust method name.#[php(final)]- Makes the method final (cannot be overridden in subclasses).#[php(abstract)]- Makes the method abstract (must be implemented by subclasses). Can only be used in abstract classes.
The #[php(defaults)] and #[php(optional)] attributes operate the same as the
equivalent function attribute parameters.
Static Methods
Methods that do not take a &self or &mut self parameter are automatically
exported as static methods. These can be called on the class itself without
creating an instance.
#[php_impl]
impl MyClass {
// Static method - no self parameter
pub fn create_default() -> Self {
Self { /* ... */ }
}
// Instance method - takes &self
pub fn get_value(&self) -> i32 {
self.value
}
}
From PHP:
$obj = MyClass::createDefault(); // Static call
$val = $obj->getValue(); // Instance call
Final Methods
Methods marked with #[php(final)] cannot be overridden in subclasses. This is
useful when you want to prevent modification of critical functionality.
use ext_php_rs::prelude::*;
#[php_class]
pub struct SecureClass;
#[php_impl]
impl SecureClass {
#[php(final)]
pub fn secure_method(&self) -> &str {
"This cannot be overridden"
}
// Final static methods are also supported
#[php(final)]
pub fn secure_static() -> i32 {
42
}
}
Abstract Methods
Methods marked with #[php(abstract)] must be implemented by subclasses. Abstract
methods can only be defined in abstract classes (classes with ClassFlags::Abstract).
use ext_php_rs::prelude::*;
use ext_php_rs::flags::ClassFlags;
#[php_class]
#[php(flags = ClassFlags::Abstract)]
pub struct AbstractShape;
#[php_impl]
impl AbstractShape {
// Protected constructor for subclasses
#[php(vis = "protected")]
pub fn __construct() -> Self {
Self
}
// Abstract method - subclasses must implement this.
// The body is never called; use unimplemented!() as a placeholder.
#[php(abstract)]
pub fn area(&self) -> f64 {
unimplemented!()
}
// Concrete method in abstract class
pub fn describe(&self) -> String {
format!("A shape with area {}", self.area())
}
}
Note: Abstract method bodies are never called - they exist only because Rust
syntax requires a body for methods in impl blocks. Use unimplemented!() as a
clear placeholder.
Note: If you try to use #[php(abstract)] on a method in a non-abstract class,
you will get a compile-time error.
Note: PHP does not support abstract static methods. If you need static behavior that can be customized by subclasses, use a regular instance method or the late static binding pattern in PHP.
Constructors
By default, if a class does not have a constructor, it is not constructable from PHP. It can only be returned from a Rust function to PHP.
Constructors are Rust methods which can take any amount of parameters and
returns either Self or Result<Self, E>, where E: Into<PhpException>. When
the error variant of Result is encountered, it is thrown as an exception and
the class is not constructed.
Constructors are designated by either naming the method __construct or by
annotating a method with the #[php(constructor)] attribute. Note that when using
the attribute, the function is not exported to PHP like a regular method.
Constructors cannot use the visibility or rename attributes listed above.
Constants
Constants are defined as regular Rust impl constants. Any type that implements
IntoZval can be used as a constant. Constant visibility is not supported at
the moment, and therefore no attributes are valid on constants.
Property getters and setters
You can add properties to classes which use Rust functions as getters and/or
setters. This is done with the #[php(getter)] and #[php(setter)] attributes. By
default, the get_ or set_ prefix is trimmed from the start of the function
name, and the remainder is used as the property name.
If you want to use a different name for the property, you can pass a name or
change_case option to the #[php] attribute which will change the property name.
Properties do not necessarily have to have both a getter and a setter, if the property is immutable the setter can be omitted, and vice versa for getters.
The #[php(getter)] and #[php(setter)] attributes are mutually exclusive on methods.
Properties cannot have multiple getters or setters, and the property name cannot
conflict with field properties defined on the struct.
As the same as field properties, method property types must implement both
IntoZval and FromZval.
Overriding field properties with getters/setters
If you have a field property defined with #[php(prop)] on your struct, you can
override its access by defining a getter or setter method with the same property
name. The method-based property will take precedence:
use ext_php_rs::prelude::*;
#[php_class]
pub struct Book {
#[php(prop)]
pub title: String, // Direct field access
}
#[php_impl]
impl Book {
pub fn __construct(title: String) -> Self {
Self { title }
}
// This getter overrides $book->title access
#[php(getter)]
pub fn get_title(&self) -> String {
format!("Title: {}", self.title)
}
}
In PHP, accessing $book->title will now call the get_title() method instead
of directly accessing the field:
$book = new Book("The Rust Book");
echo $book->title; // Output: "Title: The Rust Book"
This is useful when you need to add validation, transformation, or side effects to property access while still having the convenience of a public field in Rust.
Example
Continuing on from our Human example in the structs section, we will define a
constructor, as well as getters for the properties. We will also define a
constant for the maximum age of a Human.
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{prelude::*, types::ZendClassObject};
#[php_class]
#[derive(Debug, Default)]
pub struct Human {
name: String,
age: i32,
#[php(prop)]
address: String,
}
#[php_impl]
impl Human {
const MAX_AGE: i32 = 100;
// No `#[constructor]` attribute required here - the name is `__construct`.
pub fn __construct(name: String, age: i32) -> Self {
Self {
name,
age,
address: String::new()
}
}
#[php(getter)]
pub fn get_name(&self) -> String {
self.name.to_string()
}
#[php(setter)]
pub fn set_name(&mut self, name: String) {
self.name = name;
}
#[php(getter)]
pub fn get_age(&self) -> i32 {
self.age
}
pub fn introduce(&self) {
println!("My name is {} and I am {} years old. I live at {}.", self.name, self.age, self.address);
}
pub fn get_raw_obj(self_: &mut ZendClassObject<Human>) -> &mut ZendClassObject<Human> {
dbg!(self_)
}
pub fn get_max_age() -> i32 {
Self::MAX_AGE
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.class::<Human>()
}
fn main() {}
Using our newly created class in PHP:
<?php
$me = new Human('David', 20);
$me->introduce(); // My name is David and I am 20 years old.
var_dump(Human::get_max_age()); // int(100)
var_dump(Human::MAX_AGE); // int(100)
#[php_const] Attribute
Exports a Rust constant as a global PHP constant. The constant can be any type
that implements IntoConst.
The wrap_constant!() macro can be used to simplify the registration of constants.
It sets the name and doc comments for the constant.
You can rename the const with options:
name- Allows you to rename the property, e.g.#[php(name = "new_name")]change_case- Allows you to rename the property using rename rules, e.g.#[php(change_case = PascalCase)]
Examples
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_const]
const TEST_CONSTANT: i32 = 100;
#[php_const]
#[php(name = "I_AM_RENAMED")]
const TEST_CONSTANT_THE_SECOND: i32 = 42;
#[php_const]
const ANOTHER_STRING_CONST: &'static str = "Hello world!";
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.constant(wrap_constant!(TEST_CONSTANT))
.constant(wrap_constant!(TEST_CONSTANT_THE_SECOND))
.constant(("MANUAL_CONSTANT", ANOTHER_STRING_CONST, &[]))
}
fn main() {}
PHP usage
<?php
var_dump(TEST_CONSTANT); // int(100)
var_dump(I_AM_RENAMED); // int(42)
var_dump(MANUAL_CONSTANT); // string(12) "Hello world!"
#[php_extern] Attribute
Attribute used to annotate extern blocks which are deemed as PHP
functions.
This allows you to ‘import’ PHP functions into Rust so that they can be
called like regular Rust functions. Parameters can be any type that
implements IntoZval, and the return type can be anything that implements
[From<Zval>] (notice how Zval is consumed rather than borrowed in this
case).
Unlike most other attributes, this does not need to be placed inside a
#[php_module] block.
Panics
The function can panic when called under a few circumstances:
- The function could not be found or was not callable.
- One of the parameters could not be converted into a
Zval. - The actual function call failed internally.
- The output
Zvalcould not be parsed into the output type.
The last point can be important when interacting with functions that return
unions, such as strpos which can return an integer or a boolean. In this
case, a Zval should be returned as parsing a boolean to an integer is
invalid, and vice versa.
Example
This extern block imports the strpos function from PHP. Notice that
the string parameters can take either [String] or [&str], the optional
parameter offset is an [Option<i64>], and the return value is a Zval
as the return type is an integer-boolean union.
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{
prelude::*,
types::Zval,
};
#[php_extern]
extern "C" {
fn strpos(haystack: &str, needle: &str, offset: Option<i64>) -> Zval;
}
#[php_function]
pub fn my_strpos() {
assert_eq!(unsafe { strpos("Hello", "e", None) }.long(), Some(1));
}
#[php_module]
pub fn module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(my_strpos))
}
fn main() {}
ZvalConvert Derive Macro
The #[derive(ZvalConvert)] macro derives the FromZval and IntoZval traits
on a struct or enum.
Structs
When used on a struct, the FromZendObject and IntoZendObject traits are also
implemented, mapping fields to properties in both directions. All fields on the
struct must implement FromZval as well. Generics are allowed on structs that
use the derive macro, however, the implementation will add a FromZval bound to
all generics types.
Examples
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[derive(ZvalConvert)]
pub struct ExampleClass<'a> {
a: i32,
b: String,
c: &'a str
}
#[php_function]
pub fn take_object(obj: ExampleClass) {
dbg!(obj.a, obj.b, obj.c);
}
#[php_function]
pub fn give_object() -> ExampleClass<'static> {
ExampleClass {
a: 5,
b: "String".to_string(),
c: "Borrowed",
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.function(wrap_function!(take_object))
.function(wrap_function!(give_object))
}
fn main() {}
Calling from PHP:
<?php
$obj = new stdClass;
$obj->a = 5;
$obj->b = 'Hello, world!';
$obj->c = 'another string';
take_object($obj);
var_dump(give_object());
Another example involving generics:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
// T must implement both `PartialEq<i32>` and `FromZval`.
#[derive(Debug, ZvalConvert)]
pub struct CompareVals<T: PartialEq<i32>> {
a: T,
b: T
}
#[php_function]
pub fn take_object(obj: CompareVals<i32>) {
dbg!(obj);
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.function(wrap_function!(take_object))
}
fn main() {}
Enums
When used on an enum, the FromZval implementation will treat the enum as a
tagged union with a mixed datatype. This allows you to accept multiple types in
a parameter, for example, a string and an integer.
The enum variants must not have named fields, and each variant must have exactly one field (the type to extract from the zval). Optionally, the enum may have one default variant with no data contained, which will be used when the rest of the variants could not be extracted from the zval.
The ordering of the variants in the enum is important, as the FromZval
implementation will attempt to parse the zval data in order. For example, if you
put a String variant before an integer variant, the integer would be converted
to a string and passed as the string variant.
Examples
Basic example showing the importance of variant ordering and default field:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[derive(Debug, ZvalConvert)]
pub enum UnionExample<'a> {
Long(u64), // Long
ProperStr(&'a str), // Actual string - not a converted value
ParsedStr(String), // Potentially parsed string, i.e. a double
None // Zval did not contain anything that could be parsed above
}
#[php_function]
pub fn test_union(val: UnionExample) {
dbg!(val);
}
#[php_function]
pub fn give_union() -> UnionExample<'static> {
UnionExample::Long(5)
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.function(wrap_function!(test_union))
.function(wrap_function!(give_union))
}
fn main() {}
Use in PHP:
test_union(5); // UnionExample::Long(5)
test_union("Hello, world!"); // UnionExample::ProperStr("Hello, world!")
test_union(5.66666); // UnionExample::ParsedStr("5.6666")
test_union(null); // UnionExample::None
var_dump(give_union()); // int(5)
#[php] Attributes
There are a number of attributes that can be used to annotate elements in your extension.
Multiple #[php] attributes will be combined. For example, the following will
be identical:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_function]
#[php(name = "hi_world")]
#[php(defaults(a = 1, b = 2))]
fn hello_world(a: i32, b: i32) -> i32 {
a + b
}
fn main() {}
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
#[php_function]
#[php(name = "hi_world", defaults(a = 1, b = 2))]
fn hello_world(a: i32, b: i32) -> i32 {
a + b
}
fn main() {}
Which attributes are available depends on the element you are annotating:
| Attribute | const | fn | struct | struct field | impl | impl const | impl fn | enum | enum case |
|---|---|---|---|---|---|---|---|---|---|
| name | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
| change_case | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
| change_method_case | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| change_constant_case | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| flags | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| prop | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| extends | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| implements | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| modifier | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| defaults | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
| optional | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
| vis | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
| getter | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
| setter | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
| constructor | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
| abstract_method | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
| allow_native_discriminants | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
| discriminant | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
name and change_case
name and change_case are mutually exclusive. The name attribute is used to set the name of
an item to a string literal. The change_case attribute is used to change the case of the name.
#[php(name = "NEW_NAME")]
#[php(change_case = snake_case)]]
Available cases are:
snake_casePascalCasecamelCaseUPPER_CASEnone- No change
Exceptions
Exceptions can be thrown from Rust to PHP. The inverse (catching a PHP exception in Rust) is currently being worked on.
Throwing exceptions
PhpException is the type that represents an exception. It contains the
message contained in the exception, the type of exception and a status code to
go along with the exception.
You can create a new exception with the new(), default(), or
from_class::<T>() methods. Into<PhpException> is implemented for String
and &str, which creates an exception of the type Exception with a code of 0.
It may be useful to implement Into<PhpException> for your error type.
Calling the throw() method on a PhpException attempts to throw the exception
in PHP. This function can fail if the type of exception is invalid (i.e. does
not implement Exception or Throwable). Upon success, nothing will be
returned.
IntoZval is also implemented for Result<T, E>, where T: IntoZval and
E: Into<PhpException>. If the result contains the error variant, the exception
is thrown. This allows you to return a result from a PHP function annotated with
the #[php_function] attribute.
Examples
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use std::convert::TryInto;
// Trivial example - PHP represents all integers as `u64` on 64-bit systems
// so the `u32` would be converted back to `u64`, but that's okay for an example.
#[php_function]
pub fn something_fallible(n: u64) -> PhpResult<u32> {
let n: u32 = n.try_into().map_err(|_| "Could not convert into u32")?;
Ok(n)
}
#[php_module]
pub fn module(module: ModuleBuilder) -> ModuleBuilder {
module
}
fn main() {}
Output
ext-php-rs provides several macros and functions for writing output to PHP’s
stdout and stderr streams. These are essential when your extension needs to
produce output that integrates with PHP’s output buffering system.
Text Output
For regular text output (strings without NUL bytes), use the php_print! and
php_println! macros. These work similarly to Rust’s print! and println!
macros.
php_print!
Prints to PHP’s standard output without a trailing newline.
use ext_php_rs::prelude::*;
#[php_function]
pub fn greet(name: &str) {
php_print!("Hello, {}!", name);
}
php_println!
Prints to PHP’s standard output with a trailing newline.
use ext_php_rs::prelude::*;
#[php_function]
pub fn greet(name: &str) {
php_println!("Hello, {}!", name);
}
Note:
php_print!andphp_println!will panic if the string contains NUL bytes (\0). For binary-safe output, usephp_output!orphp_write!.
Binary-Safe Output
When working with binary data that may contain NUL bytes, use the binary-safe
output functions. These are essential for outputting raw bytes, binary file
contents, or any data that might contain \0 characters.
php_output!
Writes binary data to PHP’s output stream. This macro is both binary-safe AND
respects PHP’s output buffering (ob_start()). This is usually what you want
for binary output.
use ext_php_rs::prelude::*;
#[php_function]
pub fn output_binary() -> i64 {
// Write binary data with NUL bytes - will be captured by ob_start()
let bytes_written = php_output!(b"Hello\x00World");
bytes_written as i64
}
php_write!
Writes binary data directly to the SAPI output, bypassing PHP’s output
buffering. This macro is binary-safe but output will NOT be captured by
ob_start(). The “ub” in ub_write stands for “unbuffered”.
use ext_php_rs::prelude::*;
#[php_function]
pub fn output_binary() -> i64 {
// Write a byte literal
php_write!(b"Hello World").expect("write failed");
// Write binary data with NUL bytes (would panic with php_print!)
let bytes_written = php_write!(b"Hello\x00World").expect("write failed");
// Write a byte slice
let data: &[u8] = &[0x48, 0x65, 0x6c, 0x6c, 0x6f]; // "Hello"
php_write!(data).expect("write failed");
bytes_written as i64
}
The macro returns a Result<usize> with the number of bytes written, which can
be useful for verifying that all data was output successfully. The error case
occurs when the SAPI’s ub_write function is not available.
Function API
In addition to macros, you can use the underlying functions directly:
| Function | Binary-Safe | Output Buffering | Description |
|---|---|---|---|
zend::printf() | No | Yes | Printf-style output (used by php_print!) |
zend::output_write() | Yes | Yes | Binary-safe buffered output |
zend::write() | Yes | No | Binary-safe unbuffered output |
Example using functions directly
use ext_php_rs::zend::output_write;
fn output_data(data: &[u8]) {
let bytes_written = output_write(data);
if bytes_written != data.len() {
eprintln!("Warning: incomplete write");
}
}
Comparison
| Macro | Binary-Safe | Output Buffering | Supports Formatting |
|---|---|---|---|
php_print! | No | Yes | Yes |
php_println! | No | Yes | Yes |
php_output! | Yes | Yes | No |
php_write! | Yes | No | No |
When to Use Each
-
php_print!/php_println!: Use for text output with format strings, similar to Rust’sprint!andprintln!. Best for human-readable messages. -
php_output!: Use for binary data that needs to work with PHP’s output buffering. This is the recommended choice for most binary output needs. -
php_write!: Use when you need direct, unbuffered output that bypasses PHP’s output layer. Useful for low-level SAPI interaction or when output buffering must be avoided.
INI Settings
Your PHP Extension may want to provide it’s own PHP INI settings to configure behaviour. This can be done in the #[php_startup] annotated startup function.
Registering INI Settings
All PHP INI definitions must be registered with PHP to get / set their values via the php.ini file or ini_get() / ini_set().
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::zend::IniEntryDef;
use ext_php_rs::flags::IniEntryPermission;
pub fn startup(ty: i32, mod_num: i32) -> i32 {
let ini_entries: Vec<IniEntryDef> = vec![
IniEntryDef::new(
"my_extension.display_emoji".to_owned(),
"yes".to_owned(),
&IniEntryPermission::All,
),
];
IniEntryDef::register(ini_entries, mod_num);
0
}
#[php_module]
#[php(startup = "startup")]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
}
fn main() {}
Getting INI Settings
The INI values are stored as part of the GlobalExecutor, and can be accessed via the ini_values() function. To retrieve the value for a registered INI setting
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::{
prelude::*,
zend::ExecutorGlobals,
};
pub fn startup(ty: i32, mod_num: i32) -> i32 {
// Get all INI values
let ini_values = ExecutorGlobals::get().ini_values(); // HashMap<String, Option<String>>
let my_ini_value = ini_values.get("my_extension.display_emoji"); // Option<Option<String>>
0
}
#[php_module]
#[php(startup = "startup")]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
}
fn main() {}
Superglobals
PHP provides several superglobal arrays that are accessible from any scope. In
ext-php-rs, you can access these superglobals using the ProcessGlobals,
SapiGlobals, and ExecutorGlobals types.
Note for FrankenPHP users: In FrankenPHP worker mode, you would need to override
sapi_activate/sapi_deactivatehooks to mimicphp_rinit/php_rshutdownbehavior. However, superglobals are inaccessible duringsapi_activateand will cause crashes if accessed at that point.
Accessing HTTP Superglobals
The ProcessGlobals type provides access to the common HTTP superglobals:
| Method | PHP Equivalent |
|---|---|
http_get_vars() | $_GET |
http_post_vars() | $_POST |
http_cookie_vars() | $_COOKIE |
http_server_vars() | $_SERVER |
http_env_vars() | $_ENV |
http_files_vars() | $_FILES |
http_request_vars() | $_REQUEST |
Basic Example
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::zend::ProcessGlobals;
#[php_function]
pub fn get_cookie(name: String) -> Option<String> {
ProcessGlobals::get()
.http_cookie_vars()
.get(name.as_str())
.and_then(|zval| zval.string())
}
#[php_function]
pub fn get_query_param(name: String) -> Option<String> {
ProcessGlobals::get()
.http_get_vars()
.get(name.as_str())
.and_then(|zval| zval.string())
}
#[php_function]
pub fn get_post_param(name: String) -> Option<String> {
ProcessGlobals::get()
.http_post_vars()
.get(name.as_str())
.and_then(|zval| zval.string())
}
fn main() {}
Accessing $_SERVER
The $_SERVER superglobal is lazy-initialized in PHP, so http_server_vars()
returns an Option:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::zend::ProcessGlobals;
#[php_function]
pub fn get_request_method() -> Option<String> {
ProcessGlobals::get()
.http_server_vars()?
.get("REQUEST_METHOD")
.and_then(|zval| zval.string())
}
#[php_function]
pub fn get_remote_addr() -> Option<String> {
ProcessGlobals::get()
.http_server_vars()?
.get("REMOTE_ADDR")
.and_then(|zval| zval.string())
}
#[php_function]
pub fn get_user_agent() -> Option<String> {
ProcessGlobals::get()
.http_server_vars()?
.get("HTTP_USER_AGENT")
.and_then(|zval| zval.string())
}
fn main() {}
Working with $_FILES
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::zend::ProcessGlobals;
#[php_function]
pub fn get_uploaded_file_name(field: String) -> Option<String> {
let globals = ProcessGlobals::get();
let files = globals.http_files_vars();
// $_FILES structure: $_FILES['field']['name'], ['tmp_name'], ['size'], etc.
files
.get(field.as_str())?
.array()?
.get("name")
.and_then(|zval| zval.string())
}
#[php_function]
pub fn get_uploaded_file_tmp_path(field: String) -> Option<String> {
let globals = ProcessGlobals::get();
let files = globals.http_files_vars();
files
.get(field.as_str())?
.array()?
.get("tmp_name")
.and_then(|zval| zval.string())
}
fn main() {}
Returning Superglobals to PHP
You can return copies of superglobals back to PHP:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::boxed::ZBox;
use ext_php_rs::types::ZendHashTable;
use ext_php_rs::zend::ProcessGlobals;
#[php_function]
pub fn get_all_cookies() -> ZBox<ZendHashTable> {
ProcessGlobals::get().http_cookie_vars().to_owned()
}
#[php_function]
pub fn get_all_get_params() -> ZBox<ZendHashTable> {
ProcessGlobals::get().http_get_vars().to_owned()
}
#[php_function]
pub fn get_server_vars() -> Option<ZBox<ZendHashTable>> {
Some(ProcessGlobals::get().http_server_vars()?.to_owned())
}
fn main() {}
SAPI Request Information
For lower-level request information, use SapiGlobals:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::zend::SapiGlobals;
#[php_function]
pub fn get_request_info() -> Vec<String> {
let globals = SapiGlobals::get();
let request_info = globals.request_info();
let mut info = Vec::new();
if let Some(method) = request_info.request_method() {
info.push(format!("Method: {}", method));
}
if let Some(uri) = request_info.request_uri() {
info.push(format!("URI: {}", uri));
}
if let Some(query) = request_info.query_string() {
info.push(format!("Query: {}", query));
}
if let Some(content_type) = request_info.content_type() {
info.push(format!("Content-Type: {}", content_type));
}
info.push(format!("Content-Length: {}", request_info.content_length()));
info
}
fn main() {}
Available Request Info Methods
| Method | Description |
|---|---|
request_method() | HTTP method (GET, POST, etc.) |
request_uri() | Request URI |
query_string() | Query string |
cookie_data() | Raw cookie data |
content_type() | Content-Type header |
content_length() | Content-Length value |
path_translated() | Translated filesystem path |
auth_user() | HTTP Basic auth username |
auth_password() | HTTP Basic auth password |
Accessing Constants
Use ExecutorGlobals to access PHP constants:
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::zend::ExecutorGlobals;
#[php_function]
pub fn get_php_version_constant() -> Option<String> {
let globals = ExecutorGlobals::get();
globals
.constants()?
.get("PHP_VERSION")
.and_then(|zval| zval.string())
}
#[php_function]
pub fn constant_exists(name: String) -> bool {
let globals = ExecutorGlobals::get();
globals
.constants()
.is_some_and(|c| c.get(name.as_str()).is_some())
}
fn main() {}
Accessing INI Values
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::zend::ExecutorGlobals;
#[php_function]
pub fn get_memory_limit() -> Option<String> {
ExecutorGlobals::get()
.ini_values()
.get("memory_limit")
.cloned()
.flatten()
}
#[php_function]
pub fn get_all_ini_values() -> Vec<(String, String)> {
ExecutorGlobals::get()
.ini_values()
.iter()
.filter_map(|(k, v)| {
v.as_ref().map(|val| (k.clone(), val.clone()))
})
.collect()
}
fn main() {}
Thread Safety
All global access methods use guard types that provide thread-safe access:
ProcessGlobals::get()returns aGlobalReadGuard<ProcessGlobals>ExecutorGlobals::get()returns aGlobalReadGuard<ExecutorGlobals>SapiGlobals::get()returns aGlobalReadGuard<SapiGlobals>
The guard is automatically released when it goes out of scope. For mutable
access (rarely needed), use get_mut():
#![cfg_attr(windows, feature(abi_vectorcall))]
extern crate ext_php_rs;
use ext_php_rs::zend::ExecutorGlobals;
fn example() {
// Read-only access (most common)
let globals = ExecutorGlobals::get();
// ... use globals ...
// Guard released when `globals` goes out of scope
// Mutable access (rarely needed)
let mut globals = ExecutorGlobals::get_mut();
// ... modify globals ...
}
fn main() {}
PHP Example
<?php
// Assuming you've registered the functions above
// Access cookies
$session_id = get_cookie('session_id');
// Access query parameters
$page = get_query_param('page') ?? '1';
// Get request method
$method = get_request_method(); // "GET", "POST", etc.
// Get all cookies as array
$all_cookies = get_all_cookies();
print_r($all_cookies);
// Get request info
$info = get_request_info();
print_r($info);
// Access constants
$php_version = get_php_version_constant();
echo "PHP Version: $php_version\n";
// Check if constant exists
if (constant_exists('MY_CUSTOM_CONSTANT')) {
echo "Constant exists!\n";
}
Summary
| Type | Use Case |
|---|---|
ProcessGlobals | HTTP superglobals ($_GET, $_POST, $_COOKIE, etc.) |
SapiGlobals | Low-level request info, headers |
ExecutorGlobals | Constants, INI values, function/class tables |
All types are accessed via ::get() for read access or ::get_mut() for write
access, and provide thread-safe access through guard types.
#[php_async_impl] Attribute
Using #[php_async_impl] instead of #[php_impl] allows us to expose any async Rust library to PHP, using PHP fibers, php-tokio and the PHP Revolt event loop under the hood to handle async interoperability.
This allows full compatibility with amphp, PSL, reactphp and any other async PHP library based on Revolt.
Traits annotated with #[php_async_impl] can freely expose any async function, using await and any async Rust library.
Make sure to also expose the php_tokio::EventLoop::init and php_tokio::EventLoop::wakeup functions to PHP in order to initialize the event loop, as specified in the full example here ».
Also, make sure to invoke EventLoop::shutdown in the request shutdown handler to clean up the tokio event loop before finishing the request.
Async example
In this example, we’re exposing an async Rust HTTP client library called reqwest to PHP, using PHP fibers, php-tokio and the PHP Revolt event loop under the hood to handle async interoperability.
This allows full compatibility with amphp, PSL, reactphp and any other async PHP library based on Revolt.
Make sure to require php-tokio as a dependency before proceeding.
extern crate ext_php_rs;
extern crate php_tokio;
extern crate reqwest;
use ext_php_rs::prelude::*;
use php_tokio::{php_async_impl, EventLoop};
#[php_class]
struct Client {}
#[php_async_impl]
impl Client {
pub fn init() -> PhpResult<u64> {
EventLoop::init()
}
pub fn wakeup() -> PhpResult<()> {
EventLoop::wakeup()
}
pub async fn get(url: &str) -> anyhow::Result<String> {
Ok(reqwest::get(url).await?.text().await?)
}
}
pub extern "C" fn request_shutdown(_type: i32, _module_number: i32) -> i32 {
EventLoop::shutdown();
0
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.request_shutdown_function(request_shutdown)
}
Here’s the async PHP code we use to interact with the Rust class we just exposed.
The Client::init method needs to be called only once in order to initialize the Revolt event loop and link it to the Tokio event loop, as shown by the following code.
See here » for more info on async PHP using amphp + revolt.
<?php declare(strict_types=1);
namespace Reqwest;
use Revolt\EventLoop;
use function Amp\async;
use function Amp\Future\await;
final class Client
{
private static ?string $id = null;
public static function init(): void
{
if (self::$id !== null) {
return;
}
$f = \fopen("php://fd/".\Client::init(), 'r+');
\stream_set_blocking($f, false);
self::$id = EventLoop::onReadable($f, fn () => \Client::wakeup());
}
public static function reference(): void
{
EventLoop::reference(self::$id);
}
public static function unreference(): void
{
EventLoop::unreference(self::$id);
}
public static function __callStatic(string $name, array $args): mixed
{
return \Client::$name(...$args);
}
}
Client::init();
function test(int $delay): void
{
$url = "https://httpbin.org/delay/$delay";
$t = time();
echo "Making async reqwest to $url that will return after $delay seconds...".PHP_EOL;
Client::get($url);
$t = time() - $t;
echo "Got response from $url after ~".$t." seconds!".PHP_EOL;
};
$futures = [];
$futures []= async(test(...), 5);
$futures []= async(test(...), 5);
$futures []= async(test(...), 5);
await($futures);
Result:
Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds...
Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds...
Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds...
Got response from https://httpbin.org/delay/5 after ~5 seconds!
Got response from https://httpbin.org/delay/5 after ~5 seconds!
Got response from https://httpbin.org/delay/5 after ~5 seconds!
Bailout Guard
When PHP triggers a “bailout” (via exit(), die(), or a fatal error), it uses
longjmp to unwind the stack. This bypasses Rust’s normal drop semantics,
meaning destructors for stack-allocated values won’t run. This can lead to
resource leaks for things like file handles, network connections, or locks.
The Problem
Consider this code:
#[php_function]
pub fn process_file(callback: ZendCallable) {
let file = File::open("data.txt").unwrap();
// If callback calls exit(), the file handle leaks!
callback.try_call(vec![]);
// file.drop() never runs
}
If the PHP callback triggers exit(), the File handle is never closed because
longjmp skips Rust’s destructor calls.
Solution 1: Using try_call
The simplest solution is to use try_call for PHP callbacks. It catches bailouts
internally and returns normally, allowing Rust destructors to run:
#[php_function]
pub fn process_file(callback: ZendCallable) {
let file = File::open("data.txt").unwrap();
// try_call catches bailout, function returns, file is dropped
let result = callback.try_call(vec![]);
if result.is_err() {
// Bailout occurred, but file will still be closed
// when this function returns
}
}
Solution 2: Using BailoutGuard
For cases where you need guaranteed cleanup even if bailout occurs directly
(not through try_call), use BailoutGuard:
use ext_php_rs::prelude::*;
use std::fs::File;
#[php_function]
pub fn process_file(callback: ZendCallable) {
// Wrap the file handle in BailoutGuard
let file = BailoutGuard::new(File::open("data.txt").unwrap());
// Even if bailout occurs, the file will be closed
callback.try_call(vec![]);
// Use the file via Deref
// file.read_to_string(...);
}
How BailoutGuard Works
-
Heap allocation: The wrapped value is heap-allocated so it survives the
longjmpstack unwinding. -
Cleanup registration: A cleanup callback is registered in thread-local storage when the guard is created.
-
On normal drop: The cleanup is cancelled and the value is dropped normally.
-
On bailout: Before re-triggering the bailout, all registered cleanup callbacks are executed, dropping the guarded values.
API
// Create a guard
let guard = BailoutGuard::new(value);
// Access the value (implements Deref and DerefMut)
guard.do_something();
let inner: &T = &*guard;
let inner_mut: &mut T = &mut *guard;
// Explicitly get references
let inner: &T = guard.get();
let inner_mut: &mut T = guard.get_mut();
// Extract the value, cancelling cleanup
let value: T = guard.into_inner();
Performance Note
BailoutGuard incurs a heap allocation. Only use it for values that absolutely
must be cleaned up, such as:
- File handles
- Network connections
- Database connections
- Locks and mutexes
- Other system resources
For simple values without cleanup requirements, the overhead isn’t worth it.
Nested Calls
BailoutGuard works correctly with nested function calls. Guards at all
nesting levels are cleaned up when bailout occurs:
#[php_function]
pub fn outer_function(callback: ZendCallable) {
let _outer_resource = BailoutGuard::new(Resource::new());
inner_function(&callback);
}
fn inner_function(callback: &ZendCallable) {
let _inner_resource = BailoutGuard::new(Resource::new());
// If bailout occurs here, both inner and outer resources are cleaned up
callback.try_call(vec![]);
}
Best Practices
-
Prefer
try_call: For most cases, usingtry_calland handling the error result is simpler and doesn’t require heap allocation. -
Use
BailoutGuardfor critical resources: Only wrap values that absolutely must be cleaned up (connections, locks, etc.). -
Don’t overuse: Not every value needs to be wrapped. Simple data structures without cleanup requirements don’t need
BailoutGuard. -
Combine approaches: Use
try_callwhere possible andBailoutGuardfor critical resources that must be cleaned up regardless.
Observer API
The Observer API allows you to build profilers, tracers, and instrumentation tools that observe PHP function calls and errors. This is useful for:
- Performance profiling
- Request tracing (APM)
- Error monitoring
- Code coverage tools
- Debugging tools
Enabling the Feature
The Observer API is behind a feature flag. Add it to your Cargo.toml:
[dependencies]
ext-php-rs = { version = "0.15", features = ["observer"] }
Function Call Observer
Implement the FcallObserver trait to observe function calls:
use ext_php_rs::prelude::*;
use ext_php_rs::types::Zval;
use ext_php_rs::zend::ExecuteData;
use std::sync::atomic::{AtomicU64, Ordering};
struct CallCounter {
count: AtomicU64,
}
impl CallCounter {
fn new() -> Self {
Self {
count: AtomicU64::new(0),
}
}
}
impl FcallObserver for CallCounter {
fn should_observe(&self, info: &FcallInfo) -> bool {
!info.is_internal
}
fn begin(&self, _execute_data: &ExecuteData) {
self.count.fetch_add(1, Ordering::Relaxed);
}
fn end(&self, _execute_data: &ExecuteData, _retval: Option<&Zval>) {}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.fcall_observer(CallCounter::new)
}
The FcallObserver Trait
| Method | Description |
|---|---|
should_observe(&self, info: &FcallInfo) -> bool | Called once per function definition. Result is cached by PHP. |
begin(&self, execute_data: &ExecuteData) | Called when function begins execution. |
end(&self, execute_data: &ExecuteData, retval: Option<&Zval>) | Called when function ends (even on exceptions). |
FcallInfo - Function Metadata
| Field | Type | Description |
|---|---|---|
function_name | Option<&str> | Function name (None for anonymous/main) |
class_name | Option<&str> | Class name for methods |
filename | Option<&str> | Source file (None for internal functions) |
lineno | u32 | Line number (0 for internal functions) |
is_internal | bool | True for built-in PHP functions |
Error Observer
Implement the ErrorObserver trait to observe PHP errors:
use ext_php_rs::prelude::*;
use std::sync::atomic::{AtomicU64, Ordering};
struct ErrorTracker {
fatal_count: AtomicU64,
warning_count: AtomicU64,
}
impl ErrorTracker {
fn new() -> Self {
Self {
fatal_count: AtomicU64::new(0),
warning_count: AtomicU64::new(0),
}
}
}
impl ErrorObserver for ErrorTracker {
fn should_observe(&self, error_type: ErrorType) -> bool {
(ErrorType::FATAL | ErrorType::WARNING).contains(error_type)
}
fn on_error(&self, error: &ErrorInfo) {
if ErrorType::FATAL.contains(error.error_type) {
self.fatal_count.fetch_add(1, Ordering::Relaxed);
if let Some(trace) = error.backtrace() {
for frame in trace {
eprintln!(" at {}:{}",
frame.file.as_deref().unwrap_or("<internal>"),
frame.line
);
}
}
} else {
self.warning_count.fetch_add(1, Ordering::Relaxed);
}
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.error_observer(ErrorTracker::new)
}
The ErrorObserver Trait
| Method | Description |
|---|---|
should_observe(&self, error_type: ErrorType) -> bool | Filter which error types to observe. |
on_error(&self, error: &ErrorInfo) | Called when an observed error occurs. |
ErrorType - Error Level Bitflags
ErrorType::ERROR // E_ERROR
ErrorType::WARNING // E_WARNING
ErrorType::PARSE // E_PARSE
ErrorType::NOTICE // E_NOTICE
ErrorType::CORE_ERROR // E_CORE_ERROR
ErrorType::CORE_WARNING // E_CORE_WARNING
ErrorType::COMPILE_ERROR // E_COMPILE_ERROR
ErrorType::COMPILE_WARNING // E_COMPILE_WARNING
ErrorType::USER_ERROR // E_USER_ERROR
ErrorType::USER_WARNING // E_USER_WARNING
ErrorType::USER_NOTICE // E_USER_NOTICE
ErrorType::RECOVERABLE_ERROR // E_RECOVERABLE_ERROR
ErrorType::DEPRECATED // E_DEPRECATED
ErrorType::USER_DEPRECATED // E_USER_DEPRECATED
// Convenience groups
ErrorType::ALL // All error types
ErrorType::FATAL // ERROR | CORE_ERROR | COMPILE_ERROR | USER_ERROR | RECOVERABLE_ERROR | PARSE
ErrorType::CORE // CORE_ERROR | CORE_WARNING
ErrorInfo - Error Metadata
| Field | Type | Description |
|---|---|---|
error_type | ErrorType | The error level/severity |
filename | Option<&str> | Source file where error occurred |
lineno | u32 | Line number |
message | &str | The error message |
Lazy Backtrace
The backtrace() method captures the PHP call stack on demand:
fn on_error(&self, error: &ErrorInfo) {
if let Some(trace) = error.backtrace() {
for frame in trace {
println!("{}::{}() at {}:{}",
frame.class.as_deref().unwrap_or(""),
frame.function.as_deref().unwrap_or("<main>"),
frame.file.as_deref().unwrap_or("<internal>"),
frame.line
);
}
}
}
The backtrace is only captured when called, so there’s zero cost if unused.
Exception Observer
Implement the ExceptionObserver trait to observe thrown PHP exceptions:
use ext_php_rs::prelude::*;
use std::sync::atomic::{AtomicU64, Ordering};
struct ExceptionTracker {
exception_count: AtomicU64,
}
impl ExceptionTracker {
fn new() -> Self {
Self {
exception_count: AtomicU64::new(0),
}
}
}
impl ExceptionObserver for ExceptionTracker {
fn on_exception(&self, exception: &ExceptionInfo) {
self.exception_count.fetch_add(1, Ordering::Relaxed);
eprintln!("[EXCEPTION] {}: {} at {}:{}",
exception.class_name,
exception.message.as_deref().unwrap_or("<no message>"),
exception.file.as_deref().unwrap_or("<unknown>"),
exception.line
);
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.exception_observer(ExceptionTracker::new)
}
The ExceptionObserver Trait
| Method | Description |
|---|---|
on_exception(&self, exception: &ExceptionInfo) | Called when an exception is thrown, before any catch blocks. |
ExceptionInfo - Exception Metadata
| Field | Type | Description |
|---|---|---|
class_name | String | Exception class name (e.g., “RuntimeException”) |
message | Option<String> | The exception message |
code | i64 | The exception code |
file | Option<String> | Source file where thrown |
line | u32 | Line number where thrown |
Exception Backtrace
The backtrace() method captures the PHP call stack at exception throw time:
impl ExceptionObserver for MyObserver {
fn on_exception(&self, exception: &ExceptionInfo) {
eprintln!("[EXCEPTION] {}: {}",
exception.class_name,
exception.message.as_deref().unwrap_or("<no message>")
);
if let Some(trace) = exception.backtrace() {
for frame in trace {
eprintln!(" at {}::{}() in {}:{}",
frame.class.as_deref().unwrap_or(""),
frame.function.as_deref().unwrap_or("<main>"),
frame.file.as_deref().unwrap_or("<internal>"),
frame.line
);
}
}
}
}
The backtrace is lazy - only captured when called, so there’s zero cost if unused.
BacktraceFrame - Stack Frame Metadata
| Field | Type | Description |
|---|---|---|
function | Option<String> | Function name (None for main script) |
class | Option<String> | Class name for method calls |
file | Option<String> | Source file |
line | u32 | Line number |
Using All Observers
You can register all observers on the same module:
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.fcall_observer(MyProfiler::new)
.error_observer(MyErrorTracker::new)
.exception_observer(MyExceptionTracker::new)
}
Thread Safety
Observers are created once during MINIT and stored as global singletons.
They must implement Send + Sync because:
- NTS: A single instance handles all requests
- ZTS: The same instance may be called from different threads
Use thread-safe primitives like AtomicU64, Mutex, or RwLock for mutable state.
Best Practices
-
Keep observers lightweight: Observer methods are called frequently. Avoid heavy computations or I/O.
-
Use filtering wisely:
should_observeresults are cached for fcall observers. For error observers, filter early to avoid unnecessary processing. -
Handle errors gracefully: Don’t panic in observer methods.
-
Consider memory usage: Implement limits or periodic flushing to avoid unbounded memory growth.
-
Use lazy backtrace: Only call
backtrace()when needed. BothErrorInfoandExceptionInfosupport lazy backtrace capture.
Limitations
- Only one fcall observer can be registered per extension
- Only one error observer can be registered per extension
- Only one exception observer can be registered per extension
- Observers are registered globally for the entire PHP process
Allowed Bindings
The extension limits the bindings that are generated by bindgen to a subset of the original bindings.
Those bindings are defined in the allowed_bindings.rs file.
Should you need to add more bindings, you can do so by defining them as a comma-separated list in the EXT_PHP_RS_ALLOWED_BINDINGS environment variable.
This can be configured in your .cargo/config.toml file:
[env]
EXT_PHP_RS_ALLOWED_BINDINGS = "php_foo,php_bar"
Your bindings should now appear in the ext_php_rs::ffi module.
Be aware, that bindings that do not exist in the PHP version you are targeting will not be available.
Some bindings may also change between PHP versions, so make sure to test your extension with all PHP versions you are targeting.
Contributing
If you think that a binding should be added to the allowed bindings, please open an issue or a pull request on the GitHub repository so that everyone can benefit from it.
Migrating to v0.16
Void Return Type for Functions Without Explicit Return
Functions and methods without an explicit Rust return type now declare void as their PHP return type.
Before (v0.15)
#[php_function]
pub fn do_something() {
println!("Hello");
}
Generated PHP signature: function do_something() (implicit mixed return)
After (v0.16)
The same Rust code now generates: function do_something(): void
Migration
If your function actually returns a value but didn’t declare it, you must now add the return type:
// Before: worked but was incorrect
#[php_function]
pub fn get_value() {
42 // implicitly returned, but no declared type
}
// After: must declare return type
#[php_function]
pub fn get_value() -> i32 {
42
}
Exceptions
The magic methods __destruct and __clone are excluded from this change, as PHP forbids return type declarations on them.
Migrating to v0.14
New Macro Transition
The old macro system used a global state to be able to automatically register
functions and classes when the #[php_module] attribute is used. However,
global state can cause problems with incremental compilation and is not
recommended.
To solve this, the macro system has been re-written but this will require changes to user code. This document summarises the changes.
There is no real changes on existing macros, however you will now need to register functions, classes, constants and startup function when declaring the module.
#[php_module]
#[php(startup = "startup_function")]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.class::<TestClass>()
.function(wrap_function!(hello_world))
.constant(wrap_constant!(SOME_CONSTANT))
}
Functions
Mostly unchanged in terms of function definition, however you now need to register the function with the module builder:
use ext_php_rs::prelude::*;
#[php_function]
pub fn hello_world() -> &'static str {
"Hello, world!"
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.function(wrap_function!(hello_world))
}
Supported #[php] attributes:
#[php(name = "NEW_NAME")]- Renames the function#[php(change_case = case)]- Changes the case of the function name#[php(vis = "public")]- Changes the visibility of the function#[php(defaults(a = 5, test = 100))]- Sets default values for function arguments#[php(variadic)]- Marks the function as variadic. The last argument must be a&[&Zval]
Classes
Mostly unchanged in terms of the class and impl definitions, however you now need to register the classes with the module builder:
use ext_php_rs::prelude::*;
#[php_class]
#[derive(Debug)]
pub struct TestClass {
#[php(prop)]
a: i32,
#[php(prop)]
b: i32,
}
#[php_impl]
impl TestClass {
#[php(name = "NEW_CONSTANT_NAME")]
pub const SOME_CONSTANT: i32 = 5;
pub const SOME_OTHER_STR: &'static str = "Hello, world!";
pub fn __construct(a: i32, b: i32) -> Self {
Self { a: a + 10, b: b + 10 }
}
#[php(defaults(a = 5, test = 100))]
pub fn test_camel_case(&self, a: i32, test: i32) {
println!("a: {} test: {}", a, test);
}
fn x(&self) -> i32 {
5
}
pub fn builder_pattern(
self_: &mut ZendClassObject<TestClass>,
) -> &mut ZendClassObject<TestClass> {
dbg!(self_)
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.class::<TestClass>()
}
Supported #[php] attributes (struct):
#[php(name = "NEW_NAME")]- Renames the class#[php(change_case = case)]- Changes the case of the class name#[php(vis = "public")]- Changes the visibility of the class#[php(extends(ce = ce_fn, stub = "ParentClass")]- Extends a parent class#[php(implements(ce = ce_fn, stub = "Interface"))]- Implements an interface#[php(prop)]- Marks a field as a property
Supported #[php] attributes (impl):
#[php(change_constant_case = case)]- Changes the case of the constant names. Can be overridden by attributes on the constants.#[php(change_method_case = case)]- Changes the case of the method names. Can be overridden by attributes on the methods.
For elements in the #[php_impl] block see the respective function and constant attributes.
Extends and Implements
Extends and implements are now taking a second parameter which is the
stub name. This is the name of the class or interface in PHP.
This value is only used for stub generation and is not used for the class name in Rust.
Constants
Mostly unchanged in terms of constant definition, however you now need to register the constant with the module builder:
use ext_php_rs::prelude::*;
#[php_const]
const SOME_CONSTANT: i32 = 100;
#[php_const]
#[php(name = "HELLO_WORLD")]
const SOME_OTHER_CONSTANT: &'static str = "Hello, world!";
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.constant(wrap_constant!(SOME_CONSTANT)) // SOME_CONSTANT = 100
.constant(wrap_constant!(SOME_OTHER_CONSTANT)) // HELLO_WORLD = "Hello, world!"
.constant(("CONST_NAME", SOME_CONSTANT, &[])) // CONST_NAME = 100
}
Supported #[php] attributes:
#[php(name = "NEW_NAME")]- Renames the constant#[php(change_case = case)]- Changes the case of the constant name#[php(vis = "public")]- Changes the visibility of the constant
Extern
No changes.
use ext_php_rs::prelude::*;
#[php_extern]
extern "C" {
fn phpinfo() -> bool;
}
fn some_rust_func() {
let x = unsafe { phpinfo() };
println!("phpinfo: {x}");
}
Startup Function
The #[php_startup] macro has been deprecated. Instead, define a function with
the signature fn(ty: i32, mod_num: i32) -> i32 and provide the function name
use ext_php_rs::prelude::*;
fn startup_function(ty: i32, mod_num: i32) -> i32 {
0
}
#[php_module]
#[php(startup = "startup_function")]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
}
#[php] Attributes
Attributes like #[rename] or #[prop] have been moved to the #[php] attribute.
The #[php] attribute on an item are combined with each other. This means that
the following variants are equivalent:
#[php(change_case = case)]
#[php(vis = "public")]
#[php(change_case = case, vis = "public")]
Renaming and Case Changes
Default case was adjusted to match PSR standards:
- Class names are now
PascalCase - Property names are now
camelCase - Method names are now
camelCase - Constant names are now
UPPER_CASE - Function names are now
snake_case
This can be changed using the change_case attribute on the item.
Additionally, the change_method_case and change_constant_case attributes can be used
to change the case of all methods and constants in a class.
name vs change_case
Previously the (re)name parameter was used to rename items. This has been
unified to use name to set the name of an item to a string literal. The
change_case parameter is now used to change the case of the name.
#[php(name = "NEW_NAME")]
#[php(change_case = snake_case)]]
Available cases are:
snake_casePascalCasecamelCaseUPPER_CASEnone- No change