Tips for Getting PHP to Work With Go, Rust, and C++ Using Foreign Function Interface

The 2020 release of PHP 7.4 has given developers the ability to do something they have never done before – access data structures and call functions written in another language with pure PHP code, no extensions, and no bindings to external libraries needed.

How is this possible? With PHP FFI (Foreign Function Interface).

In this article, we’re going to discuss what PHP FFI is, its benefits, and capabilities, and compare how PHP can work with languages such as Go, Rust, and C++ without the need to create plug-ins. We will also share experiments we used in implementing this function and highlight where we found it most useful and where, in our opinion, it was not worth the trouble.

Programming with PHP FFI

What is PHP FFI?

In general terms, FFI is an interface that enables developers to use one language to call a library function written in another language. For example, FFI makes it possible to call a function written in Rust, C++, or Go from pure PHP. In order to connect an interpreted language with a compiled language, the libffi [GitHub] library is used – Wiki

Since the interpreted languages ​​do not know where to specifically search for the parameters of the called function (i.e., in which registers), nor where to get the results of the function after the call, they rely on Libffi to do that work. So, you need to install this library, as it is part of the system libraries (Linux).

Benefits of PHP FFI

While wholly experimental right now, early testing of PHP FFI reveals a host of benefits that could potentially do away with some cumbersome PHP extensions, and ultimately, usher in an interesting new era of development.

  • Save time and energy not having to write PHP-specific extensions/modules just to interface with C programs/libraries
  • Execute faster on heavy-computing jobs like image and video processing
  • Save money launching instances of PHP on common cloud platforms versus launching expensive VM’s and containers

PHP FFI Experiments

NOTE: All PHP FFI experiments were conducted on ArchLinux (5.6.1 kernel), Libffi 3.2.1.

While we weren’t adhering to the scientific method in a very strict way with our experiments, we did set out with a purpose. It is certainly interesting to explore new language features, but the question we were asking ourselves was, does this make practical sense for software development?

The results are as follows:

Calculating Fibonacci Sequences with PHP FFI

For our first experiment, we thought a problem like calculating the Fibonacci sequences was simple yet interesting. And of course, we did not set out to do it in the most efficient way; rather, we wanted to employ the help of recursion so as to use the processor as much as possible. This would also prevent compiled languages from optimizing this function (for example, applying the technique of loop unwinding).

Experiment 1: Using Rust

For PHP, the first thing we did was to uncomment the extension ffi in php.ini (/etc/php/php.ini in ArchLinux).

