在应用程序中使用 FFI
使用 FFI 扩展,任何共享的 C 库(一般带有 *.so
扩展名)都可以包含在 PHP 应用程序中。如果打算使用任何核心 PHP 库或安装 PHP 扩展时产生的库,必须注意的是,你有能力修改 PHP 语言本身的行为。
在研究其工作原理之前,让我们先看看如何使用 FFI 扩展将外部 C 库并入 PHP 脚本。
将外部 C 库集成到 PHP 脚本中
为了便于说明,我们使用一个可能源自计算机科学 101(CS101)课程的简单函数:著名的冒泡排序。这种算法在计算机科学初学者的课程中被广泛使用,因为它简单易学。
冒泡排序是一种效率极低的排序算法,早已被壳排序、快速排序或合并排序等更快的排序算法所取代。虽然没有关于冒泡排序算法的权威参考文献,但你可以在这里读到关于它的一般性讨论: https://en.wikipedia.org/wiki/Bubble_sort 。 |
在本小节中,我们将不再详述算法的细节。相反,本小节的目的是演示如何使用现有的 C 库,并将其中的一个函数合并到 PHP 脚本中。现在我们将向您展示原始的 C 源代码,如何将其转换为共享库,以及最后如何使用 FFI 将该库并入 PHP。下面是我们要做的:
-
当然,第一步是将 C 代码编译成目标代码。下面是本例中使用的冒泡排序 C 代码:
#include <stdio.h> void bubble_sort(int [], int); void bubble_sort(int list[], int n) { int c, d, t, p; for (c = 0 ; c < n - 1; c++) { p = 0; for (d = 0 ; d < n - c - 1; d++) { if (list[d] > list[d+1]) { t = list[d]; list[d] = list[d+1]; list[d+1] = t; p++; } } if (p == 0) break; } }
-
然后,我们使用 GNU C 编译器(包含在本课程使用的 Docker 镜像中)将 C 代码编译成目标代码,如下所示:
gcc -c -Wall -Werror -fpic bubble.c
-
接下来,我们将目标代码整合到共享库中。这一步是必要的,因为 FFI 扩展只能访问共享库。为此,我们运行以下代码:
gcc -shared -o libbubble.so bubble.o
-
现在我们可以定义使用新共享库的 PHP 脚本了。首先,我们要定义一个显示
FFI\CData
数组输出的函数,如下所示:// /repo/ch04/php8_ffi_using_func_from_lib.php function show($label, $arr, $max) { $output = $label . "\n"; for ($x = 0; $x < $max; $x++) $output .= $arr[$x] . ','; return substr($output, 0, -1) . "\n"; }
-
接下来是关键部分:定义 FFI 实例。我们使用
FFI::cdef()
来完成这项工作,并提供两个参数。第一个参数是函数签名,第二个参数是新创建共享库的路径。这两个参数可以在下面的代码片段中看到:$bubble = FFI::cdef( "void bubble_sort(int [], int);", "./libbubble.so");
-
然后,我们使用
rand()
函数将FFI\CData
元素创建为一个整数数组,其中包含 16 个随机整数值。代码显示在下面的代码段中:$max = 16; $arr_b = FFI::new('int[' . $max . ']'); for ($i = 0; $i < $max; $i++) $arr_b[$i]->cdata = rand(0,9999);
-
最后,我们显示排序前数组的内容,执行排序,并显示排序后的内容。请注意,在下面的代码片段中,我们是通过调用 FFI 实例中的
bubble_sort()
来执行排序的:echo show('Before Sort', $arr_b, $max); $bubble->bubble_sort($arr_b, $max); echo show('After Sort', $arr_b, $max);
-
如你所料,输出结果显示了排序前的随机整数数组。排序后,数值按顺序排列。下面是步骤 7 中代码的输出结果:
Before Sort 245,8405,8580,7586,9416,3524,8577,4713, 9591,1248,798,6656,9064,9846,2803,304 After Sort 245,304,798,1248,2803,3524,4713,6656,7586, 8405,8577,8580,9064,9416,9591,9846
现在,您已经了解了如何使用 FFI 扩展将外部 C 库集成到 PHP 应用程序中,接下来我们将讨论最后一个主题:PHP 回调。
使用 PHP 回调
正如我们在本节开头提到的,可以使用 FFI 扩展来整合作为 PHP 语言(或其扩展)一部分的共享 C 库。这种集成非常重要,因为它允许你通过访问 PHP 共享 C 库中定义的 C 数据结构,在 C 库中读写本地 PHP 数据。
不过,本小节的目的不是向你展示如何创建 PHP 扩展。相反,在本小节中,我们将向你介绍 FFI 扩展覆盖本地 PHP 语言功能的能力。这种能力被称为 PHP 回调。在了解实现细节之前,我们必须首先检查与这种能力相关的潜在危险。
了解 PHP 回调的内在危险
重要的是要明白,在各种 PHP 共享库中定义的 C 函数经常被多个 PHP 函数使用。因此,如果在 C 层覆盖某个低级函数,可能会在 PHP 应用程序中出现意外行为。
另一个已知的问题是,覆盖本地 PHP C 函数很可能会产生 内存泄漏。随着时间的推移,使用此类重载的长期运行应用程序可能会失败,并有可能导致服务器崩溃!
最后一个需要考虑的问题是,并非所有的 FFI 平台都支持 PHP 回调功能。因此,虽然代码可以在 Linux 服务器上运行,但在 Windows 服务器上可能无法运行(或运行方式不同)。
与使用 FFI PHP 回调来覆盖本地 PHP C 库功能相比,定义自己的 PHP 函数可能更简单、更快捷、更安全! |
现在您已经了解了使用 PHP 回调所涉及的危险,让我们来看看示例实现。
实现 PHP 回调
在下面的示例中,zend_write
内部 PHP 共享库 C 函数被重写,使用回调函数在输出的末尾添加换行符(LF)。请注意,该重载会影响任何依赖于它的本地 PHP 函数,包括 echo
、print
、printf
:换句话说,任何产生直接输出的 PHP 函数。要实现 PHP 回调,请按以下步骤操作:
-
首先,我们使用
FFI::cdef()
定义一个 FFI 实例。第一个参数是zend_write
的函数签名。代码如下所示:// /repo/ch04/php8_php_callbacks.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
不会在末尾添加额外的 LF。您可以在此处查看代码:echo "Original echo command does not output LF:\n"; echo 'A','B','C'; echo 'Next line';
-
不出所料,输出结果是 ABCNext line。输出结果中没有回车或 LF,如图所示:
Original echo command does not output LF: ABCNext line
-
然后,我们将指向
zend_write
的指针克隆到$orig_zend_write
变量中。如果不这样做,我们就无法使用原始函数!代码如下所示:$orig_zend_write = clone $zend->zend_write;
-
接下来,我们以匿名函数的形式创建了一个 PHP 回调函数,它覆盖了原始的
zend_write
函数。在该函数中,我们调用原始的zend_write
函数,并在其输出中附加一个 LF,如下所示:$zend->zend_write = function($str, $len) { global $orig_zend_write; $ret = $orig_zend_write($str, $len); $orig_zend_write("\n", 1); return $ret; };
-
如图所示,剩余的代码会重新运行上一步中显示的
echo
命令:echo 'Revised echo command adds LF:'; echo 'A','B','C';
-
下面的输出显示,PHP
echo
命令现在会在每条命令的末尾产生一个 LF:Revised echo command adds LF: A B C
还需要注意的是,修改 PHP 库 C 语言的
zend_write
函数会影响所有使用该 C 语言函数的 PHP 本地函数。这包括print()
、printf()
(及其变体)等。
关于在 PHP 应用程序中使用 FFI 扩展的讨论到此结束。现在你知道了如何从外部共享库中加入本地 C 函数。您还知道了如何用 PHP 回调代替本地 PHP 核心或扩展共享库,从而有可能改变 PHP 语言本身的行为。
总结
在本章中,您了解了 FFI、它的历史以及如何使用它来促进快速 PHP 扩展原型设计。 您还了解到,虽然 FFI 扩展不应该用于提高速度,但它也可以实现允许 PHP 应用程序直接从外部 C 库调用本机 C 函数的目的。 通过从外部 C 库调用冒泡排序函数的示例演示了此功能的强大功能。 同样的功能可以扩展到涵盖数千个可用的 C 库中的任何一个,包括机器学习、光学字符识别、通信、加密; 无穷无尽。
在本章中,您对 PHP 本身如何在 C 语言级别上运行有了更深入的了解。 您学习了如何创建和直接使用 C 语言数据结构,使您能够交互甚至覆盖 PHP 语言本身。 此外,您现在已经了解如何将任何 C 语言库的功能直接合并到 PHP 应用程序中。 这些知识的另一个好处是,如果您在一家计划开发或已经开发自己的自定义 PHP 扩展的公司找到工作,它可以增强您的职业前景。
下一章标志着本书新部分 “PHP 8 技巧” 的开始。 在下一节中,您将了解升级到 PHP 8 时的向后兼容性问题。下一章将专门解决与 OOP 相关的向后兼容性问题。