PHP外部函数接口:FFI,是一个PHP扩展,允许您轻松地将一些外部库包含到PHP代码中。这意味着可以直接在PHP中使用C,Go,Rust等共享库,而无需在C中编写PHP扩展。这个概念在其他语言(如Python或Go)中已经存在多年了。
UUID生成
让我们从一个小例子开始:UUID生成。
PHP 7.4 FFI(外部函数接口)
使用PHP,有几种生成UUID的方法。最好的方法是使用PECL UUID扩展名。您可以在GitHub上阅读其代码。这个PHP扩展负责将PHP函数绑定到libuuid。要使其正常工作,您必须在系统上安装libuuid(不必担心,几乎总是这样)和PECL。
这就是我们从PHP用户代码调用uuid_create()时发生的情况:
your PHP code |
+---+-------------^---+
v ^
+---v-------------+---+
| PHP engine |
+---+-------------^---+
v ^
+---v-------------+---+
| UUID ext |
+---+-------------^---+
v ^
+---v-------------+---+
| UUID lib |
+---------------------+
FFI承诺用纯PHP代码替换“ UUID扩展”层。
在讨论PHP扩展或FFI层之前,我们需要解释什么是库。库通常是用C编写的。但是也可以用许多其他可以编译为共享库的语言来编写:C ++,Rust,Go等。在unix或linux上,该库将被编译成一个.so文件。在Windows上它将是一个.dll文件。也可以将库静态包含到二进制文件中,但是本章不在本文的讨论范围之内。
在库代码源中,有.h文件。它们包含库能够执行的操作。这是uuid.h文件的摘录:
…
Some constants:
define UUID_VARIANT_NCS 0
define UUID_VARIANT_DCE 1
define UUID_VARIANT_MICROSOFT 2
define UUID_VARIANT_OTHER 3
Some function declarations:
void uuid_generate(uuid_t out);
int uuid_compare(const uuid_t uu1, const uuid_t uu2);
…
一个.h文件类似于PHP接口的东西:它含有常量和函数签名。
FFI/UUID 层
为了工作,FFI 需要我们想要使用的基础库 (libuuid) 的函数签名。因此,我们将.h文件复制到我们的项目中。有时,您可以清理并调整此文件以满足您的需要。例如,您可以删除永远不会使用的函数。这就是我们的文件的样子:
define FFI_LIB "libuuid.so.1"
typedef unsigned char uuid_t[16];
extern void uuid_generate_time(uuid_t out); // v1
extern void uuid_generate_md5(uuid_t out, const uuid_t ns, const char *name, size_t len); // v3
extern void uuid_generate_random(uuid_t out); // v4
extern void uuid_generate_sha1(uuid_t out, const uuid_t ns, const char *name, size_t len); // v5
这是最重要的,也是编写代码中更复杂的部分。完成后,我们可以将此文件包含在我们的PHP代码中:
$ffi = FFI::load(__DIR__ . '/include/uuid-php.h');
我们现在可以直接从PHP代码中使用libuuid。容易,不是吗?
但等一下,libuuid不能完全那样工作。所有函数都期望一些类型化的参数,您可能已经看到过。这些函数不返回 UUID,但将通过引用第一个参数进行修改。因此,在调用 函数之前,我们需要此值:
$output = $ffi->new('uuid_t');
$output是的实例FFI\CData。根据的内部类型,CData由于文档中描述了不同的运算符,我们可以访问不同的值。
最后,我们可以调用我们的函数。uuid_generate_random()匹配.h文件中库公开的名称:
$ffi->uuid_generate_random($output);
$output的内容将用组成 UUID 的十进制值数组进行更新。现在,我们需要将此数组转换为十六进制值的字符串:
foreach ($output as $values[]);
$uuid = sprintf('%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x', ...$values);
你喜欢它吗?如果您不想麻烦复制它,我们为您制作了它,并且它是开源的:https: //github.com/jolicode/ffi-uuid🍾
一些想法
简单
外部库的绑定确实很容易。最复杂的部分是创建最小 .h文件,并将PHP类型映射到库,反之亦然。
性能
看看我们实现的性能也很有趣。在我们的存储库中,您可以找到一个基准脚本。这是我们的实现与PECL之间进行比较的结果:
FFI:
- [v1] 1.254s
- [v4] 5.301s
PECL:
- [v1] 0.626s
- [v4] 4.583s
可以看到,PECL比我们的UUID V1实现快两倍,但对于UUID V4仅快15%。我们可以很容易地解释一下:UUID V4仅由伪随机数据组成,而UUID V1包含许多静态块。获取随机数据有点慢,这就是为什么V4生成要慢得多的原因。在V4上,这两种实现的区别不明显,因为几乎所有时间都花在了内部libuuid。
我们可以得出什么结论?
FFI确实还很年轻(在撰写本文时甚至没有发布)。因此,我们可以期待一些性能上的改进。但是我们已经可以说:
如果本机扩展已经存在并且可以安装,请使用它:
如果扩展不存在,那么FFI是一个很好的候选项;
如果应用程序中存在瓶颈,在 C、Rust 等中移植这些代码位并将其绑定到 FFI 可能很有趣。当 CPU 受限制时,FFI 会变得非常有趣:DOM 管理、大阵列、复杂计算等。
原来的扩展是否将替换为FFI?
现在说还真为时过早。但是,某些扩展(例如PDO)不仅仅具有简单绑定到库的功能。我非常有信心这些扩展将不会被FFI取代。
但是,某些扩展可能会被替换。php-redis,amqp,uuid等就是这种情况。例如Remi Collet已经开始使用FFI来代替redis扩展。
FFI敞开了大门:可以替换一些纯PHP库,而改用低级库。gitlib可以使用带有FFI的libgit2就是这种情况。
在某些情况下,没有C扩展,也没有纯PHP实现。如果您曾经尝试在PHP中测试TensorFlow,您会知道它很复杂。Dmitry Stogov是最重要的PHP Core贡献者之一,也是PHP / FFI的作者,他创建了一个POC来将TensorFlow绑定到PHP。
选择哪种语言将lib绑定到PHP?
能够编译到共享库(.so)的所有语言在系统上都不能很好地绑定到PHP。最好使用没有运行时的语言(C / C ++ / Rust /…),因为运行时可能会有副作用。例如,在GO中,运行时具有垃圾回收器并管理goroutine的线程。这可能会降低执行速度,甚至破坏您的应用程序。
如何将Rust lib绑定到PHP?
我想尝试一下用另一种语言比PHP执行复杂的计算是否更快。从网页提取HTML片段是很常见的:为了测试您的网站,或在爬网时。
Joel创建了一个小型库,用于提取与CSS表达式匹配的文档的第一个HTML元素。该代码确实很短,在某种程度上,Rust类型向C Type的转换代表了代码的2/3以上。
PHP绑定看起来与UUID非常相似。但是这里我们将.so直接包含在源代码中:
$ffi = FFI::cdef(<<<EOH
const char cssfilter(const char html, const char *filter);
EOH, __DIR__.'/../target/release/libcssfilter.so');
而且用法更简单:
$value = $ffi->cssfilter($html, $selector);
性能确实令人印象深刻,令人鼓舞:
FFI:
Duration: 1.731s
symfony/crawler:
Duration: 2.321s
我们可以轻松得出结论,如果计算复杂,将一部分PHP代码移植到另一种语言以提高应用程序性能可能非常有趣。
结论
FFI是个好东西。即使绑定尚不存在,FFI也会允许我们尝试一些库。它将使我们可以用Rust(例如)植入替换代码的某些慢速部分。而且我敢肯定,它将解锁一些我们还没有的想法。
评论 (0)