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
IntoZval
andFromZval
for 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.
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-config
binary
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.exe
is 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"
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. The #[php_module]
attribute automatically registers your new
function so we don't need to do anything except return the ModuleBuilder
that
we were given.
We also need to enable the abi_vectorcall
feature when compiling for Windows.
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))]
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
}
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
S
be the empty string - append to
S
the value of std::env::consts::DLL_PREFIX (empty on windows,lib
on unixes) - append to
S
the lower-snake-case version of your crate name - append to
S
the value of std::env::consts::DLL_SUFFIX (.dll
on windows,.dylib
on macOS,.so
on 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
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`
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 (
String
and&str
) Vec<T>
where T implementsIntoZval
and/orFromZval
.HashMap<String, T>
where T implementsIntoZval
and/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 implementsIntoZval
and/orFromZval
, and whereNone
is 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
self
when used in a method, through theClassRef
type. - A Rust closure wrapped with
Closure
. Result<T, E>
, whereT: IntoZval
andE: Into<PhpException>
. When the error variant is encountered, it is converted into aPhpException
and 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"
HashMap
HashMap
s 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
.
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: hello v: world
k: rust v: php
k: 0 v: okk
array(3) {
[0] => string(5) "world",
[1] => string(3) "php",
[2] => string(3) "okk"
}
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 } 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 } 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 } fn main() {}
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 { // Even though this function doesn't have a `self` type, it is still treated as an associated method // and not a static method. pub fn builder_pattern(#[this] this: &mut ZendClassObject<Example>) -> &mut ZendClassObject<Example> { // do something with `this` this } } #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module } 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 } 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 , Callable for 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() {}
#[php_async_impl]
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.
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!
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 Rustimpl
block to PHP, including all methods and constants.php_const
- Used to export a Rust constant to PHP as a global constant.
These macros do abuse the fact that (at the moment) proc macro expansion seems
to happen orderly, on one single thread. It has been stated many times that this
order is undefined behaviour (see here), so these macros could break at any
time with a rustc
update (let's just keep our fingers crossed).
The macros abuse this fact by storing a global state, which stores information
about all the constants, functions, methods and classes you have registered
throughout your crate. It is then read out of the state in the function tagged
with the #[php_module]
attribute. This is why this function must be the
last function in your crate.
In the case the ordering does change (or we find out that it already was not in
order), the most likely solution will be having to register your PHP exports
manually inside the #[php_module]
function.
#[php_module]
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.
Using the macro, any functions annotated with the php_function
macro will be
automatically registered with the extension in this function. If you have
defined any constants or classes with their corresponding macros, a 'module
startup' function will also be generated if it has not already been defined.
Automatically registering these functions requires you to define the module function after all other functions have been registered, as macros are expanded in-order, therefore this macro will not know that other functions have been used after.
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):
- 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
- Functions not automatically registered
Classes and constants are not registered 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::*;
use ext_php_rs::{info_table_start, info_table_row, info_table_end};
use ext_php_rs::php::module::ModuleEntry;
/// 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.info_function(php_module_info)
}
#[php_startup]
Used to define the PHP extension startup function. This function is used to register extension classes and constants with the PHP interpreter.
This function is automatically generated if you have registered classes or
constants and have not already used this macro. If you do use this macro, it
will be automatically registered in the get_module
function when you use the
#[php_module]
attribute.
Most of the time you won't need to use this macro as the startup function will be automatically generated when required (if not already defined).
Read more about what the module startup function is used for here.
Example
#![cfg_attr(windows, feature(abi_vectorcall))] extern crate ext_php_rs; use ext_php_rs::prelude::*; #[php_startup] pub fn startup_function() { } fn main() {}
#[php_function]
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 } fn main() {}
Default parameter values can also be set for optional parameters. This is done
through the 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(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) } 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 } 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(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 } 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 equivelant of a PHP function using
the ...$args
syntax.
#![cfg_attr(windows, feature(abi_vectorcall))] extern crate ext_php_rs; use ext_php_rs::prelude::*; use ext_php_rs::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 } 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.
Classes
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
The attribute takes some options to modify the output of the class:
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.
There are also additional macros that modify the class. These macros must be
placed underneath the #[php_class]
attribute.
#[extends(ce)]
- Sets the parent class of the class. Can only be used once.ce
must be a valid Rust expression when it is called inside the#[php_module]
function.#[implements(ce)]
- Implements the given interface on the class. Can be used multiple times.ce
must be a valid Rust expression when it is called inside the#[php_module]
function.
You may also use the #[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 rename the property with options:
rename
- Allows you to rename the property, e.g.#[prop(rename = "new_name")]
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, #[prop] address: String, } #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module } 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::*; use ext_php_rs::{exception::PhpException, zend::ce}; #[php_class(name = "Redis\\Exception\\RedisException")] #[extends(ce::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 } fn main() {}
Implementing an Interface
To implement an interface, use #[implements(ce)]
where ce
is an expression returning a ClassEntry
.
The following example implements ArrayAccess
:
#![cfg_attr(windows, feature(abi_vectorcall))] extern crate ext_php_rs; use ext_php_rs::prelude::*; use ext_php_rs::{exception::PhpResult, types::Zval, zend::ce}; #[php_class] #[implements(ce::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 } fn main() {}
#[php_impl]
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.
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.
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 is
annotated with the #[this]
attribute. This can also be used to return a
reference to $this
.
By default, all methods are renamed in PHP to the camel-case variant of the Rust
method name. This can be changed on the #[php_impl]
attribute, by passing one
of the following as the rename_methods
option:
"none"
- does not rename the methods."camelCase"
- renames all methods to camel case (default)."snake_case"
- renames all methods to snake case.
For example, to disable renaming, change the #[php_impl]
attribute to
#[php_impl(rename_methods = "none")]
.
The rest of the options are passed as separate attributes:
#[defaults(i = 5, b = "hello")]
- Sets the default value for parameter(s).#[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>
.#[public]
,#[protected]
and#[private]
- Sets the visibility of the method.#[rename("method_name")]
- Renames the PHP method to a different identifier, without renaming the Rust method name.
The #[defaults]
and #[optional]
attributes operate the same as the
equivalent function attribute parameters.
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 #[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 #[getter]
and #[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 rename
option to the 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 #[getter]
and #[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
.
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, #[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() } } #[getter] pub fn get_name(&self) -> String { self.name.to_string() } #[setter] pub fn set_name(&mut self, name: String) { self.name = name; } #[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(#[this] this: &mut ZendClassObject<Human>) { dbg!(this); } pub fn get_max_age() -> i32 { Self::MAX_AGE } } #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module } 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_async_impl]
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.
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!
#[php_const]
Exports a Rust constant as a global PHP constant. The constant can be any type
that implements IntoConst
.
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] const ANOTHER_STRING_CONST: &'static str = "Hello world!"; #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module } fn main() {}
PHP usage
<?php
var_dump(TEST_CONSTANT); // int(100)
var_dump(ANOTHER_STRING_CONST); // string(12) "Hello world!"
ZvalConvert
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 } 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 } 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 } 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)
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() {}
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; #[php_startup] pub fn startup_function(ty: i32, module_number: i32) { let ini_entries: Vec<IniEntryDef> = vec![ IniEntryDef::new( "my_extension.display_emoji".to_owned(), "yes".to_owned(), IniEntryPermission::All, ), ]; IniEntryDef::register(ini_entries, module_number); } 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::*; use ext_php_rs::zend::ExecutorGlobals; #[php_startup] pub fn startup_function(ty: i32, module_number: 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>> } fn main() {}