FFI允許在純 PHP 中加載共享庫(.DLL 或 .so)、調(diào)用 C 函數(shù)、訪問 C 數(shù)據(jù)結(jié)構(gòu),而無需深入了解 Zend 擴(kuò)展 API,也無需學(xué)習(xí)第三方“中間”語言。該擴(kuò)展的公共 API 由 FFI 類實(shí)現(xiàn),其中包含幾個靜態(tài)方法和對象重載方法,用于執(zhí)行與 C 數(shù)據(jù)的實(shí)際交互。
簡單來說,這個擴(kuò)展使得 PHP 開發(fā)者可以使用 C 語言編寫的庫,并在 PHP 中輕松地調(diào)用其中的函數(shù)和訪問 C 語言中定義的數(shù)據(jù)結(jié)構(gòu),無需編寫 C 擴(kuò)展或了解底層的 C 函數(shù)調(diào)用方式。這個擴(kuò)展的核心是 FFI 類,它提供了一系列靜態(tài)方法和對象重載方法,用于在 PHP 中調(diào)用 C 函數(shù)和訪問 C 數(shù)據(jù)結(jié)構(gòu)。
一、基礎(chǔ)FFI用法
在深入了解 FFI API 細(xì)節(jié)之前,先看幾個示例,展示 FFI API 在常規(guī)任務(wù)中的簡單使用。
注意:其中一些示例需要 libc.so.6,因此在沒有該庫的系統(tǒng)上無法運(yùn)行。
從共享庫中調(diào)用函數(shù):
<?php // 創(chuàng)建 FFI 對象,加載 libc 和輸出函數(shù) 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 函數(shù)需要特定的調(diào)用規(guī)則,例如 __fastcall、__stdcall 或 __vectorcall。
調(diào)用函數(shù),通過參數(shù)返回結(jié)構(gòu)體:
<?php // 創(chuàng)建 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"); // 創(chuàng)建 C 數(shù)據(jù)結(jié)構(gòu) $tv = $ffi->new("struct timeval"); $tz = $ffi->new("struct timezone"); // 調(diào)用 C 的 gettimeofday() var_dump($ffi->gettimeofday(FFI::addr($tv), FFI::addr($tz))); // 訪問 C 數(shù)據(jù)結(jié)構(gòu)的字段 var_dump($tv->tv_sec); // 打印完整數(shù)據(jù)結(jié)構(gòu) var_dump($tz); ?>
以上示例的輸出類似于:
int(0) int(1555946835) object(FFI\CData:struct timezone)#3 (2) { ["tz_minuteswest"]=> int(0) ["tz_dsttime"]=> int(0) }
訪問已存在的 C 變量:
<?php // 創(chuàng)建 FFI 對象,加載 libc 和輸出 errno 變量 $ffi = FFI::cdef( "int errno;", // 這是普遍的 C 聲明 "libc.so.6"); // print C's errno var_dump($ffi->errno); ?>
以上示例會輸出:
int(0)
創(chuàng)建和修改 C 變量:
<?php // 創(chuàng)建新的 C int 變量 $x = FFI::new("int"); var_dump($x->cdata); // 簡單賦值 $x->cdata = 5; var_dump($x->cdata); // 復(fù)合賦值 $x->cdata += 2; var_dump($x->cdata); ?>
以上示例會輸出:
int(0) int(5) int(7)
使用 C 數(shù)組:
<?php // 創(chuàng)建 C 數(shù)據(jù)結(jié)構(gòu) $a = FFI::new("long[1024]"); // 使用它就像使用常規(guī)數(shù)組 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回調(diào)
可以將 PHP 閉包分配給函數(shù)指針類型的原生變量,或?qū)⑵渥鳛楹瘮?shù)參數(shù)傳遞。
<?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代碼和數(shù)據(jù)主接口
通過工廠方法 FFI::cdef()、FFI::load() 或 FFI::scope() 創(chuàng)建該類的對象。定義的 C 變量作為有效的 FFI 實(shí)例屬性,定義的 C 函數(shù)作為有效的 FFI 實(shí)例方法。聲明的 C 類型可以用于 FFI::new() 和 FFI::type() 創(chuàng)建新的 C 數(shù)據(jù)結(jié)構(gòu)。
FFI 定義解析和共享庫加載可能需要較長時間。在 Web 環(huán)境中,每個 HTTP 請求都進(jìn)行這些操作是沒有意義的。然而,在 PHP 啟動時預(yù)加載 FFI 定義和庫,并在需要時實(shí)例化 FFI 對象是可能的。header 文件可以使用特殊的 FFI_SCOPE 定義進(jìn)行擴(kuò)展(例如 #define FFI_SCOPE “foo”),然后在預(yù)加載期間由 FFI::load() 加載。這將創(chuàng)建持久綁定,將通過 FFI::scope() 在所有后續(xù)請求中可用。
類摘要:
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 數(shù)據(jù)一樣以多種方式使用:
1、標(biāo)量類型的 C 數(shù)據(jù)可以通過 $cdata 屬性讀取和分配,例如:$x = FFI::new(‘int’);$x->cdata = 42。
2、C 結(jié)構(gòu)和聯(lián)合字段可作為常規(guī) PHP 對象屬性訪問,例如:$cdata->field。
3、C 數(shù)組元素可以像普通 PHP 數(shù)組元素一樣訪問,例如:$cdata[$offset]。
4、可以使用 foreach 語句遍歷 C 數(shù)組。
5、C 數(shù)組可用作 count() 的參數(shù)。
6、C 指針可以作為數(shù)組被取消引用,例如 $cdata[0]。
7、C 指針可以使用常規(guī)比較運(yùn)算符(<, <=, ==, !=, >=, >)進(jìn)行比較。
8、C 指針可以使用常規(guī)的 +/-/ ++/– 操作進(jìn)行遞增和遞減,例如:$cdata += 5。
9、C 指針可以通過常規(guī)的 – 運(yùn)算從另一個指針減去。
10、指向函數(shù)的 C 指針可以作為常規(guī)的 PHP 閉包調(diào)用,例如:$cdata()。
11、可以使用克隆運(yùn)算符復(fù)制任何 C 數(shù)據(jù),例如:$cdata2 = clone $cdata。
12、任何 C 數(shù)據(jù)都可以使用 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、預(yù)定義常量
- 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 }