在应用程序中使用 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。下面是我们要做的:

  1. 当然,第一步是将 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;
        }
    }
  2. 然后,我们使用 GNU C 编译器(包含在本课程使用的 Docker 镜像中)将 C 代码编译成目标代码,如下所示:

    gcc -c -Wall -Werror -fpic bubble.c
  3. 接下来,我们将目标代码整合到共享库中。这一步是必要的,因为 FFI 扩展只能访问共享库。为此,我们运行以下代码:

    gcc -shared -o libbubble.so bubble.o
  4. 现在我们可以定义使用新共享库的 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";
    }
  5. 接下来是关键部分:定义 FFI 实例。我们使用 FFI::cdef() 来完成这项工作,并提供两个参数。第一个参数是函数签名,第二个参数是新创建共享库的路径。这两个参数可以在下面的代码片段中看到:

    $bubble = FFI::cdef(
        "void bubble_sort(int [], int);",
        "./libbubble.so");
  6. 然后,我们使用 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);
  7. 最后,我们显示排序前数组的内容,执行排序,并显示排序后的内容。请注意,在下面的代码片段中,我们是通过调用 FFI 实例中的 bubble_sort() 来执行排序的:

    echo show('Before Sort', $arr_b, $max);
    $bubble->bubble_sort($arr_b, $max);
    echo show('After Sort', $arr_b, $max);
  8. 如你所料,输出结果显示了排序前的随机整数数组。排序后,数值按顺序排列。下面是步骤 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 函数,包括 echoprintprintf:换句话说,任何产生直接输出的 PHP 函数。要实现 PHP 回调,请按以下步骤操作:

  1. 首先,我们使用 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;
    ");
  2. 然后,我们添加代码以确认,在未修改的情况下,echo 不会在末尾添加额外的 LF。您可以在此处查看代码:

    echo "Original echo command does not output LF:\n";
    echo 'A','B','C';
    echo 'Next line';
  3. 不出所料,输出结果是 ABCNext line。输出结果中没有回车或 LF,如图所示:

    Original echo command does not output LF:
    ABCNext line
  4. 然后,我们将指向 zend_write 的指针克隆到 $orig_zend_write 变量中。如果不这样做,我们就无法使用原始函数!代码如下所示:

    $orig_zend_write = clone $zend->zend_write;
  5. 接下来,我们以匿名函数的形式创建了一个 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;
    };
  6. 如图所示,剩余的代码会重新运行上一步中显示的 echo 命令:

    echo 'Revised echo command adds LF:';
    echo 'A','B','C';
  7. 下面的输出显示,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 相关的向后兼容性问题。