跳转到主要内容

前言

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 扩展的方法如下。
  • 编译 PHP 时带上 --with-ffi
  • 在 Windows 上,通过 php.ini 启用 php_ffi.dll
  • 通过 php.iniffi.enable 控制是否允许使用
ffi.enable 有以下 3 个值:
在 Web 服务器环境中,通常会设置 ffi.enable=falseffi.enable=preload。包括 Laravel Cloud 在内的托管环境,也不能保证在常规 Web 应用中都能使用 FFI。
因此,即使要在 Laravel 应用中使用 FFI,也应优先考虑 CLI 工具或批处理场景。以 FPM 或 Apache mod_php 常态启用为前提并不安全。

基本用法

PHP FFI 的基本 API 有 FFI::cdef()FFI::new()FFI::addr()FFI::string() 四个。
这个例子中要点如下:
  • 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 则准备一份预处理过的头文件

从原始头文件中剥离宏与条件编译,另存为专供 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 函数。
这种设计的关键在于:不向 PHP 侧暴露 C 的细节内部结构,而是收敛到不透明句柄与函数调用。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 **
  • 将得到的句柄保存到 PHP 对象的属性中

4. C 返回的内存拷贝到 PHP 字符串后立即释放

VOICEVOX Core for PHP 在收到 JSON 字符串或 WAV 二进制时,会先用 FFI::string() 拷贝为 PHP 字符串,然后再调用 C 侧的 free 函数。
这种「拷贝后立即释放」的流程是 FFI 中最重要的实现模式之一。不将 C 侧缓冲区混入 PHP 的生命周期,能极大简化所有权管理。

注意事项与最佳实践

将 FFI 直接混入 Laravel 应用的请求/响应循环,会让环境差异与故障定位骤然变难。请先在 CLI 命令或队列外的一次性进程中让它跑通。
FFI 适用的场景是:已有稳定的 C API,但又不至于要专门写 PHP 扩展。反过来,若想在通用 Web 托管环境中运行,或者可以完全用 PHP 完成的处理,就不适合使用 FFI。

小结

使用 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年7月18日