メインコンテンツへスキップ

はじめに

PHP FFI(Foreign Function Interface)は、共有ライブラリを読み込み、C 関数を呼び出し、C のデータ構造へアクセスするための PHP 拡張です。PHP 拡張を自作しなくても、既存の C API を PHP から直接使えます。 PHP 公式ドキュメントは、FFI を低レベルで危険な機能として説明しています。C と対象ライブラリの API を理解した開発者だけが使うべき機能です。
FFI は安全に抽象化された通常の PHP API ではありません。ポインタ、所有権、解放関数を誤るとクラッシュやメモリ破壊につながります。
FFI は「速くするための機能」でもありません。PHP 公式ドキュメントでも、FFI のデータ構造アクセスはネイティブな PHP 配列やオブジェクトより遅いと説明されています。FFI を選ぶ理由は速度ではなく、既存の C ライブラリを PHP から使いたいことです。

FFI が使える環境と制限

PHP 公式ドキュメントでは、FFI 拡張を有効にする方法を次のように説明しています。
  • PHP を --with-ffi 付きでビルドする
  • Windows では php.iniphp_ffi.dll を有効にする
  • php.iniffi.enable で使用可否を制御する
ffi.enable は次の 3 つの値を取ります。
設定値意味
trueFFI API を有効にする
falseFFI API を無効にする
preloadCLI SAPI と preload 済みファイルだけで FFI API を許可する
Web サーバー環境では ffi.enable=falseffi.enable=preload が使われることが多いです。Laravel Cloud を含むホスティング環境でも、通常の Web アプリとして FFI を使えるとは限りません。
そのため、Laravel アプリで FFI を使う場合でも、まずは CLI ツールやバッチ処理で成立するかを考えてください。FPM や Apache mod_php で常時有効にする前提は避けたほうが安全です。

基本的な使い方

PHP FFI の基本は FFI::cdef()FFI::new()FFI::addr()FFI::string() の 4 つです。
<?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;
この例で押さえる点は次のとおりです。
  • FFI::cdef() は C の宣言文字列と共有ライブラリ名から FFI オブジェクトを作る
  • $ffi->new('struct timeval') は C のデータ構造を確保する
  • FFI::addr($tv)struct timeval * のようなポインタ引数を作る
  • $tv->tv_sec のように構造体フィールドへアクセスできる
文字列やバイナリを受け取る API では FFI::string() を使います。
<?php

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

// C 側が $jsonPtr に結果ポインタを書き込む想定
$result = $ffi->some_function(FFI::addr($jsonPtr));

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

$json = FFI::string($jsonPtr);
FFI::new() で確保したメモリは、通常は PHP の参照カウントで解放されます。一方で、C ライブラリが返したポインタはライブラリ専用の free 関数で解放する場合があります。どちらの所有権なのかを API ごとに確認してください。

ヘッダーファイルの読み込み

FFI::load() を使うと、C ヘッダーファイルから宣言を読み込めます。ヘッダーファイルには FFI_SCOPEFFI_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();
ただし、PHP 公式ドキュメントは FFI::cdef()FFI::load() のどちらでも通常の C プリプロセッサ命令は使えないと説明しています。#include#define、条件付きコンパイルをそのまま渡すことはできません。 そのため、実運用では次のどちらかを選ぶことが多いです。
1

単純な API なら `FFI::cdef()` に直接書く

依存する型や関数が少ないなら、必要な宣言だけを PHP 側へ埋め込みます。
2

複雑な API なら前処理済みヘッダーを用意する

元のヘッダーからマクロや条件付きコンパイルを取り除いた、FFI 用のヘッダーを別ファイルで管理します。

実際のユースケース: VOICEVOX Core for PHP

VOICEVOX Core for PHP は、VOICEVOX CORE の C 動的ライブラリを Pure PHP から使う実例です。FFI の実用パターンがまとまっているので、抽象的な説明より理解しやすい題材です。

1. FFI 用に整形したヘッダーを持つ

VOICEVOX Core の元ヘッダーはそのままでは使わず、headers/voicevox_core_ffi.h に FFI 向けの宣言を切り出しています。ここでは不透明ポインタ、構造体、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);
この設計で重要なのは、C の詳細な内部構造を PHP 側へ見せずに、不透明ハンドルと関数呼び出しに閉じ込めていることです。PHP から扱う面積が小さくなり、ラッパークラスも整理しやすくなります。

2. FFI::cdef() を 1 箇所へ集約する

src/VoicevoxFFI.php は、ヘッダー読み込みとライブラリパス解決を 1 箇所へ集約しています。
<?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',
        };
    }
}
この形にすると、あなたのアプリ側は FFI::cdef() を直接呼ばずに済みます。ライブラリパスの差分も、環境変数と OS 判定で吸収できます。

3. out parameter をラップしてオブジェクト化する

src/Synthesizer.php のコンストラクタは、struct VoicevoxSynthesizer* を確保し、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;
このパターンは、C API の Type **out_value を PHP で受ける基本形です。
  • new('struct VoicevoxSynthesizer*') でポインタ変数を作る
  • FFI::addr($ptr)Type ** を渡す
  • 取得したハンドルを PHP オブジェクトのプロパティに保持する

4. C が返したメモリは PHP 文字列へコピーしてすぐ解放する

VOICEVOX Core for PHP は、JSON 文字列や WAV バイナリを受け取ったら、まず FFI::string() で PHP 文字列へコピーし、その後で C 側の free 関数を呼んでいます。
<?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;
この「コピーして即解放」の流れは、FFI で最も重要な実装パターンの 1 つです。C 側のバッファを PHP のライフサイクルへ混ぜないことで、所有権を単純化できます。

注意点とベストプラクティス

観点実践ポイント
実行環境まず CLI 前提で設計する
ヘッダー管理元ヘッダーをそのまま渡さず、FFI 用に整形した宣言を別管理する
ライブラリパス絶対パスや環境変数で切り替えられるようにする
メモリ管理FFI::string() で PHP にコピーし、ライブラリ専用の解放関数を必ず呼ぶ
ラッパー設計生の CData をアプリ全体へ広げず、専用クラスへ閉じ込める
互換性PHP バージョンと対象ライブラリの ABI 変更を一緒に確認する
Laravel アプリの request/response サイクルに FFI を直接混ぜると、環境差分と障害切り分けが急に難しくなります。まずは CLI コマンドやキュー以外の単発プロセスで成立させてください。
FFI が向いているのは、すでに安定した C API があり、PHP 拡張を自作するほどではないケースです。逆に、通常の Web ホスティングで動かしたい機能や、PHP だけで完結できる処理には向いていません。

まとめ

PHP FFI を使うと、あなたは既存の C ライブラリを Pure PHP から呼び出せます。ただし、FFI は低レベルで危険な仕組みです。Web サーバーで常に使えるとも限りません。 VOICEVOX Core for PHP の実装を見ると、実践では次の 4 点が重要だと分かります。
  1. FFI 用のヘッダーを別に用意する
  2. FFI::cdef() とライブラリパス解決を 1 箇所へ集約する
  3. 不透明ポインタを専用クラスでラップする
  4. C が返したメモリは PHP にコピーしてから専用関数で解放する
VOICEVOX Core for PHP のページも合わせて読むと、FFI の概念と実際のパッケージ設計を往復しながら理解できます。
Last modified on May 16, 2026