Skip to main content

前言

PHP FFI(Foreign Function Interface)是用來載入共享函式庫、呼叫 C 函式並存取 C 資料結構的 PHP 擴充功能。即便不自行撰寫 PHP 擴充,也能從 PHP 直接使用既有的 C API。 PHP 官方文件將 FFI 描述為低階且危險的功能。它是應該僅由理解 C 及目標函式庫 API 的開發者使用的功能。
FFI 並非安全抽象化的一般 PHP API。若對指標、擁有權、釋放函式的使用有誤,可能導致當機或記憶體毀損。
FFI 也不是「用來加速的功能」。PHP 官方文件也指出,透過 FFI 存取資料結構會比原生的 PHP 陣列或物件慢。選擇 FFI 的理由不在速度,而在於希望從 PHP 使用既有的 C 函式庫。

FFI 可用的環境與限制

PHP 官方文件關於啟用 FFI 擴充的方式說明如下。
  • --with-ffi 建置 PHP
  • 於 Windows 中在 php.ini 啟用 php_ffi.dll
  • php.iniffi.enable 控制可否使用
ffi.enable 可取以下三種值。
於 Web 伺服器環境中,多半採用 ffi.enable=falseffi.enable=preload。包含 Laravel Cloud 在內的 Hosting 環境,通常也不保證能以一般 Web 應用程式的方式使用 FFI。
因此,即便在 Laravel 應用程式中使用 FFI,也請先考量是否可在 CLI 工具或批次處理中成立。避免以「於 FPM 或 Apache mod_php 中常時啟用」為前提會較為安全。

基本用法

PHP FFI 的核心是 FFI::cdef()FFI::new()FFI::addr()FFI::string() 這 4 個。
此例中的重點如下。
  • FFI::cdef() 從 C 的宣告字串與共享函式庫名稱建立 FFI 物件
  • $ffi->new('struct timeval') 配置 C 的資料結構
  • FFI::addr($tv) 建立如 struct timeval * 這樣的指標引數
  • 可以像 $tv->tv_sec 一樣存取結構體欄位
於接收字串或二進位資料的 API 中,使用 FFI::string()
FFI::new() 配置的記憶體通常會透過 PHP 的參考計數釋放。反之,C 函式庫回傳的指標則可能需以函式庫專用的 free 函式來釋放。請針對每個 API 確認擁有權歸屬。

讀入標頭檔

使用 FFI::load() 可以從 C 標頭檔載入宣告。標頭檔中可以寫 FFI_SCOPEFFI_LIB
不過 PHP 官方文件也指出,FFI::cdef()FFI::load() 皆無法使用常見的 C 前處理指示。無法直接傳入 #include#define、條件式編譯等。 因此於實務中,通常會選擇下列其中一種方式。
1

若 API 簡單則直接寫在 `FFI::cdef()`

若相依的型別或函式較少,僅將所需的宣告嵌入至 PHP 端。
2

若 API 複雜則準備前處理過的標頭

以另一份檔案管理已移除 macro 與條件式編譯、專供 FFI 使用的標頭。

實務案例:VOICEVOX Core for PHP

VOICEVOX Core for PHP 是從 Pure PHP 使用 VOICEVOX CORE 之 C 動態函式庫的實例。彙整了 FFI 的實務模式,比抽象說明更容易理解。

1. 準備已為 FFI 整理的標頭

VOICEVOX Core 原始標頭並非直接使用,而是將 FFI 用的宣告切出至 headers/voicevox_core_ffi.h。這裡明確定義了不透明指標、結構體與 free 函式。
這種設計的重點在於:不將 C 詳細的內部結構暴露至 PHP 端,而是收斂於不透明 handle 與函式呼叫。PHP 端需處理的面積會變小,包裝類別也會更好整理。

2. 將 FFI::cdef() 集中於一處

src/VoicevoxFFI.php 將標頭讀入與函式庫路徑解析集中於一處。
以此形式,您的應用程式端就不必直接呼叫 FFI::cdef()。函式庫路徑的差異也能透過環境變數與 OS 判斷吸收。

3. 將 out parameter 包裝為物件

src/Synthesizer.php 的建構函式會配置 struct VoicevoxSynthesizer*,並將其位址傳入 voicevox_synthesizer_new()
此模式為以 PHP 接收 C API 中 Type **out_value 的基本形式。
  • new('struct VoicevoxSynthesizer*') 建立指標變數
  • FFI::addr($ptr) 傳入 Type **
  • 將取得的 handle 保存於 PHP 物件的屬性

4. 將 C 回傳的記憶體複製到 PHP 字串後立即釋放

VOICEVOX Core for PHP 在收到 JSON 字串或 WAV 二進位資料時,會先以 FFI::string() 複製到 PHP 字串,接著再呼叫 C 端的 free 函式。
此「複製並立即釋放」流程是 FFI 中最重要的實作模式之一。將 C 端的緩衝區與 PHP 的生命週期切開,可簡化擁有權。

注意事項與最佳實踐

將 FFI 直接混入 Laravel 應用程式的 request/response 週期,會使環境差異與問題切分突然變得困難。請先讓其於 CLI 指令或非 Queue 的單次執行程序中運作。
FFI 適合已具備穩定 C API、但不至於自行製作 PHP 擴充的情境。反之,若希望在一般 Web Hosting 中運作的功能,或僅以 PHP 即可完成的處理則不適合。

總結

透過 PHP FFI,您可以從 Pure PHP 呼叫既有的 C 函式庫。但 FFI 是低階且危險的機制,也未必能常時可用於 Web 伺服器。 觀察 VOICEVOX Core for PHP 的實作可看出,實務上以下 4 點相當重要。
  1. 另行準備 FFI 用的標頭
  2. FFI::cdef() 與函式庫路徑解析集中於一處
  3. 以專用類別包裝不透明指標
  4. 將 C 回傳的記憶體先複製至 PHP 後,再以專用函式釋放
搭配 VOICEVOX Core for PHP 頁面閱讀,可以在 FFI 概念與實際套件設計之間往返理解。
最後修改於 2026年8月2日