Skip to main content

Introduction

PHP FFI (Foreign Function Interface) is a PHP extension for loading shared libraries, calling C functions, and accessing C data structures. You can use existing C APIs directly from PHP without building your own PHP extension. The official PHP documentation describes FFI as a low-level and potentially dangerous feature. Only developers who understand C and the target library API should use it.
FFI is not a regular PHP API with safe abstractions. Mistakes with pointers, ownership, or deallocation functions can cause crashes or memory corruption.
FFI is also not a feature for speed. The official PHP documentation explains that FFI data structure access is slower than native PHP arrays or objects. You choose FFI for interoperability with existing C libraries, not performance.

Supported Environments and Restrictions

The official PHP documentation explains FFI enablement as follows.
  • Build PHP with --with-ffi
  • Enable php_ffi.dll in php.ini on Windows
  • Control availability with ffi.enable in php.ini
ffi.enable accepts these three values.
ValueMeaning
trueEnable the FFI API
falseDisable the FFI API
preloadAllow the FFI API only in CLI SAPI and preloaded files
In web server environments, ffi.enable=false or ffi.enable=preload is common. In hosting environments, including Laravel Cloud, FFI may not be available for regular web applications.
Because of this, even when you use FFI in a Laravel app, first verify whether your use case works as a CLI tool or batch process. It is safer to avoid assumptions that FFI is always enabled under FPM or Apache mod_php.

Basic Usage

The core PHP FFI methods are FFI::cdef(), FFI::new(), FFI::addr(), and FFI::string().
<?php

$ffi = FFI::cdef(<<<'CDEF'
typedef unsigned int time_t;
typedef unsigned int suseconds_t;

struct timeval {
    time_t tv_sec;
    suseconds_t tv_usec;
};

struct timezone {
    int tz_minuteswest;
    int tz_dsttime;
};

int gettimeofday(struct timeval *tv, struct timezone *tz);
CDEF, 'libc.so.6');

$tv = $ffi->new('struct timeval');
$tz = $ffi->new('struct timezone');

$ffi->gettimeofday(FFI::addr($tv), FFI::addr($tz));

echo $tv->tv_sec.PHP_EOL;
Key points in this example are:
  • FFI::cdef() creates an FFI object from a C declaration string and a shared library name
  • $ffi->new('struct timeval') allocates a C data structure
  • FFI::addr($tv) creates a pointer argument like struct timeval *
  • You can access struct fields like $tv->tv_sec
For APIs that return strings or binary data, use FFI::string().
<?php

$jsonPtr = $ffi->new('char*');

// Assume the C side writes a result pointer into $jsonPtr
$result = $ffi->some_function(FFI::addr($jsonPtr));

if ($result !== 0) {
    throw new RuntimeException('C API call failed.');
}

$json = FFI::string($jsonPtr);
Memory allocated by FFI::new() is usually released through PHP reference counting. In contrast, pointers returned by C libraries may need a library-specific free function. Check ownership per API.

Loading Header Files

With FFI::load(), you can load declarations from a C header file. Header files can include FFI_SCOPE and FFI_LIB.
#define FFI_SCOPE "mylib"
#define FFI_LIB "/absolute/path/to/libmylib.so"

typedef struct MyContext MyContext;

MyContext *mylib_new(void);
void mylib_delete(MyContext *context);
<?php

FFI::load(__DIR__.'/mylib.h');

$ffi = FFI::scope('mylib');
$context = $ffi->mylib_new();
However, the official PHP documentation explains that normal C preprocessor directives are not supported with either FFI::cdef() or FFI::load(). You cannot pass #include, #define, or conditional compilation directives as-is. Because of this, production code usually chooses one of these approaches.
1

Use `FFI::cdef()` directly for simple APIs

If dependencies are small, embed only the declarations you need on the PHP side.
2

Prepare a preprocessed header for complex APIs

Maintain a separate FFI header that removes macros and conditional compilation from the original header.

Real-world Use Case: VOICEVOX Core for PHP

VOICEVOX Core for PHP is a practical example of using the VOICEVOX CORE C dynamic library from pure PHP. It is a concrete reference for practical FFI patterns.

1. Maintain an FFI-oriented header

Instead of using the original VOICEVOX Core header directly, declarations for FFI are extracted into headers/voicevox_core_ffi.h. This defines opaque pointers, structs, and free functions explicitly.
typedef struct VoicevoxSynthesizer VoicevoxSynthesizer;

typedef struct VoicevoxInitializeOptions {
    int32_t  acceleration_mode;
    uint16_t cpu_num_threads;
} VoicevoxInitializeOptions;

