Vai al contenuto principale

Introduzione

PHP FFI (Foreign Function Interface) è un’estensione di PHP che permette di caricare librerie condivise, chiamare funzioni C e accedere a strutture dati C. Senza scrivere estensioni PHP native puoi usare direttamente da PHP API C già esistenti. La documentazione ufficiale di PHP descrive FFI come una funzionalità di basso livello e pericolosa. Va usata solo da chi conosce a fondo C e l’API della libreria di destinazione.
FFI non è la solita API PHP astratta in modo sicuro. Errori con puntatori, ownership o funzioni di liberazione possono causare crash o corruzione della memoria.
FFI non è nemmeno “una funzione per velocizzare”. Anche la documentazione ufficiale spiega che l’accesso alle strutture dati via FFI è più lento rispetto ad array e oggetti PHP nativi. Il motivo per scegliere FFI non è la velocità: è poter usare da PHP librerie C già esistenti.

Ambienti in cui FFI è utilizzabile e limitazioni

La documentazione ufficiale di PHP indica i seguenti modi per abilitare l’estensione FFI.
  • Compilare PHP con --with-ffi
  • Su Windows abilitare php_ffi.dll in php.ini
  • Controllare l’abilitazione con ffi.enable in php.ini
ffi.enable accetta i seguenti tre valori.
ValoreSignificato
trueAbilita l’API FFI
falseDisabilita l’API FFI
preloadConsente l’API FFI solo per la SAPI CLI e per i file preloaded
Sui web server è comune trovare ffi.enable=false o ffi.enable=preload. Anche sugli hosting (incluso Laravel Cloud) non è detto che tu possa usare FFI in una normale web app.
Perciò, anche in un’app Laravel, per prima cosa considera se il caso d’uso può essere risolto con tool CLI o batch. Meglio evitare di dare per scontato che FFI sia sempre attivo su FPM o Apache mod_php.

Uso di base

Le basi di PHP FFI sono quattro: FFI::cdef(), FFI::new(), FFI::addr() e 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;
Punti chiave dell’esempio:
  • FFI::cdef() crea un oggetto FFI da una stringa di dichiarazioni C e dal nome di una libreria condivisa
  • $ffi->new('struct timeval') alloca una struttura dati C
  • FFI::addr($tv) produce un argomento puntatore come struct timeval *
  • Puoi accedere ai campi della struttura con $tv->tv_sec
Per API che restituiscono stringhe o binari usa FFI::string().
<?php

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

// Si presume che il lato C scriva su $jsonPtr il puntatore al risultato
$result = $ffi->some_function(FFI::addr($jsonPtr));

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

$json = FFI::string($jsonPtr);
La memoria allocata con FFI::new() viene rilasciata dal reference counting di PHP. I puntatori restituiti da una libreria C, invece, richiedono spesso di essere liberati con la specifica funzione free della libreria. Verifica caso per caso a chi appartiene la memoria.

Caricare file header

Con FFI::load() puoi caricare le dichiarazioni da un file header C. Nell’header puoi scrivere FFI_SCOPE e 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();
Tuttavia, la documentazione ufficiale precisa che né FFI::cdef()FFI::load() supportano le normali direttive del preprocessore C. #include, #define, la compilazione condizionale non possono essere passate così come sono. Per questo, nella pratica, si sceglie una delle due strade seguenti.
1

Per API semplici scrivi direttamente in `FFI::cdef()`

Se i tipi e le funzioni da cui dipendi sono pochi, incorpori le sole dichiarazioni necessarie lato PHP.
2

Per API complesse prepara un header pre-processato

Gestisci in un file separato un header dedicato all’FFI, ripulito da macro e compilazione condizionale.

Caso d’uso reale: VOICEVOX Core for PHP

VOICEVOX Core for PHP è un esempio concreto che usa la libreria dinamica C di VOICEVOX CORE da PHP puro. Raccoglie pattern pratici di FFI, quindi è un caso di studio più concreto delle spiegazioni astratte.

1. Un header preparato per FFI

L’header originale di VOICEVOX Core non viene usato così com’è: si estraggono le dichiarazioni utili in headers/voicevox_core_ffi.h, che espone puntatori opachi, struct e funzioni di free.
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);
Il punto importante è non esporre lato PHP i dettagli interni del C: si mantengono solo handle opachi e chiamate a funzione. La superficie da gestire in PHP è minore e le classi wrapper sono più ordinate.

2. Concentrare FFI::cdef() in un unico punto

src/VoicevoxFFI.php concentra in un solo punto il caricamento dell’header e la risoluzione del path della libreria.
<?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',
        };
    }
}
Con questa forma non chiami direttamente FFI::cdef() dall’applicazione. Le differenze di path della libreria vengono assorbite da variabile d’ambiente e detection dell’OS.

3. Wrappare gli out parameter in oggetti

Il costruttore in src/Synthesizer.php alloca struct VoicevoxSynthesizer* e passa l’indirizzo a 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;
È il pattern base per ricevere in PHP i Type **out_value delle API C.
  • Crea una variabile puntatore con new('struct VoicevoxSynthesizer*')
  • Passa Type ** con FFI::addr($ptr)
  • Conserva l’handle ottenuto come proprietà dell’oggetto PHP

4. Copia la memoria restituita dal C in una stringa PHP e libera subito

VOICEVOX Core for PHP, quando riceve stringhe JSON o binari WAV, li copia prima in una stringa PHP con FFI::string() e poi chiama subito la funzione di free lato C.
<?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;
Questo flusso “copia e libera subito” è uno dei pattern più importanti in FFI. Non mescolando i buffer C con il ciclo di vita di PHP, l’ownership resta semplice.

Note e best practice

AspettoPunto pratico
Ambiente d’esecuzioneProgetta prima di tutto per la CLI
Gestione degli headerNon passare l’header originale; gestisci separatamente dichiarazioni preparate per FFI
Path della libreriaRendilo configurabile via path assoluto o variabile d’ambiente
Gestione della memoriaCopia in PHP con FFI::string() e chiama sempre la funzione di free specifica della libreria
Design del wrapperNon diffondere il CData grezzo nell’app: chiudilo in una classe dedicata
CompatibilitàVerifica insieme la versione di PHP e le eventuali variazioni di ABI della libreria
Innestare direttamente FFI nel ciclo request/response di un’app Laravel rende improvvisamente più difficili le differenze d’ambiente e il triage dei problemi. Prima fai funzionare tutto in comandi CLI o processi puntuali diversi dalla queue.
FFI è adatto quando c’è già un’API C stabile ma non vale la pena scrivere un’estensione PHP. Al contrario non è adatto a funzionalità che vuoi far girare su hosting web standard, o a lavorazioni che si esauriscono in PHP. Con PHP FFI puoi chiamare da PHP puro librerie C già esistenti. Ma FFI è un meccanismo di basso livello e pericoloso, e non è detto che sia utilizzabile su un web server. Guardando l’implementazione di VOICEVOX Core for PHP si nota che nella pratica sono importanti quattro punti.
  1. Preparare un header dedicato all’FFI
  2. Concentrare in un solo punto FFI::cdef() e la risoluzione del path della libreria
  3. Incapsulare i puntatori opachi in classi dedicate
  4. Copiare in PHP la memoria restituita dal C e liberarla con la funzione specifica
Se leggi anche la pagina di VOICEVOX Core for PHP, puoi alternare tra concetti FFI e design reale di un pacchetto per una comprensione più profonda.
Ultima modifica il 13 luglio 2026