FFI 外部函数接口
概述
FFI(Foreign Function Interface,外部函数接口)是 PHP 7.4 引入的强大功能,允许 PHP 代码直接调用 C 语言编写的动态链接库(.so/.dll)中的函数,而无需编写 PHP 扩展。FFI 极大地扩展了 PHP 的能力边界,让 PHP 能够直接与操作系统 API、C 库、系统调用等进行交互,同时也为性能敏感的场景提供了绕过 PHP 解释器的可能性。
PHP 版本要求
FFI 自 PHP 7.4 引入。PHP 8.1 增加了 FFI::scope() 和改进的性能。生产环境中使用 FFI 需要设置 ffi.enabled=true。本文基于 PHP 8.1+ 编写。
安全警告
FFI 允许 PHP 直接操作内存,如果使用不当可能导致段错误(Segmentation Fault)、内存泄漏甚至安全漏洞。在生产环境中应谨慎使用 FFI,并确保设置适当的权限限制。
基础概念
FFI 工作原理
PHP 层 C 层
┌──────────────┐ FFI Bridge ┌──────────────┐
│ FFI::cdef() │ ──────────────→ │ C 函数 │
│ 调用 C 函数 │ ←────────────── │ 返回结果 │
│ 操作 C 数据 │ │ 内存操作 │
└──────────────┘ └──────────────┘FFI 通过以下步骤工作:
- 定义 C 头文件声明(函数原型、结构体、常量)
- 加载共享库(.so 文件)
- 创建 FFI 对象
- 通过 PHP 调用 C 函数
FFI vs PHP 扩展
| 特性 | FFI | PHP 扩展 |
|---|---|---|
| 开发语言 | PHP + C 头文件 | C/C++ |
| 安装方式 | 无需编译 | 需要编译 |
| 调用开销 | 有(通过桥接层) | 无(原生绑定) |
| 开发速度 | 快 | 慢 |
| 性能 | 中等(有额外开销) | 最佳 |
| 安全性 | 需严格限制 | 编译时检查 |
安装与配置
启用 FFI
ini
; php.ini 配置
; 启用 FFI(默认禁用)
ffi.enabled = true
; 预加载定义(可选,生产环境推荐)
; ffi.preload = /etc/php/ffi-preload.phpphp
<?php
// ffi.preload.php 示例
// 这个文件在 PHP 启动时自动加载
FFI::cdef("
int printf(const char *format, ...);
void *malloc(size_t size);
void free(void *ptr);
", "libc.so.6");CLI 模式
FFI 在 CLI 模式下默认启用。在 Web 模式(PHP-FPM/Apache)中默认禁用,需要显式设置 ffi.enabled = true。
详细说明
FFI::cdef 基本使用
调用 C 标准库函数
php
<?php
declare(strict_types=1);
/**
* 通过 FFI 调用 C 标准库函数
*/
// 定义 C 函数原型并加载 libc
$libc = FFI::cdef("
// 字符串操作
int strlen(const char *s);
char *strcpy(char *dest, const char *src);
int strcmp(const char *s1, const char *s2);
// 内存操作
void *malloc(size_t size);
void free(void *ptr);
void *memset(void *s, int c, size_t n);
void *memcpy(void *dest, const void *src, size_t n);
// 数学函数
double sin(double x);
double cos(double x);
double sqrt(double x);
// I/O 函数
int printf(const char *format, ...);
int puts(const char *s);
// 随机数
int rand(void);
void srand(unsigned int seed);
// 时间
time_t time(time_t *tloc);
", "libc.so.6");
// 调用 C 函数
echo "strlen('hello'): " . $libc->strlen("hello") . PHP_EOL; // 5
// 数学计算
echo "sqrt(144): " . $libc->sqrt(144.0) . PHP_EOL; // 12
// 随机数
$libc->srand((int) time(null));
echo "rand(): " . $libc->rand() . PHP_EOL;
// 内存分配
$ptr = $libc->malloc(1024);
$libc->memset($ptr, 0, 1024);
$libc->strcpy($ptr, "Hello from C!");
echo FFI::string($ptr) . PHP_EOL;
$libc->free($ptr);操作 C 结构体
php
<?php
declare(strict_types=1);
/**
* 通过 FFI 操作 C 结构体
*/
// 定义结构体和函数
$ffi = FFI::cdef("
// 定义结构体
struct Point {
double x;
double y;
};
struct Rectangle {
struct Point top_left;
struct Point bottom_right;
};
// 定义函数
double distance(struct Point *p1, struct Point *p2);
struct Point *create_point(double x, double y);
", "libc.so.6");
// 创建结构体实例
$point = $ffi->new('struct Point');
$point->x = 3.0;
$point->y = 4.0;
// 访问结构体成员
echo "Point: ({$point->x}, {$point->y})" . PHP_EOL;
// 创建结构体数组
$points = $ffi->new('struct Point[5]');
for ($i = 0; $i < 5; $i++) {
$points[$i]->x = $i * 10.0;
$points[$i]->y = $i * 20.0;
}
// 嵌套结构体
$rect = $ffi->new('struct Rectangle');
$rect->top_left->x = 0.0;
$rect->top_left->y = 100.0;
$rect->bottom_right->x = 200.0;
$rect->bottom_right->y = 0.0;枚举和联合体
php
<?php
declare(strict_types=1);
/**
* FFI 枚举和联合体
*/
$ffi = FFI::cdef("
// 枚举
enum Color {
RED = 0,
GREEN = 1,
BLUE = 2,
};
// 联合体
union Value {
int i;
float f;
double d;
char *s;
};
", "libc.so.6");
// 使用枚举
$red = $ffi->RED; // 0
echo "RED = {$red}" . PHP_EOL;
// 使用联合体
$value = $ffi->new('union Value');
$value->i = 42;
echo "As int: {$value->i}" . PHP_EOL;
echo "As float: {$value->f}" . PHP_EOL;FFI::load 从文件加载
php
<?php
declare(strict_types=1);
/**
* 使用 FFI::load 从定义文件加载
* 推荐方式,更易于管理
*/
// 创建定义文件
// math.def
/*
header:
"""
#include <math.h>
#include <string.h>
"""
*/
// 使用 FFI::load 加载
$ffi = FFI::load(__DIR__ . '/math.def');
// 调用函数
echo "sin(3.14): " . $ffi->sin(3.14) . PHP_EOL;FFI::scope 命名空间
PHP 8.1 引入了 FFI::scope(),用于创建预加载的 FFI 命名空间:
php
<?php
declare(strict_types=1);
// ffi.preload.php(PHP 启动时加载)
/*
FFI::load(__DIR__ . '/definitions/math.ffi');
FFI::load(__DIR__ . '/definitions/crypto.ffi');
*/
// 普通脚本中使用 FFI::scope()
$math = FFI::scope('math');
$math->sin(3.14);调用第三方 C 库
调用 libcurl
php
<?php
declare(strict_types=1);
/**
* 通过 FFI 调用 libcurl
*/
class FfiCurl
{
private $ffi;
public function __construct()
{
$this->ffi = FFI::cdef("
typedef void CURL;
typedef struct {
void *data;
int (*function)(void *data, int type, char *data, int size, void *userdata);
} curl_write_callback;
CURL *curl_easy_init(void);
void curl_easy_cleanup(CURL *curl);
void curl_easy_setopt(CURL *curl, int option, ...);
int curl_easy_perform(CURL *curl);
const char *curl_easy_strerror(int code);
", "libcurl.so.4");
}
public function get(string $url): string
{
$curl = $this->ffi->curl_easy_init();
if ($curl === null) {
throw new RuntimeException('curl_easy_init 失败');
}
try {
// 设置 URL
$this->ffi->curl_easy_setopt($curl, 10000 + 2, $url); // CURLOPT_URL
// 设置超时
$this->ffi->curl_easy_setopt($curl, 13, 30); // CURLOPT_TIMEOUT
// 禁止验证 SSL
$this->ffi->curl_easy_setopt($curl, 64, 0); // CURLOPT_SSL_VERIFYPEER
// 执行请求
$result = $this->ffi->curl_easy_perform($curl);
return "curl result: {$result}";
} finally {
$this->ffi->curl_easy_cleanup($curl);
}
}
}调用 SQLite C API
php
<?php
declare(strict_types=1);
/**
* 通过 FFI 调用 SQLite C API
*/
class FfiSqlite
{
private $ffi;
public function __construct()
{
$this->ffi = FFI::cdef("
typedef struct sqlite3 sqlite3;
typedef struct sqlite3_stmt sqlite3_stmt;
int sqlite3_open(const char *filename, sqlite3 **ppDb);
int sqlite3_close(sqlite3 *db);
const char *sqlite3_errmsg(sqlite3 *db);
int sqlite3_exec(
sqlite3 *db,
const char *sql,
int (*callback)(void*, int, char**, char**),
void *arg,
char **errmsg
);
int sqlite3_prepare_v2(
sqlite3 *db,
const char *zSql,
int nByte,
sqlite3_stmt **ppStmt,
const char **pzTail
);
int sqlite3_step(sqlite3_stmt *stmt);
const unsigned char *sqlite3_column_text(sqlite3_stmt*, int iCol);
int sqlite3_column_int(sqlite3_stmt*, int iCol);
int sqlite3_finalize(sqlite3_stmt *stmt);
int sqlite3_changes(sqlite3 *db);
", "libsqlite3.so.0");
}
public function query(string $dbPath, string $sql): array
{
$dbPtr = $this->ffi->new('sqlite3*');
$rc = $this->ffi->sqlite3_open($dbPath, FFI::addr($dbPtr));
if ($rc !== 0) {
throw new RuntimeException("SQLite 错误: " . $rc);
}
$results = [];
$stmtPtr = $this->ffi->new('sqlite3_stmt*');
$this->ffi->sqlite3_prepare_v2(
$dbPtr, $sql, -1, FFI::addr($stmtPtr), null
);
while ($this->ffi->sqlite3_step($stmtPtr) === 100) { // SQLITE_ROW
$text = $this->ffi->sqlite3_column_text($stmtPtr, 0);
$results[] = FFI::string($text);
}
$this->ffi->sqlite3_finalize($stmtPtr);
$this->ffi->sqlite3_close($dbPtr);
return $results;
}
}实战示例
高性能字符串处理
php
<?php
declare(strict_types=1);
/**
* 使用 FFI 进行高性能字符串操作
* 对比 FFI 与 PHP 原生字符串操作的性能
*/
class FfiStringBenchmark
{
private $libc;
public function __construct()
{
$this->libc = FFI::cdef("
size_t strlen(const char *s);
char *strtok(char *str, const char *delim);
int strncmp(const char *s1, const char *s2, size_t n);
char *strstr(const char *haystack, const char *needle);
", "libc.so.6");
}
/**
* 使用 FFI strlen
*/
public function ffiStrlen(string $str, int $iterations): float
{
$start = hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
$this->libc->strlen($str);
}
return (hrtime(true) - $start) / 1_000_000;
}
/**
* 使用 PHP strlen
*/
public function phpStrlen(string $str, int $iterations): float
{
$start = hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
strlen($str);
}
return (hrtime(true) - $start) / 1_000_000;
}
public function compare(string $str, int $iterations = 100000): void
{
echo "=== strlen 性能对比 ===" . PHP_EOL;
echo "字符串长度: " . strlen($str) . PHP_EOL;
echo "迭代次数: {$iterations}" . PHP_EOL;
$ffiTime = $this->ffiStrlen($str, $iterations);
$phpTime = $this->phpStrlen($str, $iterations);
echo "FFI strlen: {$ffiTime}ms" . PHP_EOL;
echo "PHP strlen: {$phpTime}ms" . PHP_EOL;
echo "FFI 开销: " . round($ffiTime / $phpTime, 2) . "x" . PHP_EOL;
}
}
$bench = new FfiStringBenchmark();
$bench->compare("Hello, World! This is a test string for benchmarking.");系统信息获取
php
<?php
declare(strict_types=1);
/**
* 通过 FFI 获取 Linux 系统信息
*/
class SystemInfo
{
private $libc;
private $ffi;
public function __construct()
{
$this->libc = FFI::cdef("
#include <sys/utsname.h>
#include <unistd.h>
#include <sys/statvfs.h>
int uname(struct utsname *buf);
long sysconf(int name);
", "libc.so.6");
}
/**
* 获取系统信息
*/
public function getSystemInfo(): array
{
// utsname 结构体
$uts = $this->libc->new('struct utsname');
$this->libc->uname(FFI::addr($uts));
return [
'sysname' => FFI::string($uts->sysname),
'nodename' => FFI::string($uts->nodename),
'release' => FFI::string($uts->release),
'version' => FFI::string($uts->version),
'machine' => FFI::string($uts->machine),
];
}
/**
* 获取 CPU 核心数
*/
public function getCpuCount(): int
{
return $this->libc->sysconf(84); // _SC_NPROCESSORS_CONF
}
/**
* 获取内存页大小
*/
public function getPageSize(): int
{
return $this->libc->sysconf(39); // _SC_PAGESIZE
}
}
if (PHP_SAPI === 'cli' && PHP_OS_FAMILY === 'Linux') {
$info = new SystemInfo();
$sysInfo = $info->getSystemInfo();
echo "系统: {$sysInfo['sysname']} {$sysInfo['release']}" . PHP_EOL;
echo "CPU 核心数: " . $info->getCpuCount() . PHP_EOL;
echo "内存页大小: " . $info->getPageSize() . " bytes" . PHP_EOL;
}注意事项
FFI 的性能开销
FFI 调用 C 函数涉及以下额外开销:
- PHP 到 C 的参数转换
- C 到 PHP 的返回值转换
- 类型检查和安全验证
FFI 性能提示
单次 FFI 调用的开销约为 PHP 原生调用的 2-3 倍。只有在 C 函数内部执行大量计算时,FFI 的性能优势才会体现。对于简单的函数(如 strlen),使用 FFI 反而更慢。
内存安全
php
<?php
declare(strict_types=1);
// FFI 分配的内存需要手动释放
$ffi = FFI::cdef("
void *malloc(size_t size);
void free(void *ptr);
", "libc.so.6");
// 分配内存
$ptr = $ffi->malloc(1024);
// ... 使用内存 ...
// 必须手动释放
$ffi->free($ptr);
// 不释放 = 内存泄漏!线程安全
FFI 对象不是线程安全的,不能在多个线程间共享 FFI 对象。
最佳实践
1. 使用 preload 提升性能
php
<?php
// ffi.preload.php - 在 PHP 启动时预加载
FFI::cdef("
// 预定义所有 C 函数
double sin(double x);
double cos(double x);
", "libm.so.6");2. 封装 FFI 调用
php
<?php
declare(strict_types=1);
/**
* FFI 安全封装
* 提供类型安全的 PHP 接口
*/
class SafeFfiWrapper
{
private $ffi;
public function __construct()
{
$this->ffi = FFI::cdef(/* ... */);
}
public function safeSqrt(float $value): float
{
if ($value < 0) {
throw new InvalidArgumentException('不能对负数开平方根');
}
return $this->ffi->sqrt($value);
}
}3. 何时使用 FFI
适合使用 FFI 的场景:
- 调用没有 PHP 扩展的 C 库
- 快速原型验证(避免编写扩展)
- 性能敏感的内循环操作
不适合使用 FFI 的场景:
- 简单的字符串/数组操作(PHP 原生更快)
- 需要高频率调用简单函数
- 生产环境中的关键路径代码
下一节
继续学习:PECL 扩展管理