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.