Saltar al contenido principal

Introducción

PHP FFI (Foreign Function Interface) es una extensión de PHP que permite cargar bibliotecas compartidas, llamar a funciones C y acceder a estructuras de datos C. No hace falta crear una extensión de PHP propia: puedes usar directamente las API de C existentes desde PHP. La documentación oficial de PHP describe FFI como una funcionalidad de bajo nivel y peligrosa. Solo debería usarla quien entienda a fondo C y la API de la biblioteca objetivo.
FFI no es una API PHP habitual bien abstraída. Un manejo incorrecto de punteros, propiedad o funciones de liberación puede provocar crashes o corrupción de memoria.
FFI tampoco es una función «para acelerar». La documentación oficial de PHP indica que el acceso a estructuras de datos vía FFI es más lento que usar arrays u objetos nativos de PHP. El motivo para elegir FFI no es la velocidad, sino querer usar una biblioteca C existente desde PHP.

Entornos donde se puede usar FFI y limitaciones

La documentación oficial de PHP explica cómo habilitar la extensión FFI:
  • Compilar PHP con --with-ffi.
  • En Windows, habilitar php_ffi.dll en php.ini.
  • Controlar la disponibilidad con ffi.enable en php.ini.
ffi.enable puede tomar tres valores.
ValorSignificado
trueHabilita la API FFI.
falseDeshabilita la API FFI.
preloadPermite la API FFI solo en el CLI SAPI y en archivos ya precargados.
En entornos de servidor web es habitual ffi.enable=false o ffi.enable=preload. Incluso en hosting como Laravel Cloud, no se puede dar por hecho que se pueda usar FFI en una aplicación web normal.
Por tanto, aunque quieras usar FFI en una app Laravel, plantéate primero si puedes hacerlo desde una herramienta CLI o un proceso batch. Es más seguro evitar depender de que esté siempre disponible bajo FPM o Apache mod_php.

Uso básico

Los conceptos básicos de PHP FFI son cuatro: FFI::cdef(), FFI::new(), FFI::addr() y 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;
Puntos clave de este ejemplo:
  • FFI::cdef() crea el objeto FFI a partir de una cadena de declaraciones C y el nombre de la biblioteca compartida.
  • $ffi->new('struct timeval') reserva una estructura C.
  • FFI::addr($tv) produce un argumento puntero como struct timeval *.
  • Puedes acceder a los campos de la estructura con $tv->tv_sec.
Para APIs que devuelven cadenas o binarios, usa FFI::string().
<?php

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

// Se asume que la parte C escribe el puntero resultado en $jsonPtr
$result = $ffi->some_function(FFI::addr($jsonPtr));

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

$json = FFI::string($jsonPtr);
La memoria reservada con FFI::new() se libera normalmente con el conteo de referencias de PHP. Sin embargo, los punteros devueltos por una biblioteca C pueden requerir su propia función free. Comprueba, para cada API, de quién es la propiedad.

Cargar ficheros de cabecera

Con FFI::load() puedes cargar declaraciones desde un fichero de cabecera C. En la cabecera puedes escribir FFI_SCOPE y 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();
Ahora bien, la documentación oficial explica que ni FFI::cdef() ni FFI::load() admiten las directivas normales del preprocesador C. No puedes pasar #include, #define ni compilación condicional tal cual. Por eso, en producción se suele optar por una de estas dos vías:
1

Para APIs sencillas, escribir directamente en `FFI::cdef()`

Si hay pocos tipos y funciones dependientes, incluye solo las declaraciones necesarias en el propio PHP.
2

Para APIs complejas, prepara una cabecera preprocesada

Gestiona en un fichero aparte una cabecera pensada para FFI, con las macros y la compilación condicional ya eliminadas.

Caso real: VOICEVOX Core for PHP

VOICEVOX Core for PHP es un ejemplo real de cómo usar la biblioteca dinámica C de VOICEVOX CORE desde PHP puro. Reúne patrones prácticos de FFI, así que resulta más fácil de entender que las explicaciones abstractas.

