Skip to content

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 通过以下步骤工作:

  1. 定义 C 头文件声明(函数原型、结构体、常量)
  2. 加载共享库(.so 文件)
  3. 创建 FFI 对象
  4. 通过 PHP 调用 C 函数

FFI vs PHP 扩展

特性FFIPHP 扩展
开发语言PHP + C 头文件C/C++
安装方式无需编译需要编译
调用开销有(通过桥接层)无(原生绑定)
开发速度
性能中等(有额外开销)最佳
安全性需严格限制编译时检查

安装与配置

启用 FFI

ini
; php.ini 配置
; 启用 FFI(默认禁用)
ffi.enabled = true

; 预加载定义(可选,生产环境推荐)
; ffi.preload = /etc/php/ffi-preload.php
php
<?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 函数涉及以下额外开销:

  1. PHP 到 C 的参数转换
  2. C 到 PHP 的返回值转换
  3. 类型检查和安全验证

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 扩展管理

参考链接