PHP 7.4的 2020 版本使开发人员能够做一些他们以前从未做过的事情——使用纯PHP 代码 访问数据结构和调用用另一种语言编写的函数,不需要扩展,也不需要绑定到外部库。这怎么可能? 使用 PHP FFI(外来函数接口)。 在本文中,我们将讨论 PHP FFI 是什么、它的优点和功能,并比较 PHP 如何在不需要创建插件的情况下与Go、Rust和 C++等语言一起工作。我们还将分享我们在实现此功能时使用的实验,并强调我们发现它最有用的地方以及我们认为不值得麻烦的地方。
什么是 PHP FFI?
通俗的说,FFI是一种接口,可以让开发者使用一种语言调用另一种语言编写的库函数。例如,FFI 使得从纯 PHP 调用用 Rust、C++ 或 Go 编写的函数成为可能。为了将解释型语言与编译型语言连接起来,使用了 libffi [ GitHub ] 库 - Wiki。
由于解释型语言不知道具体到哪里去寻找被调用函数的参数(即在哪些寄存器中),也不知道从哪里得到调用后的函数结果,所以依赖于Libffi来做这项工作。因此,您需要安装此库,因为它是系统库 (Linux) 的一部分。
PHP FFI 的好处
虽然现在完全是实验性的,但 PHP FFI 的早期测试揭示了许多好处,这些好处可能会消除一些繁琐的 PHP 扩展,并最终迎来一个有趣的开发新时代。
节省时间和精力,不必编写 PHP 特定的扩展/模块来与 C 程序/库交互
在图像和视频处理等繁重的计算工作上执行得更快
与启动昂贵的 VM 和容器相比,在通用云平台上启动 PHP 实例更省钱
PHP FFI 实验
注意:所有 PHP FFI 实验均在 ArchLinux(5.6.1 内核)、Libffi 3.2.1 上进行。
虽然我们的实验并没有以非常严格的方式遵循科学方法,但我们确实是有目的的。探索新的语言特性当然很有趣,但我们问自己的问题是,这对软件开发有实际意义吗?结果如下:
使用 PHP FFI 计算斐波那契数列
对于我们的第一个实验,我们认为像计算斐波那契数列这样的问题既简单又有趣。当然,我们并没有打算以最有效的方式做到这一点。相反,我们想借助递归来尽可能多地使用处理器。这也会阻止编译语言优化此功能(例如,应用循环展开技术)。
实验 1:使用 Rust
对于 PHP,我们做的第一件事是取消注释php .ini 中的扩展名 ffi (/etc/php/php.ini在 ArchLinux 中)。
接下来,我们需要声明我们的条件接口。有一些限制,当前存在于PHP FFI,特别是不能使用C-预处理(#include,#define,等),除了一些特殊的。在 PHP 类型中:
$ffi = FFI::cdef(
"int Fib(int n);",
"/PATH/TO/SO/lib.so");
FFI::cdef – 通过这个操作,我们定义了交互界面。
int Fib (int n)– 它是编译语言的导出方法的名称。稍后我们将讨论如何正确地做到这一点。
/PATH/TO/SO/lib.so – 上面函数所在的动态库的路径。
我们使用的完整 PHP 脚本:
PHP FFI
function fib($n)
{
if ($n === 1 || $n === 2) {
return 1;
}
return fib($n - 1) + fib($n - 2);
}
$start = microtime(true);
$p = 0;
for ($i = 0; $i < 1000000; $i++) {
$p = fib(12);
}
echo '[PHP] execution time: '.(microtime(true) - $start).' Result: '.$p.PHP_EOL;
铁锈FFI
$rust_ffi = FFI::cdef(
"int Fib(int n);",
"lib/libphp_rust_ffi.so");
$start = microtime(true);
$r = 0;
for ($i=0; $i < 1000000; $i++) { $r = $rust_ffi->Fib(12);
}
echo '[RUST] execution time: '.(microtime(true) - $start).' Result: '.$r.PHP_EOL;
CPP FFI
$cpp_ffi = FFI::cdef(
"int Fib(int n);",
"lib/libphp_cpp_ffi.so");
$start = microtime(true);
$c = 0;
for ($i=0; $i < 1000000; $i++) { $c = $cpp_ffi->Fib(12);
}
echo '[CPP] execution time: '.(microtime(true) - $start).' Result: '.$c.PHP_EOL;
GOLAN FFI
$golang_ffi = FFI::cdef(
"int Fib(int n);",
"lib/libphp_go_ffi.so");
$start = microtime(true);
for ($i=0; $i < 1000000; $i++) { $golang_ffi->Fib(12);
}
echo '[GOLANG] execution time: '.(microtime(true) - $start).' Result: '.$c.PHP_EOL;
第一步是用 Rust 语言制作一个动态库。
这需要一些准备:
1.在任何平台上,我们只需要从这里安装一个指令。
2. 之后,我们可以使用命令在任何地方创建一个项目。就是这样!cargo new rust_php_ffi
这是我们使用的函数:
锈:
//src/lib.rs
#[no_mangle]
extern "C" fn Fib(n: i32) -> i32 {
if (n == 0) || (n == 1) {
return 1;
}
Fib(n - 1) + Fib(n - 2)
}
注:这是至关重要的属性#[no_mangle]添加到所需要的功能,否则编译器会喜欢的东西取代您的函数的名称:_аgs @ fs34。并且在将其导出到 PHP 时,libffi 根本无法在动态库中找到名为 Fib 的函数。您可以在此处阅读有关此问题的更多信息。
在 Cargo.toml 中,我们添加了属性:
[lib]
crate-type = ["cdylib"]
我想提请您注意一个事实,即通过 Cargo.toml 中的一个属性,动态库有三个选项:
1. dylib– Rust 与一个不稳定的 ABI 共享这个库,它可以从一个版本到另一个版本(就像在 Go 内部 ABI 中一样)。
2.cdylib是一个在C/C++中使用的动态库。这是我们的首选。
3. rlib– 带有 rlib extestion ( .rlib) 的Rust 静态库。它还包含用于链接分别用 Rust 编写的各种 rlib 的元数据
然后,我们使用cargo build --release. 在文件夹中,target/release我们看到了该.so文件。这将是我们的动态库。
C++
接下来是 C++。这里的一切也很简单:
CPP:
// in php_cpp_ffi.cpp
int main() {
}
extern "C" int Fib(int n) {
if ((n==1) || (n==2)) {
return 1;
}
return Fib(n-1) + Fib(n-2);
}
我们需要声明该extern函数,以便它可以从 php 导入。
我们编译:
g++ -fPIC -O3 -shared src/php_cpp_ffi.cpp -o ../lib/ libphp_cpp_ffi.so
关于编译的几点意见:
1. -fPIC位置无关代码。对于动态库,重要的是独立于它在内存中加载的地址。
2. -O3– 最大优化
实验二:使用 Golang
我们实验的下Golang一个是——一种具有运行时的语言。为 Go 开发了一种与动态库交互的特殊机制,称为CGO。在此处了解有关此机制如何工作的更多信息。由于 CGO 会解释从 C 生成的错误,因此无法像在 C++ link和link 中那样使用优化。鼓声,请。. . 这是代码! 高朗:
package main
import (
"C"
)
// we needed to have empty main in package main :)
// because -buildmode=c-shared requires exactly one main package
func main() {
}
//export Fib
func Fib(n C.int) C.int {
if n == 1 || n == 2 {
return 1
}
return Fib(n-1) + Fib(n-2)
}
因此,所有这些都是相同的 Fib 函数,但是,为了将此函数导出到动态库中,我们需要添加上面的注释(一种 GO 属性)//export Fib。然后,我们编译:go build -o ../lib/libphp_go_ffi.so -buildmode=c-shared. 注意我们如何添加 -buildmode = c-shared以获得动态库。我们在输出时收到了 2 个文件。一个带有标题的文件.h,.so是一个动态库。我们并不真正需要带有头文件的文件,因为我们知道函数的名称,而且 FFI PHP 在使用 C 预处理器时相当有限。
火箭发射
在我们写完所有内容(提供了源代码)之后,我们制作了一个小的 Makefile 来收集所有内容(它也位于存储库中)。我们叫后make build的在lib文件夹中,4个文件出现。两个用于 GO (.h/.so),一个用于 Rust 和 C++。
生成文件:
build_cpp:
echo 'Building cpp...'
cd cpp && g++ -fPIC -O3 -shared src/php_cpp_ffi.cpp -o libphp_cpp_ffi.so
build_go:
echo 'Building golang...'
cd golang && go build -o libphp_go_ffi.so -buildmode=c-shared
build_rust:
echo 'Building Rust...'
cargo build --release && mv rust/target/release/libphp_ffi.so libphp_rust_ffi.so
build: build_cpp build_go build_rust
run:
php php/php_fib.php
然后,我们转到该php文件夹并运行我们的脚本(或通过 Makefile – make run)。注意:在 php 脚本中FFI::cdef,.so文件路径是硬编码的,所以为了一切正常,请运行make run. 工作结果如下:
1.[PHP]执行时间:8.6763260364532结果:144
2.[RUST]执行时间:0.32162690162659结果:144
3.[CPP]执行时间:0.3515248298645结果:144
4.[GOLANG]执行时间:5.0730509757996结果:144
不出所料,PHP在加载的CPU中显示出最低的结果数学运算,但总的来说,一百万次调用感觉相当快。
我们很惊讶 CGO 的运行时间比 PHP 少一点。这是由于calling-conventionsABI 不稳定造成的。CGO被迫进行Go-types到C(h构建GO动态库后得到的文件中可以看到)类型的类型转换操作,我们不得不复制C和GO的传入和返回值兼容性。
Rust 和 C++ 在我们的实验中显示出最好的结果,老实说,这正是我们的预期,因为它们具有稳定的 ABI(Rust 中的 extern)并且 PHP 和这些语言之间的唯一层是 libffi。
结论
由于遇到各种陷阱的可能性很大,PHP FFI 方法尚未准备好投入生产,但它很有希望。PHP 开发社区甚至在PHP.net上发布了他们自己的警告,在那里他们介绍了该功能。