1. Tener una cabecera adaptada a FFI

VOICEVOX Core no utiliza la cabecera original tal cual: en headers/voicevox_core_ffi.h se extraen las declaraciones necesarias para FFI. Se declaran de forma explícita los punteros opacos, las estructuras y las funciones de liberación.
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);
Lo importante de este diseño es que la estructura interna detallada de C no se expone al lado PHP: todo se encierra tras handles opacos y llamadas a funciones. El área que PHP tiene que manejar se reduce y las clases wrapper quedan más limpias.

2. Concentrar FFI::cdef() en un solo sitio

src/VoicevoxFFI.php concentra en un único lugar la carga de la cabecera y la resolución de la ruta de la biblioteca.
<?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 esta forma, tu aplicación no tiene que llamar directamente a FFI::cdef(). Las diferencias en la ruta de la biblioteca las absorbe una variable de entorno y una comprobación del SO.

3. Envolver los parámetros de salida en objetos

El constructor de src/Synthesizer.php reserva un struct VoicevoxSynthesizer* y pasa su dirección 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;
Este patrón es la forma canónica de recibir un Type **out_value de la API C en PHP.
  • Con new('struct VoicevoxSynthesizer*') creas la variable puntero.
  • Con FFI::addr($ptr) pasas un Type **.
  • El handle obtenido se guarda como propiedad de un objeto PHP.

4. Copia a cadena PHP la memoria devuelta por C y libérala enseguida

En cuanto VOICEVOX Core for PHP recibe una cadena JSON o binarios WAV, primero los copia a una cadena PHP con FFI::string() y después llama a la función de liberación del lado 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;
Este flujo de «copiar y liberar de inmediato» es uno de los patrones más importantes en FFI. Al no mezclar los buffers del lado C con el ciclo de vida de PHP, la gestión de la propiedad se simplifica.

Precauciones y buenas prácticas

AspectoRecomendación
Entorno de ejecuciónDiseña asumiendo primero un uso CLI.
Gestión de la cabeceraNo pases la cabecera original; mantén por separado una versión adaptada a FFI.
Ruta de la bibliotecaPermite alternar mediante rutas absolutas o variables de entorno.
Gestión de memoriaCopia a PHP con FFI::string() y llama siempre a la función de liberación propia de la biblioteca.
Diseño del wrapperNo expandas el CData bruto por toda la app; enciérralo en clases dedicadas.
CompatibilidadComprueba a la vez la versión de PHP y los cambios de ABI de la biblioteca objetivo.
Mezclar FFI directamente en el ciclo request/response de una app Laravel dificulta mucho tanto los cambios de entorno como el diagnóstico de fallos. Asegúrate primero de que funciona en comandos CLI o procesos puntuales fuera de las colas.
FFI es adecuado cuando ya existe una API C estable y no vale la pena crear una extensión propia de PHP. Al contrario, no es adecuado para funcionalidades que quieras servir desde hosting web normal ni para lógica que se pueda resolver únicamente con PHP.

Resumen

Con PHP FFI puedes invocar bibliotecas C existentes desde PHP puro. Pero FFI es un mecanismo de bajo nivel y peligroso, y no siempre está disponible en un servidor web. Al mirar la implementación de VOICEVOX Core for PHP, se ve que en la práctica hay cuatro puntos clave:
  1. Preparar una cabecera aparte pensada para FFI.
  2. Concentrar FFI::cdef() y la resolución de la ruta de la biblioteca en un único lugar.
  3. Envolver los punteros opacos en clases dedicadas.
  4. Copiar la memoria devuelta por C a PHP y liberarla mediante la función específica.
Si lees también la página de VOICEVOX Core for PHP, podrás ir y volver entre el concepto de FFI y el diseño real de un paquete para asentar el conocimiento.
Última modificación el 13 de julio de 2026