FFI允许在纯 PHP 中加载共享库(.DLL 或 .so)、调用 C 函数、访问 C 数据结构,而无需深入了解 Zend 扩展 API,也无需学习第三方“中间”语言。该扩展的公共 API 由 FFI 类实现,其中包含几个静态方法和对象重载方法,用于执行与 C 数据的实际交互。
简单来说,这个扩展使得 PHP 开发者可以使用 C 语言编写的库,并在 PHP 中轻松地调用其中的函数和访问 C 语言中定义的数据结构,无需编写 C 扩展或了解底层的 C 函数调用方式。这个扩展的核心是 FFI 类,它提供了一系列静态方法和对象重载方法,用于在 PHP 中调用 C 函数和访问 C 数据结构。
一、基础FFI用法
在深入了解 FFI API 细节之前,先看几个示例,展示 FFI API 在常规任务中的简单使用。
注意:其中一些示例需要 libc.so.6,因此在没有该库的系统上无法运行。
从共享库中调用函数:
<?php // 创建 FFI 对象,加载 libc 和输出函数 printf() $ffi = FFI::cdef( "int printf(const char *format, ...);", // 这是普遍的 C 声明 "libc.so.6"); // call C's printf() $ffi->printf("Hello %s!\n", "world"); ?>
以上示例会输出:
Hello world!
注意:一些 C 函数需要特定的调用规则,例如 __fastcall、__stdcall 或 __vectorcall。
调用函数,通过参数返回结构体:
<?php // 创建 gettimeofday() 绑定 $ffi = FFI::cdef(" typedef unsigned int time_t; typedef unsigned int suseconds_t; struct timeval { time_t tv_sec; suseconds_t tv_usec; }; struct timezone { int tz_minuteswest; int tz_dsttime; }; int gettimeofday(struct timeval *tv, struct timezone *tz); ", "libc.so.6"); // 创建 C 数据结构 $tv = $ffi->new("struct timeval"); $tz = $ffi->new("struct timezone"); // 调用 C 的 gettimeofday() var_dump($ffi->gettimeofday(FFI::addr($tv), FFI::addr($tz))); // 访问 C 数据结构的字段 var_dump($tv->tv_sec); // 打印完整数据结构 var_dump($tz); ?>
以上示例的输出类似于:
int(0) int(1555946835) object(FFI\CData:struct timezone)#3 (2) { ["tz_minuteswest"]=> int(0) ["tz_dsttime"]=> int(0) }
访问已存在的 C 变量:
<?php // 创建 FFI 对象,加载 libc 和输出 errno 变量 $ffi = FFI::cdef( "int errno;", // 这是普遍的 C 声明 "libc.so.6"); // print C's errno var_dump($ffi->errno); ?>
以上示例会输出:
int(0)
创建和修改 C 变量:
<?php // 创建新的 C int 变量 $x = FFI::new("int"); var_dump($x->cdata); // 简单赋值 $x->cdata = 5; var_dump($x->cdata); // 复合赋值 $x->cdata += 2; var_dump($x->cdata); ?>
以上示例会输出:
int(0) int(5) int(7)
使用 C 数组:
<?php // 创建 C 数据结构 $a = FFI::new("long[1024]"); // 使用它就像使用常规数组 for ($i = 0; $i < count($a); $i++) { $a[$i] = $i; } var_dump($a[25]); $sum = 0; foreach ($a as $n) { $sum += $n; } var_dump($sum); var_dump(count($a)); var_dump(FFI::sizeof($a)); ?>
以上示例会输出:
int(25) int(523776) int(1024) int(8192)
使用 C 枚举:
<?php $a = FFI::cdef('typedef enum _zend_ffi_symbol_kind { ZEND_FFI_SYM_TYPE, ZEND_FFI_SYM_CONST = 2, ZEND_FFI_SYM_VAR, ZEND_FFI_SYM_FUNC } zend_ffi_symbol_kind; '); var_dump($a->ZEND_FFI_SYM_TYPE); var_dump($a->ZEND_FFI_SYM_CONST); var_dump($a->ZEND_FFI_SYM_VAR); ?>
以上示例会输出:
int(0) int(2) int(3)
二、PHP回调
可以将 PHP 闭包分配给函数指针类型的原生变量,或将其作为函数参数传递。
<?php $zend = FFI::cdef(" typedef int (*zend_write_func_t)(const char *str, size_t str_length); extern zend_write_func_t zend_write; "); echo "Hello World 1!\n"; $orig_zend_write = clone $zend->zend_write; $zend->zend_write = function($str, $len) { global $orig_zend_write; $orig_zend_write("{\n\t", 3); $ret = $orig_zend_write($str, $len); $orig_zend_write("}\n", 2); return $ret; }; echo "Hello World 2!\n"; $zend->zend_write = $orig_zend_write; echo "Hello World 3!\n"; ?>
以上示例会输出:
Hello World 1! { Hello World 2! } Hello World 3!
三、完整PHP/FFI/preloading
以下是完整的PHP/FFI/preloading示例:
1、php.ini
ffi.enable=preload opcache.preload=preload.php
2、preload.php
<?php FFI::load(__DIR__ . "/dummy.h"); opcache_compile_file(__DIR__ . "/dummy.php"); ?>
3、dummy.h
#define FFI_SCOPE "DUMMY" #define FFI_LIB "libc.so.6" int printf(const char *format, ...);
4、dummy.php
<?php final class Dummy { private static $ffi = null; function __construct() { if (is_null(self::$ffi)) { self::$ffi = FFI::scope("DUMMY"); } } function printf($format, ...$args) { return (int)self::$ffi->printf($format, ...$args); } } ?>
5、test.php
<?php $d = new Dummy(); $d->printf("Hello %s!\n", "world"); ?>
四、C代码和数据主接口
通过工厂方法 FFI::cdef()、FFI::load() 或 FFI::scope() 创建该类的对象。定义的 C 变量作为有效的 FFI 实例属性,定义的 C 函数作为有效的 FFI 实例方法。声明的 C 类型可以用于 FFI::new() 和 FFI::type() 创建新的 C 数据结构。
FFI 定义解析和共享库加载可能需要较长时间。在 Web 环境中,每个 HTTP 请求都进行这些操作是没有意义的。然而,在 PHP 启动时预加载 FFI 定义和库,并在需要时实例化 FFI 对象是可能的。header 文件可以使用特殊的 FFI_SCOPE 定义进行扩展(例如 #define FFI_SCOPE “foo”),然后在预加载期间由 FFI::load() 加载。这将创建持久绑定,将通过 FFI::scope() 在所有后续请求中可用。
类摘要:
final class FFI { /* 常量 */ public const int __BIGGEST_ALIGNMENT__; /* 方法 */ public static addr(FFI\CData &$ptr): FFI\CData public static alignof(FFI\CData|FFI\CType &$ptr): int public static arrayType(FFI\CType $type, array $dimensions): FFI\CType public static cast(FFI\CType|string $type, FFI\CData|int|float|bool|null &$ptr): ?FFI\CData public cast(FFI\CType|string $type, FFI\CData|int|float|bool|null &$ptr): ?FFI\CData public static cdef(string $code = "", ?string $lib = null): FFI public static free(FFI\CData &$ptr): void public static isNull(FFI\CData &$ptr): bool public static load(string $filename): ?FFI public static memcmp(string|FFI\CData &$ptr1, string|FFI\CData &$ptr2, int $size): int public static memcpy(FFI\CData &$to, FFI\CData|string &$from, int $size): void public static memset(FFI\CData &$ptr, int $value, int $size): void public static new(FFI\CType|string $type, bool $owned = true, bool $persistent = false): ?FFI\CData public new(FFI\CType|string $type, bool $owned = true, bool $persistent = false): ?FFI\CData public static scope(string $name): FFI public static sizeof(FFI\CData|FFI\CType &$ptr): int public static string(FFI\CData &$ptr, ?int $size = null): string public static type(string $type): ?FFI\CType public type(string $type): ?FFI\CType public static typeof(FFI\CData &$ptr): FFI\CType }
五、C Data Handles
FFI\CData 对象可以像普通 PHP 数据一样以多种方式使用:
1、标量类型的 C 数据可以通过 $cdata 属性读取和分配,例如:$x = FFI::new(‘int’);$x->cdata = 42。
2、C 结构和联合字段可作为常规 PHP 对象属性访问,例如:$cdata->field。
3、C 数组元素可以像普通 PHP 数组元素一样访问,例如:$cdata[$offset]。
4、可以使用 foreach 语句遍历 C 数组。
5、C 数组可用作 count() 的参数。
6、C 指针可以作为数组被取消引用,例如 $cdata[0]。
7、C 指针可以使用常规比较运算符(<, <=, ==, !=, >=, >)进行比较。
8、C 指针可以使用常规的 +/-/ ++/– 操作进行递增和递减,例如:$cdata += 5。
9、C 指针可以通过常规的 – 运算从另一个指针减去。
10、指向函数的 C 指针可以作为常规的 PHP 闭包调用,例如:$cdata()。
11、可以使用克隆运算符复制任何 C 数据,例如:$cdata2 = clone $cdata。
12、任何 C 数据都可以使用 var_dump()、print_r() 等可视化。
六、C Type Handles
1、类摘要
final class FFI\CType { /* 常量 */ public const int TYPE_VOID; public const int TYPE_FLOAT; public const int TYPE_DOUBLE; public const int TYPE_LONGDOUBLE; public const int TYPE_UINT8; public const int TYPE_SINT8; public const int TYPE_UINT16; public const int TYPE_SINT16; public const int TYPE_UINT32; public const int TYPE_SINT32; public const int TYPE_UINT64; public const int TYPE_SINT64; public const int TYPE_ENUM; public const int TYPE_BOOL; public const int TYPE_CHAR; public const int TYPE_POINTER; public const int TYPE_FUNC; public const int TYPE_ARRAY; public const int TYPE_STRUCT; public const int ATTR_CONST; public const int ATTR_INCOMPLETE_TAG; public const int ATTR_VARIADIC; public const int ATTR_INCOMPLETE_ARRAY; public const int ATTR_VLA; public const int ATTR_UNION; public const int ATTR_PACKED; public const int ATTR_MS_STRUCT; public const int ATTR_GCC_STRUCT; public const int ABI_DEFAULT; public const int ABI_CDECL; public const int ABI_FASTCALL; public const int ABI_THISCALL; public const int ABI_STDCALL; public const int ABI_PASCAL; public const int ABI_REGISTER; public const int ABI_MS; public const int ABI_SYSV; public const int ABI_VECTORCALL; /* 方法 */ public getAlignment(): int public getArrayElementType(): FFI\CType public getArrayLength(): int public getAttributes(): int public getEnumKind(): int public getFuncABI(): int public getFuncParameterCount(): int public getFuncParameterType(int $index): FFI\CType public getFuncReturnType(): FFI\CType public getKind(): int public getName(): string public getPointerType(): FFI\CType public getSize(): int public getStructFieldNames(): array public getStructFieldOffset(string $name): int public getStructFieldType(string $name): FFI\CType }
2、预定义常量
- FFI\CType::TYPE_VOID
- FFI\CType::TYPE_FLOAT
- FFI\CType::TYPE_DOUBLE
- FFI\CType::TYPE_LONGDOUBLE
- FFI\CType::TYPE_UINT8
- FFI\CType::TYPE_SINT8
- FFI\CType::TYPE_UINT16
- FFI\CType::TYPE_SINT16
- FFI\CType::TYPE_UINT32
- FFI\CType::TYPE_SINT32
- FFI\CType::TYPE_UINT64
- FFI\CType::TYPE_SINT64
- FFI\CType::TYPE_ENUM
- FFI\CType::TYPE_BOOL
- FFI\CType::TYPE_CHAR
- FFI\CType::TYPE_POINTER
- FFI\CType::TYPE_FUNC
- FFI\CType::TYPE_ARRAY
- FFI\CType::TYPE_STRUCT
- FFI\CType::ATTR_CONST
- FFI\CType::ATTR_INCOMPLETE_TAG
- FFI\CType::ATTR_VARIADIC
- FFI\CType::ATTR_INCOMPLETE_ARRAY
- FFI\CType::ATTR_VLA
- FFI\CType::ATTR_UNION
- FFI\CType::ATTR_PACKED
- FFI\CType::ATTR_MS_STRUCT
- FFI\CType::ATTR_GCC_STRUCT
- FFI\CType::ABI_DEFAULT
- FFI\CType::ABI_CDECL
- FFI\CType::ABI_FASTCALL
- FFI\CType::ABI_THISCALL
- FFI\CType::ABI_STDCALL
- FFI\CType::ABI_PASCAL
- FFI\CType::ABI_REGISTER
- FFI\CType::ABI_MS
- FFI\CType::ABI_SYSV
- FFI\CType::ABI_VECTORCALL
七、FFI Exceptions
类摘要:
class FFI\Exception extends Error { /* 继承的属性 */ protected string $message = ""; private string $string = ""; protected int $code; protected string $file = ""; protected int $line; private array $trace = []; private ?Throwable $previous = null; /* 继承的方法 */ public Error::__construct(string $message = "", int $code = 0, ?Throwable $previous = null) final public Error::getMessage(): string final public Error::getPrevious(): ?Throwable final public Error::getCode(): int final public Error::getFile(): string final public Error::getLine(): int final public Error::getTrace(): array final public Error::getTraceAsString(): string public Error::__toString(): string private Error::__clone(): void }
八、FFI Parser Exceptions
类摘要:
final class FFI\ParserException extends FFI\Exception { /* 继承的属性 */ protected string $message = ""; private string $string = ""; protected int $code; protected string $file = ""; protected int $line; private array $trace = []; private ?Throwable $previous = null; /* 继承的方法 */ public Error::__construct(string $message = "", int $code = 0, ?Throwable $previous = null) final public Error::getMessage(): string final public Error::getPrevious(): ?Throwable final public Error::getCode(): int final public Error::getFile(): string final public Error::getLine(): int final public Error::getTrace(): array final public Error::getTraceAsString(): string public Error::__toString(): string private Error::__clone(): void }