article thumbnail
Understanding PHP FFI
A deep dive into PHP's Foreign Function Interface (FFI)
#

PHP FFI: Advanced Patterns, Safer Bindings, and Real‑World Examples

In this follow‑up, we'll deepen our FFI practice with richer examples and patterns you can adapt in production. We'll prioritize functions not exposed in core PHP, add JSDoc‑style docs for discoverability, and cover tooling to generate correct cdef signatures.

All examples assume Linux x86‑64. Adjust library names for macOS/Windows and always verify types against your platform headers.


1) Discovering Symbols & Generating Correct cdef

Before writing a single line of PHP, verify symbol names and signatures.

# Find the math library and confirm it exports erf/erfc
ldconfig -p | grep libm
nm -D /lib/x86_64-linux-gnu/libm.so.6 | grep -E '(^|\s)erf$|(^|\s)erfc$'
readelf -Ws /lib/x86_64-linux-gnu/libm.so.6 | grep -E 'erf|erfc'

# Explore libc for sysinfo and getrandom
nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep -E 'sysinfo|getrandom'
man 2 sysinfo
man 2 getrandom

Copy the exact function prototypes from man pages or headers (e.g., /usr/include/sys/sysinfo.h, /usr/include/unistd.h, /usr/include/math.h) into your cdef. Remember to also bring along any required typedefs and structs.


2) High‑Resolution Timing via clock_gettime (contrast with hrtime(), adds control)

PHP offers hrtime(), but FFI gives you precise control of which clock you read--useful when aligning with other native code or hardware.

<?php
/**
 * High‑resolution timing with explicit clock selection
 *
 * @ffi-library libc.so.6
 * @ffi-typedef struct timespec { long tv_sec; long tv_nsec; } timespec
 * @ffi-const int CLOCK_MONOTONIC
 * @ffi-func int clock_gettime(int clk_id, struct timespec *tp)
 */
$ffi = FFI::cdef(<<<'CDEF'
typedef long time_t;
struct timespec { long tv_sec; long tv_nsec; };
int clock_gettime(int clk_id, struct timespec *tp);
CDEF, "libc.so.6");

define('CLOCK_MONOTONIC', 1); // Linux value; verify on your platform

$ts = $ffi->new("struct timespec");
if ($ffi->clock_gettime(CLOCK_MONOTONIC, FFI::addr($ts)) !== 0) {
    throw new RuntimeException("clock_gettime failed");
}
$ns = $ts->tv_sec * 1000000000 + $ts->tv_nsec;
echo "MONOTONIC time (ns): {$ns}\n";

Pattern: wrap the call in your own PHP function to normalize units and abstract platform differences.


3) Cryptographically‑secure bytes with getrandom(2) (no external process, no file handles)

random_bytes() is great, but FFI can call getrandom directly when you want explicit flags and predictable behavior under chroot/containers.

<?php
/**
 * Secure randomness via getrandom(2)
 *
 * @ffi-library libc.so.6
 * @ffi-func ssize_t getrandom(void *buf, size_t buflen, unsigned int flags)
 * @param int $length Number of bytes to read
 * @param int $flags  GRND_NONBLOCK=0x0001, GRND_RANDOM=0x0002 (Linux); 0 for default
 * @return string Raw bytes
 */
$ffi = FFI::cdef(<<<'CDEF'
typedef long ssize_t;
typedef unsigned long size_t;
ssize_t getrandom(void *buf, size_t buflen, unsigned int flags);
CDEF, "libc.so.6");

function gr_bytes(int $length, int $flags = 0): string {
    global $ffi;
    $buf = FFI::new("unsigned char[$length]");
    $read = $ffi->getrandom($buf, $length, $flags);
    if ($read < 0 || $read != $length) {
        throw new RuntimeException("getrandom failed or returned partial data: $read");
    }
    return FFI::string($buf, $length);
}

$bytes = gr_bytes(32); // 32 secure bytes
echo bin2hex($bytes), "\n";

Why bother? You can select flags (blocking semantics/entropy source) and remove filesystem dependencies entirely.


4) System Health with sysinfo(2) -- wrapped and documented

<?php
/**
 * Get a snapshot of Linux system metrics via sysinfo(2).
 *
 * @ffi-library libc.so.6
 * @ffi-typedef struct sysinfo { long uptime; unsigned long loads[3]; unsigned long totalram; unsigned long freeram; unsigned long sharedram; unsigned long bufferram; unsigned long totalswap; unsigned long freeswap; unsigned short procs; unsigned long totalhigh; unsigned long freehigh; unsigned int mem_unit; char _f[20-2*sizeof(long)-sizeof(int)]; } sysinfo
 * @ffi-func int sysinfo(struct sysinfo *info)
 *
 * @return array{
 *   uptime:int,
 *   loads:array{float,float,float},
 *   ram:array{total:float, free:float},
 *   swap:array{total:float, free:float}
 * }
 */
