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 and FromZval 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 forseeable future. It's recommended to lock the version at the patch level.

Documentation

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

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

Examples

Hello World

Let's create a basic PHP extension. 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"

[dependencies]
ext-php-rs = "*"

[lib]
crate-type = ["cdylib"]

.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"

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
}

test.php

Let's make a test script.

<?php

var_dump(hello_world("David"));

Now let's build our extension and run our test script. This is done through cargo like any other Rust crate. It is required that the php-config executable is able to be found by the ext-php-rs build script.

The extension is stored inside target/debug (if you did a debug build, target/release for release builds). The file name will be based on your crate name, so for us it will be libhello_world. The extension is based on your OS - on Linux it will be libhello_world.so, on macOS it will be libhello_world.dylib and on Windows it will be hello_world.dll (no lib prefix).

$ cargo build
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
$ php -dextension=./target/debug/libhello_world.dylib test.php
string(13) "Hello, David!"

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 implements IntoZval and/or FromZval.
  • HashMap<String, T> where T implements IntoZval and/or FromZval.
  • Binary<T> where T implements Pack, used for transferring binary string data.
  • A PHP callable closure or function wrapped with Callable.
  • Option<T> where T implements IntoZval and/or FromZval, and where None is converted to a PHP null.

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 the ClassRef type.
  • A Rust closure wrapped with Closure.
  • Result<T, E>, where T: IntoZval and E: Into<PhpException>. When the error variant is encountered, it is converted into a PhpException 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 parameterT Return type&T Return typePHP representation
YesNoYesNoi32 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 parameterT Return type&T Return typePHP representation
YesNoYesNozend_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 parameterT Return type&T Return typePHP representation
NoYesNoYeszend_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 parameterT Return type&T Return typePHP representation
YesNoYesNoUnion 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!"

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 parameterT Return type&T Return typePHP representation
YesNoYesNoZendHashTable

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

HashMaps are represented as associative arrays in PHP.

T parameter&T parameterT Return type&T Return typePHP representation
YesNoYesNoZendHashTable

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 parameterT Return type&T Return typePHP representation
YesNoYesNozend_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 }

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 parameterT Return type&T Return typePHP representation
YesNoYesNoDepends 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 parameterT Return type&T Return typePHP representation
NoYesZBox<ZendObject>Yes, mutable onlyZend object.

Examples

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 parameterT Return type&T Return typePHP 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 arguemnts, all of which must implement IntoZval and Clone.

T parameter&T parameterT Return type&T Return typePHP representation
CallableNoClosure, Callablefor PHP functionsNoCallables 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() {}

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 Rust impl 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.
  • 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() {}

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 publically 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")]

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() {}

#[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 seperate impl block.

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 of Option<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 whick 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 ommited, 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_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() {}