devrustphp
(updated ) ~4 min (671 words)

Rusty PHP - creating PHP extensions with Rust

A modified and modernized ElePHPant PHP mascot in a rusty color

Recently, I took a look at the ext-php-rs project.

With ext-php-rs you can very easily write extensions for PHP in Rust.

So, for testing, I developed a little extension implementing a function I recently used in native PHP.


Why using a native PHP function for comparison you asked?

For three reasons:

  1. Check the performance impact using an external Rust implementation compared to PHP native functions
  2. Get an idea about how to develop an extension with ext-php-rs
  3. See how comfortable it is to do that

Setting up ext-php-rs

If you have used Rust before, it's really very straightforward:

  1. Install PHP executable and the development packages
  2. Create a new Rust project via cargo new php-ext --lib
  3. Now open the project in an IDE like RustRover or IntelliJ Community or whatever
  4. Make sure the Cargo.toml looks is configured like this
[lib]
crate-type = ["cdylib"]

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

[profile.release]
strip = "debuginfo"

Now we still need the cargo subcommand to run the php extensions:

cargo install cargo-php

Via cargo php you can now install or delete extensions directly into your PHP environment.

The lib.rs can now look as simple as this:


#![cfg_attr(windows, feature(abi_vectorcall))]

use ext_php_rs::prelude::*;

#[php_function]
pub fn find_mapping(needle: String, haystack: Vec<String>) -> Option<String> {
    haystack.iter().find(|&elem| elem == &needle).cloned()
}

// Required to register the extension with PHP.
#[php_module]
pub fn module(module: ModuleBuilder) -> ModuleBuilder {
    module
}



A simple cargo build and cargo php install will enable you to use the function in any php file:

find_mapping($testString, $randomStrings);



Conclusion

Setting up the ext-php-rs project is very straightforward and knowing a bit of Rust you can create a new PHP extension in almost no time.

Also, most things can be done quite fine with better performance by native PHP functions so such an extension is especially suited for apps with compute intense processes or safety concerns that can be modularized into an extension.

Yes, I did some simple benchmarks here:

time taken with ext-php-rs: '413.46788406372ms'
time taken with PHP internal: '8.3808898925781ms'
Time taken with native Rust: '4.85131ms'

As you can see, there's a large difference with the extension's performance. Let's find out why!

Update March 2024 and the reason for the slower Rust extension

I followed up this topic since I wanted to find the reason for the performance issues I encountered. With a huge array there was a significant overhead of about 400ms above the PHP's native version which shouldn't normally happen with Rust.

So after posting the performance issue to the ext-php-rs project's GitHub issue tracker and discussing with the nice people there.

The reason for the performance issues obviously is related to internal type conversions the extension has to do so using the Zend types instead improves the performance by a lot:

#[php_function]
pub fn find_mapping(needle: &Zval, haystack: &ZendHashTable) -> Option<String> {
    match haystack.iter().position(|elem| elem.1.str() == needle.str()) {
        None => None,
        Some(_ind) => needle.string()
    }
}

An even better solution came from ju1ius:

#[php_function]
pub fn find_mapping(needle: &Zval, haystack: &ZendHashTable) -> Option<ZVal> {
    let Some(needle) = needle.zend_str() else {
        return None; // should rather be a type error, but oh well...
    };
    haystack.iter().find_map(|(_, value)| value.zend_str()
        .filter(|zs| zs == needle)
        .map(|_| value.shallow_clone())
    )
}

So be careful if you're juggling with a lot of data in your extension. Even with the above solution there's still some conversion going on impacting the performance.

Therefore, it's better making sure you have another way of reading the data in case there's a lot of it to prevent the type conversions.


And you still have to look at this adorable slightly rusty ElePHPant: Another rusty 3D ElePHPant created with ideogram.ai



Image Attribution
Both images created by ideogram.ai