Passer au contenu principal

Introduction

PHP FFI (Foreign Function Interface) est une extension PHP permettant de charger des bibliothèques partagées, d’appeler des fonctions C et d’accéder à des structures de données C. Sans écrire votre propre extension PHP, vous pouvez utiliser directement des API C existantes depuis PHP. La documentation officielle PHP décrit FFI comme une fonctionnalité bas niveau et dangereuse. Seuls les développeurs qui comprennent le C et l’API de la bibliothèque cible devraient l’utiliser.
FFI n’est pas une API PHP ordinaire abstraite de manière sûre. Une erreur sur les pointeurs, la propriété (ownership) ou les fonctions de libération peut entraîner des crashes ou une corruption mémoire.
FFI n’est pas non plus « une fonctionnalité pour accélérer les choses ». La documentation officielle PHP explique que l’accès aux structures de données via FFI est plus lent que via les tableaux et objets PHP natifs. La raison de choisir FFI n’est pas la vitesse, mais l’envie d’utiliser une bibliothèque C existante depuis PHP.

Environnements et contraintes d’utilisation

La documentation officielle PHP décrit ainsi l’activation de l’extension FFI :
  • Compiler PHP avec --with-ffi
  • Sous Windows, activer php_ffi.dll dans php.ini
  • Contrôler la disponibilité via ffi.enable dans php.ini
ffi.enable peut prendre l’une des trois valeurs suivantes.
ValeurSignification
trueActive l’API FFI
falseDésactive l’API FFI
preloadAutorise l’API FFI uniquement pour la SAPI CLI et les fichiers préchargés
En environnement de serveur web, ffi.enable=false ou ffi.enable=preload est fréquent. Même sur des hébergeurs comme Laravel Cloud, l’utilisation de FFI dans une application web ordinaire n’est pas forcément possible.
Aussi, même pour utiliser FFI dans une application Laravel, envisagez d’abord si vous pouvez vous en tirer avec un outil CLI ou un traitement batch. Il est plus prudent d’éviter de compter sur un FFI activé en permanence sous FPM ou Apache mod_php.

Utilisation de base

Les bases de PHP FFI reposent sur quatre éléments : FFI::cdef(), FFI::new(), FFI::addr(), 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;
Points à retenir dans cet exemple :
  • FFI::cdef() crée un objet FFI depuis une chaîne de déclarations C et un nom de bibliothèque partagée
  • $ffi->new('struct timeval') alloue la structure de données C
  • FFI::addr($tv) crée un argument pointeur du type struct timeval *
  • On peut accéder aux champs de la structure comme $tv->tv_sec
Pour les API qui reçoivent des chaînes ou des binaires, utilisez FFI::string().
<?php

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

// Supposons que le C écrive un pointeur résultat dans $jsonPtr
$result = $ffi->some_function(FFI::addr($jsonPtr));

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

$json = FFI::string($jsonPtr);
La mémoire allouée par FFI::new() est habituellement libérée par le comptage de références PHP. À l’inverse, un pointeur retourné par une bibliothèque C se libère parfois via une fonction free propre à la bibliothèque. Vérifiez la propriété de chaque API.

Charger des fichiers d’en-tête

FFI::load() permet de charger des déclarations depuis un fichier d’en-tête C. Le fichier d’en-tête peut contenir FFI_SCOPE et 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();
Cependant, la documentation officielle PHP explique que ni FFI::cdef() ni FFI::load() n’acceptent les directives de préprocesseur C ordinaires. Vous ne pouvez pas passer tels quels #include, #define, ni de la compilation conditionnelle. En pratique, on choisit donc l’une de ces deux approches :
1

Pour une API simple, écrivez directement dans `FFI::cdef()`

Si les types et fonctions dépendants sont peu nombreux, intégrez uniquement les déclarations nécessaires dans le PHP.
2

Pour une API complexe, préparez un en-tête pré-traité

Gérez dans un fichier séparé un en-tête pour FFI, débarrassé des macros et de la compilation conditionnelle de l’en-tête d’origine.

Cas concret : VOICEVOX Core for PHP

