前言
PHP FFI(Foreign Function Interface)是用于加载共享库、调用 C 函数、访问 C 数据结构的 PHP 扩展。无需自己编写 PHP 扩展,就能从 PHP 直接使用已有的 C API。 PHP 官方文档将 FFI 说明为一项低级且危险的功能。它是仅适合那些既理解 C 又理解目标库 API 的开发者使用的功能。 FFI 也不是「让程序更快的功能」。PHP 官方文档同样指出,FFI 的数据结构访问比原生 PHP 数组或对象更慢。选择 FFI 的理由并非速度,而是希望从 PHP 使用已有的 C 库。FFI 可用的环境与限制
PHP 官方文档中,启用 FFI 扩展的方法如下。- 编译 PHP 时带上
--with-ffi - 在 Windows 上,通过
php.ini启用php_ffi.dll - 通过
php.ini的ffi.enable控制是否允许使用
ffi.enable 有以下 3 个值:
在 Web 服务器环境中,通常会设置
ffi.enable=false 或 ffi.enable=preload。包括 Laravel Cloud 在内的托管环境,也不能保证在常规 Web 应用中都能使用 FFI。基本用法
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一样访问结构体字段
FFI::string()。
加载头文件
使用FFI::load() 可以从 C 头文件读入声明。头文件中可以写入 FFI_SCOPE 与 FFI_LIB。
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 函数。
2. 将 FFI::cdef() 集中在一处
src/VoicevoxFFI.php 将头文件读取与库路径解析集中在一处。
FFI::cdef()。库路径的差异也可以通过环境变量与 OS 判断来吸收。
3. 用对象包装 out parameter
src/Synthesizer.php 的构造函数分配 struct VoicevoxSynthesizer*,并将其地址传给 voicevox_synthesizer_new()。
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 API,但又不至于要专门写 PHP 扩展。反过来,若想在通用 Web 托管环境中运行,或者可以完全用 PHP 完成的处理,就不适合使用 FFI。
小结
使用 PHP FFI,你可以从 Pure PHP 调用已有的 C 库。但 FFI 是一项低级且危险的机制,也未必在 Web 服务器上可用。 从 VOICEVOX Core for PHP 的实现可以看到,实践中要点有 4 条:- 为 FFI 单独准备头文件
- 将
FFI::cdef()与库路径解析集中在一处 - 用专用类包装不透明指针
- C 返回的内存先拷贝到 PHP,再用专用函数释放