$ffi = FFI::cdef(<<<'CDEF'
typedef long time_t;
struct sysinfo {
    long uptime;
    unsigned long loads[3];
    unsigned long totalram;
    unsigned long freeram;
    unsigned long sharedram;
    unsigned long bufferram;
    unsigned long totalswap;
    unsigned long freeswap;
    unsigned short procs;
    unsigned long totalhigh;
    unsigned long freehigh;
    unsigned int mem_unit;
    char _f[20-2*sizeof(long)-sizeof(int)];
};
int sysinfo(struct sysinfo *info);
CDEF, "libc.so.6");

function host_health(): array {
    global $ffi;
    $info = $ffi->new("struct sysinfo");
    if ($ffi->sysinfo(FFI::addr($info)) !== 0) {
        throw new RuntimeException("sysinfo failed");
    }
    $scale = 65536.0;
    $toGB  = fn(int $n, int $unit) => ($n * $unit) / 1_000_000_000;
    return [
        'uptime' => $info->uptime,
        'loads'  => [$info->loads[0]/$scale, $info->loads[1]/$scale, $info->loads[2]/$scale],
        'ram'    => ['total' => $toGB($info->totalram, $info->mem_unit), 'free' => $toGB($info->freeram, $info->mem_unit)],
        'swap'   => ['total' => $toGB($info->totalswap, $info->mem_unit), 'free' => $toGB($info->freeswap, $info->mem_unit)],
    ];
}

print_r(host_health());

5) Zero‑copy file transfer with sendfile(2) (bypasses userland buffers)

Handy for high‑throughput file servers or proxies.

<?php
/**
 * sendfile(2) from one FD to another.
 *
 * @ffi-library libc.so.6
 * @ffi-func ssize_t sendfile(int out_fd, int in_fd, long *offset, size_t count)
 * @param resource $src PHP stream (readable)
 * @param resource $dst PHP stream (writable)
 * @param int $count Max bytes per call
 * @return int Bytes transferred
 */
$ffi = FFI::cdef(<<<'CDEF'
typedef long ssize_t;
typedef unsigned long size_t;
ssize_t sendfile(int out_fd, int in_fd, long *offset, size_t count);
CDEF, "libc.so.6");

function fd_of($stream): int {
    $meta = stream_get_meta_data($stream);
    $name = $meta['uri'] ?? null;
    $st   = fopen($name, $meta['mode'] ?? 'r');
    return intval(FFI::fd($st)); // If your PHP lacks FFI::fd(), use stream_get_meta_data + cast or ext/ffi helpers.
}

// Example usage (CLI only):
$src = fopen(__FILE__, 'rb');
$dst = fopen('php://stdout', 'wb');

$offset = FFI::new('long');
$offset->cdata = 0;
$chunk = 1<<20;
$copied = $ffi->sendfile(fileno($dst), fileno($src), FFI::addr($offset), $chunk);
echo "\nCopied: $copied bytes\n";

Note: Obtaining file descriptors from PHP streams varies by version/SAPI. In production, bind through stable wrappers (Swoole, RoadRunner, custom extensions) or restrict this to CLI utilities where you can open FDs explicitly.


6) Safer Bindings Pattern: Central Loader + Thin Facades

Create a single loader that chooses library names per OS/arch and exposes typed facades. Example for libm:

<?php
/**
 * @internal Central FFI loader
 */
final class FfiLib {
    private static ?FFI $libm = null;

    public static function libm(): FFI {
        if (!self::$libm) {
            $name = PHP_OS_FAMILY === 'Darwin' ? 'libSystem.B.dylib' : 'libm.so.6';
            self::$libm = FFI::cdef("double erf(double); double erfc(double);", $name);
        }
        return self::$libm;
    }
}

/**
 * @function erf
 * @param float $x
 * @return float
 */
function erf(float $x): float { return FfiLib::libm()->erf($x); }

/**
 * @function erfc
 * @param float $x
 * @return float
 */
function erfc(float $x): float { return FfiLib::libm()->erfc($x); }

echo erf(1.0), " ", erfc(1.0), "\n";

Benefits: one place to manage portability and testing; userland functions stay clean.


7) Windows Example: GetTickCount64 (no PHP equivalent with this API shape)