Next, we needed to declare our conditional interface. There are some restrictions that are currently present in PHP FFI, in particular, the inability to use a C-preprocessor (#include, #define, etc.), except for some special ones. In PHP type:

$ffi = FFI::cdef(
     "int Fib(int n);",
    "/PATH/TO/SO/lib.so");
  1. FFI::cdef – with this operation we define the interaction interface.
  2. int Fib (int n) – IT’s the name of the exported method of the compiled language. We will talk about how to do it right a little bit later.
  3. /PATH/TO/SO/lib.so – the path to the dynamic library where the function above is located.

The full PHP script we used:

PHP FFI

function fib($n)
{
    if ($n === 1 || $n === 2) {
        return 1;
    }
    return fib($n - 1) + fib($n - 2);
}

$start = microtime(true);
$p = 0;
for ($i = 0; $i < 1000000; $i++) {
    $p = fib(12);
}

echo '[PHP] execution time: '.(microtime(true) - $start).' Result: '.$p.PHP_EOL;

RUST FFI

$rust_ffi = FFI::cdef(
    "int Fib(int n);",
    "lib/libphp_rust_ffi.so");

$start = microtime(true);
$r = 0;
for ($i=0; $i < 1000000; $i++) { $r = $rust_ffi->Fib(12);
}

echo '[RUST] execution time: '.(microtime(true) - $start).' Result: '.$r.PHP_EOL;

CPP FFI

$cpp_ffi = FFI::cdef(
    "int Fib(int n);",
    "lib/libphp_cpp_ffi.so");

$start = microtime(true);
$c = 0;
for ($i=0; $i < 1000000; $i++) { $c = $cpp_ffi->Fib(12);
}

echo '[CPP] execution time: '.(microtime(true) - $start).' Result: '.$c.PHP_EOL;

GOLANG FFI

$golang_ffi = FFI::cdef(
    "int Fib(int n);",
    "lib/libphp_go_ffi.so");

$start = microtime(true);

for ($i=0; $i < 1000000; $i++) { $golang_ffi->Fib(12);
}

echo '[GOLANG] execution time: '.(microtime(true) - $start).' Result: '.$c.PHP_EOL;

The first step was to make a dynamic library in the Rust language.

This required some preparation:

1. On any platform, for the installation we needed only one instruction from here.

2. After that, we could create a project anywhere with the command cargo new rust_php_ffi. And that was it!

Here is the function we used:

RUST:

//src/lib.rs

#[no_mangle]
extern "C" fn Fib(n: i32) -> i32 {
    if (n == 0) || (n == 1) {
        return 1;
    }

    Fib(n - 1) + Fib(n - 2)
}

NOTE: It is critical to add the attribute # [no_mangle] to the required function, because otherwise the compiler will replace the name of your function with something like: _аgs @ fs34. And when exporting it to PHP, libffi simply won’t be able to find a function named Fib in the dynamic library. You can read more about this issue here.

In Cargo.toml, we added the attribute:

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

I would like to draw your attention to the fact that there are three options for a dynamic library through an attribute in Cargo.toml:

1. dylib – Rust shares this library with an unstable ABI, which can change from version to version (as in Go internal ABI).

2. cdylib is a dynamic library for using in C/C++. This is our top choice.

3. rlib – Rust static library with rlib extestion (.rlib). It also contains metadata used to link various rlibs written respectively in Rust

Then, we compiled it using cargo build --release. And in the folder target/release we saw the .so file. This would be our dynamic library.
C++

Next in line is C++. Here everything is quite simple, too:

CPP:

// in php_cpp_ffi.cpp

int main() {
    
}

extern "C" int Fib(int n) {
    if ((n==1) || (n==2)) {
        return 1;
    }

    return Fib(n-1) + Fib(n-2);
}

We needed to declare the extern function so that it could be imported from php.

We compiled:

g++ -fPIC -O3 -shared src/php_cpp_ffi.cpp -o ../lib/ libphp_cpp_ffi.so

A few comments on the compilation:

1. -fPIC position-independence code. For a dynamic library, it is important to be independent of the address at which it is loaded in memory.

2. -O3 – maximum optimization

Experiment 2: Using Golang

Next in line for our experiment was Golang – a language with runtime. A special mechanism for interacting with dynamic libraries was developed for Go called CGO. Learn more about how this mechanism works here.

Due to the fact that CGO interprets the generated errors from C, there was no way to use optimizations as we did in C++ link and link.

Drumroll, please . . . and here is the code!

GOLANG:

package main

import (
        "C"
)

// we needed to have empty main in package main :)
// because -buildmode=c-shared requires exactly one main package
func main() {

}

//export Fib
func Fib(n C.int) C.int {
        if n == 1 || n == 2 {
                return 1
        }

        return Fib(n-1) + Fib(n-2)
}

So, all of this was the same Fib function, however, in order for this function to be exported in a dynamic library, we needed to add the comment above (a sort of GO attribute) //export Fib.
Then, we compiled: go build -o ../lib/libphp_go_ffi.so -buildmode=c-shared. Pay attention to how we added -buildmode = c-shared in order to get a dynamic library.

We received 2 files at output. A file with the headers .h and .so was a dynamic library. We do not really need the file with headers, since we know the name of the function, and FFI PHP is rather limited in working with the C preprocessor.

Rocket Launch

After we wrote everything (source codes are provided), we made a small Makefile to collect it all (it is also located in the repository). After we called make build in the lib folder, 4 files appeared. Two for GO (.h/.so) and one for Rust and C ++.

Makefile:

build_cpp:
        echo 'Building cpp...'
        cd cpp && g++ -fPIC -O3 -shared src/php_cpp_ffi.cpp -o libphp_cpp_ffi.so

build_go:
        echo 'Building golang...'
        cd golang && go build -o libphp_go_ffi.so -buildmode=c-shared

build_rust:
        echo 'Building Rust...'
        cargo build --release && mv rust/target/release/libphp_ffi.so libphp_rust_ffi.so

build: build_cpp build_go build_rust


run:
        php php/php_fib.php

Then, we went to the php folder and ran our script (or via the Makefile – make run). NOTE: In the php script in FFI::cdef the paths to the .so files are hardcoded, so for everything to work, please run through make run. The result of the work is as follows:

1. [PHP] execution time: 8.6763260364532 Result: 144

2. [RUST] execution time: 0.32162690162659 Result: 144

3. [CPP] execution time: 0.3515248298645 Result: 144

4. [GOLANG] execution time: 5.0730509757996 Result: 144

As expected, PHP showed the lowest result in the CPU loaded with math operations, but nevertheless, on the whole, it felt pretty fast for a million calls.

We were surprised that the running time of CGO was a little less than PHP. This was due to calling-conventions that resulted from an unstable ABI. CGO was forced to carry out type conversion operations from Go-types to C (you can see in the h file that is obtained after building the GO dynamic library) types, and we had to copy the incoming and return values ​​for C and GO compatibility.


Rust and C++ showed the best results in our experiment which, honestly, was what we expected since they have a stable ABI (extern in Rust) and the only layer between PHP and these languages ​​is libffi.

Conclusion

Due to the high potential for encountering various pitfalls, the PHP FFI approach isn’t yet production-ready, but it is promising. The PHP development community even issued their own warning on PHP.net where they introduce the feature.

PHP FFI warning

Bottomline: There are no normal ways to work with the preprocessor.

This article was simply meant to highlight the capabilities of a new language feature. However, if this feature of PHP becomes stable, imagine the possibilities for optimizing hot spots in your code. It could be a game-changer for our software development company and countless others. We’ll see!

Valery Piashchynski
Valery Piashchynski

Golang/C++ software engineer at SpiralScout. Linux kernel researcher.

All author posts
Scroll to top