int32_t voicevox_synthesizer_new(
    const struct VoicevoxOnnxruntime *onnxruntime,
    const struct OpenJtalkRc *open_jtalk,
    struct VoicevoxInitializeOptions options,
    struct VoicevoxSynthesizer **out_synthesizer
);

void voicevox_synthesizer_delete(struct VoicevoxSynthesizer *synthesizer);
void voicevox_json_free(char *json);
void voicevox_wav_free(uint8_t *wav);
The important point in this design is that detailed internal C structures are hidden from PHP. Opaque handles and function calls keep the PHP-side surface area small and wrapper classes easier to maintain.

2. Centralize FFI::cdef() in one place

src/VoicevoxFFI.php centralizes header loading and library path resolution.
<?php

class VoicevoxFFI
{
    private static ?FFI $ffi = null;

    public static function getInstance(): FFI
    {
        return self::$ffi ??= FFI::cdef(
            file_get_contents(__DIR__.'/../headers/voicevox_core_ffi.h'),
            self::getLibraryPath(),
        );
    }

    public static function getLibraryPath(): string
    {
        if ($path = getenv('VOICEVOX_CORE_LIB_PATH')) {
            return $path;
        }

        return match (PHP_OS_FAMILY) {
            'Darwin' => 'libvoicevox_core.dylib',
            'Windows' => 'voicevox_core.dll',
            default => 'libvoicevox_core.so',
        };
    }
}
With this shape, your application code does not call FFI::cdef() directly. Library path differences are also absorbed through environment variables and OS detection.

3. Wrap out parameters into objects

The constructor in src/Synthesizer.php allocates struct VoicevoxSynthesizer* and passes its address into voicevox_synthesizer_new().
<?php

$options = $this->ffi->voicevox_make_default_initialize_options();
$options->acceleration_mode = $accelerationMode->value;
$options->cpu_num_threads = $cpuNumThreads;

$ptr = $this->ffi->new('struct VoicevoxSynthesizer*');

$result = $this->ffi->voicevox_synthesizer_new(
    $onnxruntime->handle(),
    $openJtalk->handle(),
    $options,
    FFI::addr($ptr),
);

$this->handle = $ptr;
This pattern is the baseline way to receive Type **out_value in PHP with a C API.
  • Create a pointer variable with new('struct VoicevoxSynthesizer*')
  • Pass Type ** via FFI::addr($ptr)
  • Store the acquired handle in a PHP object property

4. Copy C-returned memory into PHP strings and free immediately

VOICEVOX Core for PHP receives JSON strings and WAV binary data, copies them into PHP strings with FFI::string(), and then calls the C-side free functions.
<?php

$jsonPtr = $this->ffi->voicevox_synthesizer_create_metas_json($this->handle);

$json = FFI::string($jsonPtr);
$this->ffi->voicevox_json_free($jsonPtr);

return $json;
<?php

$wavSize = $this->ffi->new('uint64_t');
$wavPtr = $this->ffi->new('uint8_t*');

$result = $this->ffi->voicevox_synthesizer_tts(
    $this->handle,
    $text,
    $styleId,
    $options,
    FFI::addr($wavSize),
    FFI::addr($wavPtr),
);

$wav = FFI::string($wavPtr, (int) $wavSize->cdata);
$this->ffi->voicevox_wav_free($wavPtr);

return $wav;
This copy-and-free flow is one of the most important implementation patterns in FFI. It simplifies ownership by keeping C buffers out of the PHP lifecycle.

Cautions and Best Practices

PerspectivePractical Point
Runtime environmentDesign for CLI-first usage
Header managementDo not pass original headers directly; maintain FFI-shaped declarations separately
Library pathMake switching possible with absolute paths or environment variables
Memory managementCopy to PHP with FFI::string() and always call the library-specific free function
Wrapper designDo not spread raw CData across the app; isolate it in dedicated classes
CompatibilityCheck both PHP version changes and target library ABI changes
If you mix FFI directly into the Laravel request/response cycle, environment differences and failure isolation become much harder. Start with CLI commands or other short-lived processes.
FFI is a good fit when you already have a stable C API and do not need to build a custom PHP extension. It is not a good fit for features that must run in standard web hosting or for logic that can be completed purely in PHP.

Summary

With PHP FFI, you can call existing C libraries from pure PHP. However, FFI is a low-level and dangerous mechanism, and it may not always be available in web server environments. The VOICEVOX Core for PHP implementation highlights four practical priorities.
  1. Prepare a dedicated header for FFI
  2. Centralize FFI::cdef() and library path resolution
  3. Wrap opaque pointers with dedicated classes
  4. Copy C-returned memory into PHP and free it with dedicated functions
If you read the VOICEVOX Core for PHP page together with this guide, you can understand both the FFI concepts and practical package design patterns.
Last modified on May 17, 2026