Exploring the PHP FFI
The Foreign Function Interface (FFI) PHP extension makes it possible to use shared libraries and call external functions. This is useful to use functionality not in existing PHP extensions, or call high-performance implementations. While it open doors, the low-level nature requires some caution. Additionally, there is some overhead in the translation between PHP and C datastructures.
Nevertheless, there are benefits to easily interfacing with external libraries. This compared with writing PHP extensions in C in order to create interfaces.
Hello world
A “Hello World” example from the documentation, for macOS
<?php
// simple.php
$library = "libSystem.dylib" // macOS stdlib
$ffi = FFI::cdef("int printf(const char *format, ...);", library);
$ffi->printf("Hello %s!\n", "world");
Hello world!
With a “C header”-like syntax, you can define types and function prototypes that can be later called via the constructed FFI object.
More complex types
Structs and arrays can be manipulated too from PHP. It is possible (though not recommended due to overhead and resource leaks) to define PHP callbacks that can be called by external logic.
<?php
// complex.php
function dump($A) { foreach ($A as $p) { echo "($p->x, $p->y) "; } echo "\n"; }
$ffi = FFI::cdef(
<<<H
struct Point { int x; int y; };
typedef int (*compare_fn)(struct Point* p1, struct Point* p2);
void qsort(void *base, size_t nel, size_t width, compare_fn cmp);
H,
"libSystem.dylib"
);
// build array of random points and show
$A = $ffi->new("struct Point[100]");
for ($i = 0; $i < 100; $i++) {
$A[$i]->x = random_int(100, 1000);
$A[$i]->y = random_int(100, 1000);
}
dump($A);
// define comparator callback function, call qsort and show
$cmp = fn($p1, $p2) => hypot($p1->x, $p1->y) <=> hypot($p2->x, $p2->y);
$ffi->qsort($A, 100, FFI::sizeof($A[0]), $cmp);
dump($A);
(688, 291) (341, 689) ... (894, 605)
(101, 132) (219, 269) ... (796, 979)
External Libraries
This snippet calls functions outside the C standard library: namely from libgeos to perform some computational geometry and generate a Voronoi diagram from a set of coordinates. Note an existing PHP extension with bindings exists which would be more convinient in practice:
<?php
$ffi = FFI::cdef(
<<<H
typedef void(* GEOSMessageHandler) (const char *fmt);
typedef struct GEOSContextHandle_HS* GEOSContextHandle_t;
typedef struct GEOSGeom_t GEOSGeometry;
typedef struct GEOSGeoJSONWriter_t GEOSGeoJSONWriter;
GEOSGeoJSONWriter* GEOSGeoJSONWriter_create(GEOSContextHandle_t handle);
char* GEOSGeoJSONWriter_writeGeometry(GEOSGeoJSONWriter *writer, const GEOSGeometry *g, int indent);
GEOSContextHandle_t GEOS_init_r (void);
GEOSGeometry* GEOSGeom_createPointFromXY_r (GEOSContextHandle_t handle, double x, double y);
GEOSGeometry* GEOSUnion_r (GEOSContextHandle_t handle, const GEOSGeometry *ga, const GEOSGeometry *gb);
GEOSGeometry* GEOSVoronoiDiagram (const GEOSGeometry *g, const GEOSGeometry *env, double tolerance, int flags);
void initGEOS (GEOSMessageHandler notice_function, GEOSMessageHandler error_function);
void finishGEOS(void);
H,
"/opt/homebrew/lib/libgeos_c.dylib"
);
// list of coordinates of places scraped from a Google Maps search for "tea" in Madrid
$locs = [[-3.7051612755592,40.414486435677006] /* , ... etc */];
$ffi->initGEOS(fn($fmt) => var_dump($fmt), fn($fmt) => var_dump($fmt));
$ctx = $ffi->GEOS_init_r();
$points = $ffi->GEOSGeom_createPointFromXY_r ($ctx, $locs[0][0], $locs[0][1]);
for ($i = 1; $i < count($locs); $i++) {
$p = $ffi->GEOSGeom_createPointFromXY_r ($ctx, $locs[$i][0], $locs[$i][1]);
$points = $ffi->GEOSUnion_r($ctx, $points, $p);
}
$diagram = $ffi->GEOSVoronoiDiagram($points, null, 0, 0);
$writer = $ffi->GEOSGeoJSONWriter_create($ctx);
$out = FFI::string($ffi->GEOSGeoJSONWriter_writeGeometry($writer, $diagram, 1));
print_r($out);
// omitted, but should clean and call the respective functions to free these resources
$ffi->finishGEOS();
Visualising the GeoJSON output on https://geojson.io:
Nested interpreters
The Python interpreter can be embedded in applications. A minimal embedding requires initialising the interpeter Py_Initialize, and calling with some statements to execute, for example by PyRun_SimpleString.
Python too has a FFI library – ctypes, which could ne used to nest an additional interpreter…
<?php
// python.php
$python = FFI::cdef(
<<<H
void Py_Initialize();
int PyRun_SimpleString(const char *command);
H,
'/opt/homebrew/Cellar/python@3.13/3.13.6/Frameworks/Python.framework/Versions/3.13/lib/libpython3.13.dylib'
);
$python->Py_Initialize();
$python->PyRun_SimpleString("x = 3");
$python->PyRun_SimpleString("print(x)");
$python->PyRun_SimpleString("print('Hello from Python in PHP')");
$python->PyRun_SimpleString(<<<Python
from ctypes import *
from ctypes.util import find_library
lua = cdll.LoadLibrary(find_library('liblua'))
# define function prototypes
lua.luaL_newstate.restype = c_void_p
lua.luaopen_base.argtypes = [c_void_p]
lua.luaL_loadstring.argtypes = [c_void_p, c_char_p]
lua.lua_callk.argtypes = [c_void_p, c_int, c_int, c_void_p, c_void_p]
state = lua.luaL_newstate()
lua.luaopen_base(state)
lua.luaL_loadstring(state, b'print("Howdy from Lua in Python in PHP")')
lua.lua_callk(state, 0, 0, None, None)
Python);
Prints:
3
Hello from Python in PHP
Howdy from Lua in Python in PHP
It is a little more involved to convert to/from the C PyObject types needed to pass data to/from the nested interpreter.
Something egregious
⚠️ Unsafe things here ⚠️ a small shared library built with cc -shared -o insecure.so insecure.c:
// insecure.c
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
typedef int (*fptr)(void);
int insecure(size_t len, char* input) {
size_t page_size = sysconf( _SC_PAGE_SIZE );
fptr ptr = aligned_alloc(page_size, (1 + len / page_size) * page_size);
memcpy(ptr, input, len);
mprotect(ptr, aligned_size, PROT_EXEC | PROT_READ);
int ret = ptr();
free(ptr);
return ret;
}
insecure function takes a string, copies to a region of memory, makes it executable via an mprotect system call (byassing execution protection on original input), and finally executes the provided input. A small API in PHP:
<?php
$ffi = FFI::cdef("int insecure(size_t len, char* input);", "insecure.so");
$bin = base64_decode(substr($_SERVER['REQUEST_URI'], 1));
echo $ffi->insecure(strlen($bin), $bin) . "\n";
ffi.enable=true needs to be set in php.ini to enable FFI outside CLI contexts. A PHP dev server can be launched with php -S localhost:5002 insecure.php, altogether giving a small API that executes any http://localhost:5002/{binary}, where binary is a base64-encoded machine-code binary string. On AArch64/macOS:
; return a value (/4AaA0sADX9Y=)
ldr x0, =55 ; e0 06 80 d2
ret ; c0 03 5f d6
; cause PHP interpreter to exit (/AACA0jAAgNIBEADU)
ldr x0, =0 ; 00 00 80 d2
ldr x16, =1 ; 30 00 80 d2
svc 0x80 ; 01 10 00 d4
; breakpoint (/AAAg1A==)
brk 0 ; 00 00 20 d4
...
Don’t do this ;)