Foreign Function Interface (FFI)
Since its beginning, PHP can be extended through extensions written in C. Extensions can be used, for instance, to implement computationally expensive algorithms more efficiently than in PHP or to wrap functionality provided by a library to be used in PHP code. Given that you’re proficient with C, implementing such an extension is easy – once you have mastered the relevant programming interfaces of the PHP interpreter and runtime. This extensibility has been, and continues to be, a key success factor for PHP.
In an effort to make using functionality from a library usable
from PHP even easier, PHP 7.4 introduced a foreign function
interface (FFI). Generally speaking, such an interface allows a
library implemented in one programming language, for instance in C,
to be used in a program written in another programming language, for
instance in PHP. With this foreign function interface, that is
provided by PHP’s ffi
extension, a PHP developer no
longer needs to write C code to use a library written in C from
PHP.
We look at a simple C library with only one function,
leapyear()
, that is defined in library.h
and implemented in library.c
:
library.h
#ifndef __LIBRARY_H__
#define __LIBRARY_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
bool leapyear(unsigned year);
#ifdef __cplusplus
}
#endif
#endif
library.c
#include "library.h"
#include <stdbool.h>
bool leapyear(unsigned year)
{
return !(year % 4) && ((year % 100) || !(year % 400));
}
Using gcc
, the GNU Compiler Collection’s C compiler,
we can compile library.c
to library.so
like so:
$ gcc library.c -fPIC -shared -o library.so
Your operating system needs to be able to find
library.so
. This can be achieved, for instance, by
placing it in a directory that is listed in the
LD_LIBRARY_PATH
environment variable. This is a
colon-separated list of directories where libraries are searched
for.
Now that we have C library compiled as a shared object, we can use it from PHP like so:
$ffi = FFI::cdef(
'bool leapyear(unsigned year);',
'library.so'
);
var_dump($ffi->leapyear(2020));
Executing the code shown above will print the output shown below:
bool(true)
FFI
is a class provided by PHP’s ffi
extension. Its cdef()
method is used to create an
object that acts as a bridge between regular PHP code, and the
functionality that is implemented in the C library.
The first argument that is passed to FFI::cdef()
is
a string containing a sequence of C declarations for the library
functions that we want to use in PHP. This is usually a simple
copy-and-paste from the C library’s header file,
library.h
in our example.
The second argument that is passed to FFI::cdef()
is
a string containing the name of the shared library we want to use.
As explained above, the operating system needs to be able to find
it.
Behind the scenes, FFI::cdef()
automatically creates
the bindings necessary to call the leapyear()
function
implemented in the C library from PHP. The function is exposed as a
method on the FFI
object returned by
FFI::cdef()
.
Being able to basically use any function from arbitrary libraries available on a system is powerful. So powerful, in fact, that we need to be concerned about security. Hopefully nobody would even want to write code like this:
$ffi = FFI::cdef(
$_GET['cdef'],
$_GET['library']
);
The code shown above would allow an attacker to specify the C
declarations as well as the library name from the outside via HTTP
GET parameters. And while this is – hopefully! – obviously a bad
idea, you sometimes may need to use variables for the C
declarations, or the library name, or both. And then it might not be
as obvious that a value from the outside can be passed to
FFI::cdef()
. It is better to be safe than sorry, so how
can we safely use the foreign function interface?
First things first: if you do not need foreign function
interface’s functionality then do not even install or load the
ffi
extension. If you actually need the foreign
function interface, then you should be familiar with the
ffi
extension’s configuration directives,
ffi.enable
and ffi.preload
.
ffi.enable=false
completely disables the loading and
binding of C declarations and has the same effect as not loading the
ffi
extension.
ffi.enable=preload
limits the loading and binding of
C declarations to the PHP process’ startup phase during which C
declarations are loaded from C header files that are specified using
the ffi.preload
configuration directive.
ffi.enable=true
enables the loading and binding of C
declarations in all sourcecode files and is strongly
discouraged.
Here is an example of a C header file for use with
ffi.preload
:
php_library.h
#define FFI_SCOPE "LIBRARY"
#define FFI_LIB "library.so"
bool leapyear(unsigned year);
We define two constants, FFI_SCOPE
and
FFI_LIB
. The former defines the name of a foreign
function interface scope. The latter defined the name of the library
we want to use. The C declarations that we loaded from a C header
file in the preload script during startup are available under the
foreign function interface scope of name LIBRARY
. Its
functions can be accessed through FFI::scope('LIBRARY')
in PHP code that is preloaded:
test.php
<?php declare(strict_types=1);
$ffi = FFI::scope('LIBRARY');
var_dump($ffi->leapyear(2020));
Here is how we can use php_library.h
and
test.php
on the command line:
$ php -d ffi.enable=preload \
-d ffi.preload=php_library.h \
test.php
bool(true)
With ffi.enable=preload
we limited the loading of C
declarations as well as the generation of bindings for using the
functions behind those declarations to preload scripts. This is good
from a security perspective, because neither the C declarations nor
the library to be used can be injected from the outside via request
parameters.
We will now explore how Preloading can be used to improve the performance of the foreign function interface.
First, we need a preload script:
preload.php
<?php declare(strict_types=1);
FFI::load(__DIR__ . '/php_library.h');
opcache_compile_file(__DIR__ . '/Library.php');
preload.php
contains two statements. First, we use
FFI::load()
to load C declarations from a C header
file, php_library.h
. Then we use
opcache_compile_file()
to compile a PHP sourcecode
file, Library.php
. As we already discussed in the
section on preloading, opcache_compile_file()
only
loads and compiles a file but does not execute any code in the
file’s global scope.
Library.php
<?php declare(strict_types=1);
final class Library
{
private ?FFI $ffi = null;
public function leapyear(int $year): bool {
return $this->ffi()->leapyear($year);
}
private function ffi(): FFI
{
if ($this->ffi === null) {
$this->ffi = FFI::scope('LIBRARY');
}
return $this->ffi;
}
}
Let us recap: during preload, we loaded C declarations from a C
header file, created foreign function interface bindings for them
that we bound to a scope, and declared a class,
Library
, that wraps this scope.
Here is an example of how we can use this Library
class:
test.php
<?php declare(strict_types=1);
$library = new Library;
var_dump($library->leapyear(2020));
Finally, here is how we can use preload.php
,
php_library.h
, Library.php
, and
test.php
on the commandline:
$ php -d ffi.enable=preload \
-d opcache.enable=1 \
-d opcache.enable_cli=1 \
-d opcache.preload=preload.php \
test.php
bool(true)
ffi.enable=preload
enables the foreign function
interface (only) for preload scripts, opcache.enable=1
enables OpCache, opcache.enable_cli=1
configures
OpCache to also work on the commandline, and
opcache.preload=preload.php
configures OpCache to
preload the preload.php
script.
Binding foreign functions during preload improves the performance as the binding needs to be performed only once at startup, and not for each and every request that is served.
The ffi
extension significantly reduces the amount
of work required to use functionality that is implemented in a
dynamic library (shared object) and for which you have C
declarations. But make no mistake: while you no longer need to
implement an extension for the PHP runtime using hand-written C
code, you still need to have knowledge about C programming, memory
management, and so on.
Please note that we barely scratched the surface of what is
possible with the ffi
extension. We only showed you
simple examples for the most common use cases. There is also
functionality, for instance, to access memory that is used by a
library.