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.

What is PHP FFI?
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
/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");
-
FFI::cdef
– with this operation we define the interaction interface. -
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. -
/PATH/TO/SO/lib.so
– the path to the dynamic library where the function above is located.
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
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

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!