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