VOICEVOX Core for PHP est un exemple concret d’utilisation en Pure PHP de la bibliothèque dynamique C VOICEVOX CORE. C’est un sujet plus facile à comprendre qu’une description abstraite car il rassemble des patterns d’utilisation pratique de FFI.

1. Avoir un en-tête préparé pour FFI

L’en-tête original de VOICEVOX Core n’est pas utilisé tel quel : on isole les déclarations dédiées à FFI dans headers/voicevox_core_ffi.h. On y explicite les pointeurs opaques, les structures et les fonctions de libération.
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);
L’important dans cette conception, c’est que les détails internes du C ne sont pas exposés au PHP : on les enferme derrière un handle opaque et des appels de fonctions. La surface exposée à PHP est réduite et les classes wrappers plus faciles à organiser.

2. Centraliser FFI::cdef() en un seul endroit

src/VoicevoxFFI.php centralise le chargement de l’en-tête et la résolution du chemin de la bibliothèque.
<?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',
        };
    }
}
Ainsi, l’application côté vôtre n’a plus besoin d’appeler directement FFI::cdef(). Les différences de chemin de bibliothèque sont absorbées par les variables d’environnement et la détection d’OS.

3. Envelopper les out parameters pour en faire des objets

Le constructeur de src/Synthesizer.php alloue un struct VoicevoxSynthesizer* et passe son adresse à 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;
Ce pattern est la forme de base pour recevoir en PHP un Type **out_value d’une API C.
  • Créer une variable pointeur avec new('struct VoicevoxSynthesizer*')
  • Passer Type ** avec FFI::addr($ptr)
  • Conserver le handle obtenu dans une propriété d’un objet PHP

4. Copier la mémoire renvoyée par C dans une chaîne PHP puis libérer immédiatement

VOICEVOX Core for PHP, dès qu’il reçoit une chaîne JSON ou un binaire WAV, la copie d’abord dans une chaîne PHP via FFI::string(), puis appelle la fonction free côté 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;
Ce flux « copier puis libérer immédiatement » est l’un des patterns d’implémentation les plus importants avec FFI. En ne mêlant pas les buffers du C au cycle de vie PHP, on simplifie la propriété.

Points d’attention et bonnes pratiques

AspectRecommandation
Environnement d’exécutionConcevoir d’abord pour CLI
Gestion des en-têtesNe pas passer l’en-tête d’origine ; gérer séparément une version dédiée à FFI
Chemin de bibliothèqueRendre commutable via chemin absolu ou variable d’environnement
Gestion mémoireCopier en PHP via FFI::string() puis appeler impérativement la fonction de libération dédiée
Conception du wrapperNe pas diffuser les CData bruts dans l’application ; les enfermer dans des classes dédiées
CompatibilitéVérifier ensemble la version PHP et les changements d’ABI de la bibliothèque cible
Mélanger directement FFI au cycle requête/réponse d’une application Laravel rend les différences d’environnement et le triage des incidents rapidement difficiles. Faites d’abord fonctionner le tout dans une commande CLI ou un processus ponctuel hors queue.
FFI est indiqué lorsque vous avez déjà une API C stable et que créer une extension PHP serait excessif. À l’inverse, il n’est pas indiqué pour des fonctionnalités qui doivent tourner sur un hébergement web ordinaire, ni pour du traitement qui peut se faire en PHP seul.

Récapitulatif

Avec PHP FFI, vous pouvez appeler des bibliothèques C existantes depuis Pure PHP. Toutefois, FFI est un mécanisme bas niveau et dangereux, qui n’est pas nécessairement toujours disponible sur un serveur web. En regardant l’implémentation de VOICEVOX Core for PHP, on constate qu’en pratique quatre points sont essentiels :
  1. Préparer un en-tête dédié à FFI
  2. Centraliser FFI::cdef() et la résolution de chemin de bibliothèque en un seul endroit
  3. Envelopper les pointeurs opaques dans des classes dédiées
  4. Copier la mémoire renvoyée par C en PHP puis la libérer avec la fonction dédiée
Lire aussi la page de VOICEVOX Core for PHP permet de faire des allers-retours entre le concept FFI et la conception concrète d’un package.
Dernière modification le 13 juillet 2026