<?php
/**
 * Windows kernel32 timing call
 *
 * @ffi-library kernel32.dll
 * @ffi-func unsigned long long GetTickCount64(void)
 * @return int Milliseconds since system start
 */
if (stripos(PHP_OS_FAMILY, 'Windows') !== false) {
    $ffi = FFI::cdef("unsigned long long GetTickCount64(void);", "kernel32.dll");
    echo "Uptime (ms): ", $ffi->GetTickCount64(), "\n";
}

8) Testing & Tooling Checklist

9) Bonus scripts

Here is a linux shell script you can use to identify functions and build the cdef stub for your PHP scripts.

#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'USAGE'
ffi-cdefgen.sh — List exported functions from a shared library and build PHP FFI cdef stubs.

Usage:
  # list function names
  ffi-cdefgen.sh /path/to/lib.so

  # generate cdef stub(s) for given function name(s)
  ffi-cdefgen.sh /path/to/lib.so func1 [func2 ...]

Env:
  CDEF_LIBNAME    Override the libname used in the generated FFI::cdef call.
USAGE
}

if [[ "${1:-}" == "-h" || "${1:-}" == "--help" || $# -lt 1 ]]; then
  usage; exit 0
fi

LIB="$1"; shift || true

if [[ ! -f "$LIB" ]]; then
  echo "error: library not found: $LIB" >&2
  exit 2
fi

have() { command -v "$1" >/dev/null 2>&1; }

extract_symbols() {
  local lib="$1"
  local symbols
  if have readelf; then
    symbols=$(readelf -Ws "$lib" | awk '$4=="FUNC" && $7 !~ /UND/ {print $8}')
  elif have nm; then
    symbols=$(nm -D --defined-only "$lib" | awk '$2 ~ /T|W/ {print $3}')
  elif have objdump; then
    symbols=$(objdump -T "$lib" | awk '$6 ~ /FUNC/ {print $7}')
  else
    echo "error: need readelf or nm or objdump in PATH" >&2
    exit 3
  fi
  echo "$symbols" | sed 's/@.*//' | grep -E '^[A-Za-z_][A-Za-z0-9_]*$' | sort -u
}

SYMS="$(extract_symbols "$LIB")"

# If no extra args: just list symbols.
if [[ $# -eq 0 ]]; then
  if [[ -z "$SYMS" ]]; then
    echo "No exported functions found (or not a shared library)." >&2
    exit 4
  fi
  echo "$SYMS"
  exit 0
fi

# Additional args are function names to generate stubs for.
FUNCS=("$@")

extract_proto_from_man() {
  local fn="$1"
  local sec
  for sec in 3 2; do
    if man -w "$sec" "$fn" >/dev/null 2>&1; then
      man "$sec" "$fn" | col -b | awk '
        BEGIN{flag=0}
        /^SYNOPSIS/{flag=1; next}
        /^[A-Z][A-Z ]+$/ { if(flag){flag=0} }
        flag { print }
      ' | sed '/^#/d' | sed 's/ *$//' | awk -v fn="$fn" '
        BEGIN{capture=0}
        /[A-Za-z_][A-Za-z0-9_[:space:]\*\(\),\[\]]*'"$fn"'[[:space:]]*\(/ {capture=1}
        capture { print }
        /;[[:space:]]*$/ { if(capture){ exit } }
      '
      return
    fi
  done
}

extract_proto_from_headers() {
  local fn="$1"
  local INCS
  INCS=$(cpp -v </dev/null 2>&1 | awk '/#include <...> search starts here:/{f=1;next} /End of search list./{f=0} f{print $1}')
  local HDRS CAND
  HDRS=$(grep -Rsl --include='*.h' -w "$fn" $INCS 2>/dev/null | head -n 8 || true)
  for h in $HDRS; do
    CAND=$(awk -v fn="$fn" '
      BEGIN{RS=";"; ORS=";\n"}
      $0 ~ "\\<"fn"\\s*\\(" {print}
    ' "$h" | head -n 1)
    if [[ -n "$CAND" ]]; then
      echo "$CAND"
      return
    fi
  done
}

clean_proto() {
  sed -E '
    s/__attribute__\s*\(\([^)]*\)\)//g;
    s/__THROW//g;
    s/__wur//g;
    s/__restrict|restrict//g;
    s/__const|const/const/g;
    s/\s+/ /g;
    s/^\s+//; s/\s+$//;
  ' | tr '\n' ' '
}

LIBNAME="${CDEF_LIBNAME:-$LIB}"

PROTOS=()
for FUNC in "${FUNCS[@]}"; do
  PROTO="$(extract_proto_from_man "$FUNC" || true)"
  if [[ -z "$PROTO" ]]; then
    PROTO="$(extract_proto_from_headers "$FUNC" || true)"
  fi
  if [[ -n "$PROTO" ]]; then
    ONE=$(printf "%s" "$PROTO" | clean_proto)
  else
    ONE="/* TODO: confirm signature on this platform */ int ${FUNC}(void);"
    # Warn if function not found in the export list
    if ! grep -qx "$FUNC" <<<"$SYMS"; then
      echo "warning: '$FUNC' not found among exported symbols for $LIB; stub is a placeholder." >&2
    fi
  fi
  PROTOS+=("$ONE")
done

# Emit combined cdef block
echo
echo "--- PHP FFI cdef stub(s) ---"
echo
echo "FFI::cdef(<<<'CDEF'"
for P in "${PROTOS[@]}"; do
  echo "$P"
done
echo "CDEF, '$LIBNAME');"
echo
echo "# Example:"
echo "# \$ffi = FFI::cdef(<<<'CDEF'"
for P in "${PROTOS[@]}"; do
  echo "# $P"
done
echo "# CDEF, '$LIBNAME');"

This can also be done in windows using powershell

param(
  [Parameter(Position=0, Mandatory=$true)]
  [string]$Lib,
  [Parameter(Position=1, ValueFromRemainingArguments=$true)]
  [string[]]$Functions,
  [string]$SdkIncludeRoot
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

if (!(Test-Path $Lib)) {
  throw "Library not found: $Lib"
}

# Inline C# to parse PE exports
Add-Type -TypeDefinition @"
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;

public class PeExports {
  private static UInt32 ReadUInt32(BinaryReader br) => br.ReadUInt32();
  private static UInt16 ReadUInt16(BinaryReader br) => br.ReadUInt16();

  public static List<string> GetExports(string path) {
    using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
    using (var br = new BinaryReader(fs)) {
      fs.Seek(0x3C, SeekOrigin.Begin);
      int peOffset = br.ReadInt32();
      fs.Seek(peOffset, SeekOrigin.Begin);
      uint peSig = br.ReadUInt32();
      if (peSig != 0x4550) throw new Exception("Not a PE file");

      UInt16 machine = br.ReadUInt16();
      UInt16 sections = br.ReadUInt16();
      UInt32 timeDateStamp = br.ReadUInt32();
      UInt32 symTablePtr = br.ReadUInt32();
      UInt32 symCount = br.ReadUInt32();
      UInt16 optHeaderSize = br.ReadUInt16();
      UInt16 characteristics = br.ReadUInt16();

      long optStart = fs.Position;
      UInt16 magic = br.ReadUInt16();
      bool isPE32Plus = (magic == 0x20b);
      fs.Seek(optStart + (isPE32Plus ? 108 : 92), SeekOrigin.Begin);
      UInt32 exportRVA = br.ReadUInt32();
      UInt32 exportSize = br.ReadUInt32();

      fs.Seek(peOffset + 4 + 20 + optHeaderSize, SeekOrigin.Begin);
      var sects = new List<Tuple<string, UInt32, UInt32, UInt32>>();
      for (int i=0; i<sections; i++) {
        byte[] nameb = br.ReadBytes(8);
        string name = Encoding.UTF8.GetString(nameb).TrimEnd('\0');
        UInt32 vSize = br.ReadUInt32();
        UInt32 vAddr = br.ReadUInt32();
        UInt32 rawSize = br.ReadUInt32();
        UInt32 rawPtr = br.ReadUInt32();
        br.BaseStream.Seek(16, SeekOrigin.Current);
        sects.Add(Tuple.Create(name, vAddr, rawPtr, rawSize));
      }

      Func<UInt32, UInt32> rva2ofs = (rva) => {
        foreach (var s in sects) {
          if (rva >= s.Item2 && rva < s.Item2 + s.Item4) {
            return s.Item3 + (rva - s.Item2);
          }
        }
        return 0;
      };

      if (exportRVA == 0) return new List<string>();

      fs.Seek(rva2ofs(exportRVA), SeekOrigin.Begin);
      UInt32 Characteristics = br.ReadUInt32();
      UInt32 TimeDateStamp = br.ReadUInt32();
      UInt16 MajorVersion = br.ReadUInt16();
      UInt16 MinorVersion = br.ReadUInt16();
      UInt32 NameRVA = br.ReadUInt32();
      UInt32 OrdinalBase = br.ReadUInt32();
      UInt32 NumberOfFunctions = br.ReadUInt32();
      UInt32 NumberOfNames = br.ReadUInt32();
      UInt32 AddressOfFunctions = br.ReadUInt32();
      UInt32 AddressOfNames = br.ReadUInt32();
      UInt32 AddressOfNameOrdinals = br.ReadUInt32();

      var names = new List<string>();
      UInt32 namesOfs = rva2ofs(AddressOfNames);
      UInt32 ordOfs = rva2ofs(AddressOfNameOrdinals);
      if (namesOfs == 0 || ordOfs == 0) return names;

      fs.Seek(namesOfs, SeekOrigin.Begin);
      for (int i=0; i<NumberOfNames; i++) {
        UInt32 nameRva = br.ReadUInt32();
        long ret = fs.Position;
        fs.Seek(rva2ofs(nameRva), SeekOrigin.Begin);
        var sb = new StringBuilder();
        byte b;
        while ((b = br.ReadByte()) != 0) sb.Append((char)b);
        names.Add(sb.ToString());
        fs.Seek(ret, SeekOrigin.Begin);
      }
      names.Sort(StringComparer.Ordinal);
      return names;
    }
  }
}
"@

# 1) List exports
$exports = [PeExports]::GetExports($Lib) | Where-Object { $_ -match '^[A-Za-z_][A-Za-z0-9_]*$' } | Sort-Object -Unique
if (!$exports -or $exports.Count -eq 0) {
  throw "No exported functions found."
}

# If no function args: just list function names and exit.
if (-not $Functions -or $Functions.Count -eq 0) {
  $exports | ForEach-Object { $_ } | Write-Output
  exit 0
}

function Find-PrototypeInHeaders {
  param([string]$Fn, [string]$Root)
  if (-not (Test-Path $Root)) { return $null }
  $hdrs = Get-ChildItem -Path $Root -Recurse -Filter *.h -ErrorAction SilentlyContinue
  foreach ($h in $hdrs) {
    try {
      $txt = Get-Content $h.FullName -Raw -ErrorAction Stop
    } catch { continue }
    $m = [regex]::Match($txt, "(?ms)^[^\n]*\b$([regex]::Escape($Fn))\s*\([^;]+;")
    if ($m.Success) { return $m.Value }
  }
  return $null
}

function Clean-Prototype {
  param([string]$s)
  if (-not $s) { return $null }
  $s = $s -replace "(?m)^\s*#.*$", ""
  $s = $s -replace "__declspec\([^)]+\)|DECLSPEC_[A-Z_]+", ""
  $s = $s -replace "\b(WINAPI|APIENTRY|CALLBACK|NTAPI)\b", "/* WINAPI */"
  $s = $s -replace "\b__in\b|\b__out\b|\b__inout\b", ""
  $s = $s -replace "_In_\b|_Out_\b|_Inout_\b|_Check_return_\b|_Success_\([^)]+\)", ""
  $s = $s -replace @"\\\r?\n"@, " "
  $s = $s -replace "\s+", " "
  $s = $s.Trim()
  return $s
}

# Determine SDK include root, if any
$SdkRoot = $SdkIncludeRoot
if (-not $SdkRoot) {
  $candidates = @(
    "C:\Program Files (x86)\Windows Kits\10\Include",
    "C:\Program Files\Microsoft SDKs\Windows"
  )
  foreach ($r in $candidates) { if (Test-Path $r) { $SdkRoot = $r; break } }
}

$protos = New-Object System.Collections.Generic.List[string]
foreach ($fn in $Functions) {
  $proto = $null
  if ($SdkRoot) { $proto = Find-PrototypeInHeaders -Fn $fn -Root $SdkRoot }
  $clean = Clean-Prototype $proto
  if (-not $clean) {
    $clean = "/* TODO: confirm signature from MSDN/headers; calling convention may be stdcall (WINAPI). */ int $fn(void);"
    if ($exports -notcontains $fn) {
      Write-Warning "'$fn' not found among exported symbols for $Lib; stub is a placeholder."
    }
  }
  $protos.Add($clean)
}

$cdefLib = $Env:CDEF_LIBNAME
if (-not $cdefLib) { $cdefLib = $Lib }

"--- PHP FFI cdef stub(s) ---`n"
"FFI::cdef(<<<'CDEF'"
foreach ($p in $protos) { $p }
"CDEF, '$cdefLib');`n"
"# Example:"
"# `$ffi = FFI::cdef(<<<'CDEF'"
foreach ($p in $protos) { "# $p" }
"# CDEF, '$cdefLib');" | Write-Output

Wrap‑up

With FFI you can integrate precise clock sources, OS health metrics, cryptographic entropy, and zero‑copy IO straight from PHP. Package those calls behind tiny, well‑documented facades and you'll get the best of both worlds: PHP's developer speed and native code's power.