检查 FFI 类

正如本章所述,并非每个开发人员都需要使用 FFI 扩展。直接使用 FFI 扩展会加深你对 PHP 语言内部的理解,而这种理解的加深会对你作为 PHP 开发人员的职业生涯产生有益的影响:很有可能在将来的某个时候,你会受雇于一家开发了自定义 PHP 扩展的公司。了解如何在这种情况下操作 FFI 扩展,可以让你为定制的 PHP 扩展开发新功能,还能帮助你排除扩展问题。

FFI 类由 20 个方法组成,分为以下四大类:

  • Creational:此类方法可创建 FFI 扩展应用编程接口 (API) 中可用类的实例。

  • Comparison:比较方法用于比较 C 数据值。

  • Informational:这组方法可提供 C 数据值的元数据,包括大小和对齐方式。

  • Infrastructural:基础结构方法用于执行后勤操作,如复制、填充和释放内存。

完整的 FFI 类记录在此处: https://www.php.net/manual/en/class.ffi.php

有趣的是,所有 FFI 类方法都可以静态方式调用。现在是时候深入了解与 FFI 相关的类的细节和用法了,首先从创建方法开始。

使用 FFI 创建方法

属于创建类的 FFI 方法旨在直接生成 FFI 实例或 FFI 扩展提供的类的实例。在使用通过 FFI 扩展提供的 C 函数时,必须认识到不能直接将本地 PHP 变量传入函数并指望它起作用。必须先将数据创建为 FFI 数据类型或导入到 FFI 数据类型中,然后才能将 FFI 数据类型传递到 C 函数中。要创建 FFI 数据类型,请使用表 4.1 中汇总的函数之一:

Table 1. Table 4.1 – Summary of FFI class creational methods
FFI 方法 返回 描述

arrayType()

FFI\CType

第一个参数是调用 FFI::type() 生成的数据类型。第二个参数是一个数组,代表数组的维数。

cdef()

FFI

作为参数,您必须提供一个 C 语言代码块。返回的 FFI 实例是 PHP 程序和 C 语言库之间的桥梁。

new()

FFI\CData

如果第一个参数是字符串,它必须是一个有效的 C 类型,否则,请提供一个 FFI\CType 对象作为第一个参数。

scope()

FFI

根据预加载的 C 声明创建 FFI 实例。

string()

string

从 FFI\CData 实例返回包含给定字节数的本地 PHP 字符串。

type()

FFI\CType

创建一个 FFI\CType 对象,该对象可与其他 FFI 扩展类一起使用。

cdef()scope() 方法会直接产生一个 FFI 实例,而其他方法则会产生可用于创建 FFI 实例的对象实例。 string() 用于从本地 C 变量中提取给定的字节数。让我们来看看如何创建和使用 FFI/CType 实例。

创建和使用 FFI\CType 实例

需要注意的是,一旦创建了 FFI\CType 实例,不要把它当做一个本地 PHP 变量一样简单地给它赋值。这样做会覆盖 FFI\CType 实例,因为 PHP 是松散类型的。相反,要给 FFI\CType 实例分配一个标量值,可以使用它的 cdata 属性。

下面的示例创建了一个 $arr C 数组。然后在本地 C 数组中填充数值,直至其最大值,之后我们使用简单的 var_dump() 查看其内容。我们的操作步骤如下:

  1. 首先,我们使用 FFI::arrayType() 创建数组。作为参数,我们提供 FFI::type() 方法和维数。然后,我们使用 FFI::new() 创建 FFI\Ctype 实例。代码示例如下:

    // /repo/ch04/php8_ffi_array.php
    $type = FFI::arrayType(FFI::type("char"), [3, 3]);
    $arr = FFI::new($type);
  2. 或者,我们也可以将这些操作合并为一条语句,如图所示:

    $arr = FFI::new(FFI::type("char[3][3]"));
  3. 然后,我们初始化三个提供测试数据的变量,如下代码片段所示。请注意,本地 PHP count() 函数适用于 FFI\CData 数组类型:

    $pos = 0;
    $val = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    $y_max = count($arr);
  4. 现在,我们可以像使用 PHP 数组一样在其中填充值,但需要使用 cdata 属性,以便将元素保留为 FFI\CType 实例。代码如下所示:

    for ($y = 0; $y < $y_max; $y++) {
        $x_max = count($arr[$y]);
        for ($x = 0; $x < $x_max; $x++) {
            $arr[$y][$x]->cdata = $val[$pos++];
        }
    }
    var_dump($arr);

在前面的示例中,我们使用嵌套的 for() 循环将字母填充到 3 x 3 的二维数组中。如果我们现在执行一个简单的 var_dump(),会得到如下结果:

root@php8_tips_php8 [ /repo/ch04 ]# php php8_ffi_array.php

object(FFI\CData:char[3][3])#2 (3) {
    [0]=> object(FFI\CData:char[3])#3 (3) {
        [0]=> string(1) "A"
        [1]=> string(1) "B"
        [2]=> string(1) "C"
    }
    [1]=> object(FFI\CData:char[3])#1 (3) {
        [0]=> string(1) "D"
        [1]=> string(1) "E"
        [2]=> string(1) "F"
    }
    [2]=> object(FFI\CData:char[3])#4 (3) {
        [0]=> string(1) "G"
        [1]=> string(1) "H"
        [2]=> string(1) "I"
    }

从输出结果中需要注意的第一件事是,索引都是整数。var_dump() 显示每个数组元素都是一个 FFI\CData 实例。另外,请注意 C 语言字符串的处理方式与数组类似。

因为数组的类型是 char,所以我们可以使用 FFI::string() 来显示其中一行。下面是一条产生 ABC 响应的命令:

echo FFI::string($arr[0], 3);

任何将 FFI\CData 实例提供给将数组作为参数的 PHP 函数的尝试都是注定要失败的,即使它被定义为数组类型。在下面的代码片段中,请注意如果我们将这条命令添加到前面的代码块中,输出结果会是什么:

echo implode(',', $arr);

从下图的输出结果可以看出,由于数据类型不是数组,implode() 会产生一个致命错误。下面是结果输出:

PHP Fatal error: Uncaught TypeError: implode(): Argument #2
($array) must be of type ?array, FFI\CData given in /repo/ch04/
php8_ffi_array.php:25

您现在知道如何创建和使用 FFI\CType 实例了。现在,让我们把注意力转向创建 FFI 实例。

创建和使用 FFI 实例

正如本章引言中提到的,FFI 扩展有助于快速原型开发。因此,使用 FFI 扩展,您可以一次开发出设计用于新扩展的 C 函数,并立即在 PHP 应用程序中进行测试。

FFI 扩展不编译 C 代码。要在 FFI 扩展中使用 C 函数,必须先使用 C 编译器将 C 代码编译成共享库。您将在本章最后一节 "在应用程序中使用 FFI" 中了解如何进行编译。

为了在 PHP 和本地 C 库函数调用之间架起桥梁,需要创建一个 FFI 实例。FFI 扩展需要提供一个 C 定义,定义 C 函数签名和计划使用的 C 库。FFI::cdef()FFI::scope() 都可以用来直接创建 FFI 实例。

下面的示例使用 FFI::cdef() 绑定了两个本地 C 库函数。结果如下:

  1. 第一个本地方法 srand() 用于随机化序列的播种。另一个本地 C 语言函数 rand() 用于调用序列中的下一个数字。$key 变量保存随机化的最终结果。$size 表示要调用的随机数个数。代码示例如下

    // /repo/ch04/php8_ffi_cdef.php
    $key = '';
    $size = 4;
  2. 然后,我们通过调用 cdef() 创建 FFI 实例,并从 libc.so.6 本地 C 语言库中提取字符串 $code 来识别本地 C 语言函数,如下所示:

    $code = <<<EOT
    void srand (unsigned int seed);
    int rand (void);
    EOT;
    $ffi = FFI::cdef($code, 'libc.so.6');
  3. 然后,我们调用 srand() 为随机化播种。然后,在一个循环中,我们调用 rand() 本地 C 库函数生成一个随机数。我们使用本地 PHP 函数 sprintf() 将生成的整数转换为十六进制,并将其输出附加到 $key 中,然后回传。代码见此处:

    $ffi->srand(random_int(0, 999));
    for ($x = 0; $x < $size; $x++)
        $key .= sprintf('%x', $ffi->rand());
    echo $key;

    下面是前面代码段的输出结果。请注意,生成的值可以用作随机密钥:

    root@php8_tips_php8 [ /repo/ch04 ]# php php8_ffi_cdef.php
    23f306d51227432e7d8d921763b7eedf

    在输出结果中,你会看到一串转换为十六进制的串联随机整数。请注意,每次调用脚本时,结果值都会发生变化。

    要实现真正的随机化,最好使用 random_int() PHP 本地函数。此外,openssl 扩展中还有很多出色的密钥生成函数。这里展示的示例主要是为了让你熟悉 FFI 扩展的用法。

    FFI 扩展还包括两个额外的创建方法:FFI::load()FFI::scope()FFI::load() 用于在预加载过程中直接从 C 头文件(*.h)中加载 C 函数定义。FFI::scope() 可通过 FFI 扩展使用预加载的 C 函数。有关预加载的更多信息,请点击此处查看 FFI 文档中的完整预加载示例: https://www.php.net/manual/en/ffi.examples-complete.php

现在让我们来看看用于比较本地 C 数据类型的 FFI 扩展函数。

使用 FFI 比较数据

重要的是要记住,当你使用 FFI 扩展创建 C 语言数据结构时,它存在于 PHP 应用程序之外。正如您在前面的示例中所看到的(参见创建和使用 FFI\CType 实例部分),PHP 可以在一定程度上与 C 数据交互。不过,出于比较的目的,最好使用 FFI::memcmp(),因为本地 PHP 函数可能会返回不一致的结果。

表 4.2 总结了 FFI 扩展中可用的两个比较函数:

Table 2. Table 4.2 – Summary of FFI class comparison methods
FFI 方法 Return 描述

isNull()

bool

如果 FFI\CData 实例是空指针,则返回 TRUE。

memcmp()

int

如果第一个 FFI\CData 实例小于第二个,则返回小于 0 的值。如果两个实例相等,则返回 0;如果第二个参数的值大于第一个,则返回大于 0 的值。

FFI::isNull() 可用于确定 FFI\CData 实例是否为 NULL。更有趣的是 FFI::memcmp()。虽然该函数的操作方式与空格运算符 (<=>) 相同,但它接受第三个参数,该参数代表您希望在比较中包含多少字节。下面的示例说明了这种用法:

  1. 我们首先定义了一组代表 FFI\CData 实例的四个变量,这些实例最多可包含六个字符,并用样本数据填充实例,如下所示:

    // /repo/ch04/php8_ffi_memcmp.php
    $a = FFI::new("char[6]");
    $b = FFI::new("char[6]");
    $c = FFI::new("char[6]");
    $d = FFI::new("char[6]");
  2. 回想一下,C 语言将字符数据视为数组,因此即使使用 cdata 属性,我们也不能直接赋值字符串。因此,我们需要定义一个匿名函数,用字母表中的字母填充实例。我们使用以下代码来实现这一功能:

    $populate = function ($cdata, $start, $offset, $num) {
        for ($x = 0; $x < $num; $x++)
            $cdata[$x + $offset] = chr($x + $offset + $start);
        return $cdata;
    };
  3. 接下来,我们使用该函数为四个 FFI\CData 实例填充不同的字母集,如下所示:

    $a = $populate($a, 65, 0, 6);
    $b = $populate($b, 65, 0, 3);
    $b = $populate($b, 85, 3, 3);
    $c = $populate($c, 71, 0, 6);
    $d = $populate($d, 71, 0, 6);
  4. 现在,我们可以使用 FFI::string() 方法显示迄今为止的内容,如下所示:

    $patt = "%2s : %6s\n";
    printf($patt, '$a', FFI::string($a, 6));
    printf($patt, '$b', FFI::string($b, 6));
    printf($patt, '$c', FFI::string($c, 6));
    printf($patt, '$d', FFI::string($d, 6));
  5. 下面是 printf() 语句的输出结果:

    $a : ABCDEF
    $b : ABCXYZ
    $c : GHIJKL
    $d : GHIJKL
  6. 从输出结果可以看出,$c$d 的值相同。$a$b 的前三个字符相同,但后三个字符不同。

  7. 此时,如果我们尝试使用飞船运算符 (<=>) 进行比较,结果将如下:

    PHP Fatal error: Uncaught FFI\Exception: Comparison of
    incompatible C types
  8. 同样,如果尝试使用 strcmp(),即使数据是字符类型,结果也会如下:

    PHP Warning: strcmp() expects parameter 1 to be string,
    object given
  9. 因此,我们唯一的选择是使用 FFI::memcmp()。在这里显示的一组比较中,请注意第三个参数是 6,表示 PHP 应最多比较 6 个字符:

    $p = "%20s : %2d\n";
    printf($p, 'memcmp($a, $b, 6)', FFI::memcmp($a, $b, 6));
    printf($p, 'memcmp($c, $a, 6)', FFI::memcmp($c, $a, 6));
    printf($p, 'memcmp($c, $d, 6)', FFI::memcmp($c, $d, 6));
  10. 不出所料,输出结果与在本地 PHP 字符串上使用空格运算符相同,如图所示:

    memcmp($a, $b, 6) : -1
    memcmp($c, $a, 6) : 1
    memcmp($c, $d, 6) : 0
  11. 请注意,如果我们将比较限制为只有三个字符,会发生什么情况。下面是添加到代码块中的另一个 FFI::memcmp() 比较,将第三个参数设置为 3:

    echo "\nUsing FFI::memcmp() but not full length\n";
    printf($p, 'memcmp($a, $b, 3)', FFI::memcmp($a, $b, 3));
  12. 从这里的输出结果可以看出,通过限制 memcmp() 只能使用三个字符,$a$b 被认为是相等的,因为它们都以相同的三个字符 a、b 和 c 开头:

    Using FFI::memcmp() but not full length
    memcmp($a, $b, 3) : 0

从这个示例中最重要的一点是,您需要在要比较的字符数和数据性质之间找到平衡。比较的字符越少,整个操作就越快。但是,如果数据的性质可能导致错误的结果,那么就必须增加字符数,并在性能上略有损失。

现在让我们来看看如何从 FFI 扩展数据中收集信息。

从 FFI 扩展数据中提取信息

在使用 FFI 实例和本地 C 数据结构时,本地 PHP 信息方法(如 strlen()ctype_digit())不会产生有用的信息。因此,FFI 扩展包含了三种方法,用于生成有关 FFI 扩展数据的信息。表 4.3 总结了这三种方法:

Table 3. Table 4.3 – Summary of FFI class informational methods
FFI 方法 Returns 描述

alignof()

int

返回 FFI\CType 或 FFI\CData 实例的对齐方式。将返回值可视化为内存块大小的好方法。

sizeof()

int

返回 FFI\CType 或 FFI\CData 实例使用的内存量。

typeof()

FFI\CType

返回 FFI\CData 对象的数据类型。返回的数据类型不是 PHP 本地数据类型,而是一个 FFI\CType 实例,用于标识 C 数据类型。

我们首先查看 FFI::typeof(),然后深入研究其他两个方法。

使用 FFI::typeof() 确定 FFI 数据的性质

下面的示例说明了如何使用 FFI::typeof()。该示例还说明,在处理 FFI 数据时,本地 PHP 信息函数不会产生有用的结果。这就是我们要做的:

  1. 首先,我们定义一个 $char C 字符串,并在其中填入字母表的前六个字母,如下所示:

    // /repo/ch04/php8_ffi_typeof.php
    $char = FFI::new("char[6]");
    for ($x = 0; $x < 6; $x++)
        $char[$x] = chr(65 + $x);
  2. 然后,我们尝试使用 strlen() 来获取字符串的长度。在下面的代码片段中,请注意使用了 $t::class 这相当于 get_class($t)。这种用法仅在 PHP 8 及以上版本中可用:

    try {
        echo 'Length of $char is ' . strlen($char);
    } catch (Throwable $t) {
        echo $t::class . ':' . $t->getMessage();
    }
  3. 在 PHP 7.4 中的结果是一条警告信息。但在 PHP 8 中,如果向 strlen() 传递的不是字符串,就会出现致命的错误信息。下面是 PHP 8 此时的输出结果:

    TypeError:strlen(): Argument #1 ($str) must be of type
    string, FFI\CData given
  4. 类似地,使用 ctype_alnum() 的方法如下:

    echo '$char is ' .
        ((ctype_alnum($char)) ? 'alpha' : 'non-alpha');
  5. 下面是步骤 4 中显示的 echo 命令的输出结果:

    $char is non-alpha
  6. 我们可以清楚地看到,使用本地 PHP 函数并不能获得有关 FFI 数据的任何有用信息!不过,使用 FFI::typeof()(如图所示)可以得到更好的结果:

    $type = FFI::typeOf($char);
    var_dump($type);
  7. 下面是 var_dump() 的输出结果:

    object(FFI\CType:char[6])#3 (0) {}

从最终输出结果可以看出,我们现在获得了有用的信息!现在让我们看看其他两个 FFI 信息方法。

使用 FFI::alignof() 和 FFI::sizeof()

在举例说明如何使用这两种方法之前,我们有必要了解一下对齐的具体含义。为了理解对齐方式,你需要对大多数计算机中内存的组织方式有一个基本的了解。

RAM 仍然是临时存储程序运行周期中所用信息的最快方式。计算机的中央处理器(CPU)会在程序执行时将信息移入和移出内存。内存以并行数组的形式组织。alignof() 返回的对齐值表示可以从并行对齐的内存数组中一次获取多少字节。在老式计算机中,对齐值通常为 4。对于大多数现代微机来说,8 或 16(或更大)的值很常见。

现在我们来看一个例子,说明这两种 FFI 扩展信息方法是如何使用的,以及这些信息是如何提高性能的。我们将这样进行

  1. 首先,我们创建一个 FFI 实例 $ffi,并在其中定义两个 C 结构,分别标为 Good 和 Bad。请注意,在下面的代码片段中,这两个结构具有相同的属性;不过,属性的排列顺序不同:

    $struct = 'struct Bad { char c; double d; int i; }; '
        . 'struct Good { double d; int i; char c; };';
    $ffi = FFI::cdef($struct);
  2. 然后,我们从 $ffi 中提取两个结构如下:

    $bad = $ffi->new("struct Bad");
    $good = $ffi->new("struct Good");
    var_dump($bad, $good);
  3. 这里显示了 var_dump() 的输出结果:

    object(FFI\CData:struct Bad)#2 (3) {
        ["c"]=> string(1) ""
        ["d"]=> float(0)
        ["i"]=> int(0)
    }
    object(FFI\CData:struct Good)#3 (3) {
        ["d"]=> float(0)
        ["i"]=> int(0)
        ["c"]=> string(1) ""
    }
  4. 然后,我们使用两种信息方法来报告两种数据结构,如下所示:

    echo "\nBad Alignment:\t" . FFI::alignof($bad);
    echo "\nBad Size:\t" . FFI::sizeof($bad);
    echo "\nGood Alignment:\t" . FFI::alignof($good);
    echo "\nGood Size:\t" . FFI::sizeof($good);

    此处显示了该代码示例的最后四行输出:

    Bad Alignment: 8
    Bad Size: 24
    Good Alignment: 8
    Good Size: 16

    从输出中可以看到,FFI::alignof() 的返回值告诉我们,对齐块的宽度为 8 字节。不过,你也可以看到,Bad 结构占用的字节数比 Good 结构所需的字节数大 50%。由于这两种数据结构具有完全相同的属性,任何头脑正常的开发人员都会选择 Good 结构。

从这个例子中可以看出,FFI 扩展信息方法能够让我们了解如何以最佳方式构造 C 数据,以产生最有效的结果。

有关 C 语言中 sizeof()alignof() 之间区别的精彩讨论,请参阅本文: https://stackoverflow.com/questions/11386946/whats-thedifference-between-size-of-and-alignof

现在,您已经了解了什么是 FFI 扩展信息方法,并看到了使用这些方法的一些示例。现在让我们来看看与基础设施相关的 FFI 扩展方法。

使用 FFI 基础设施方法

FFI 扩展的基础结构类别方法可以看作是幕后组件,它们支持 C 函数绑定正常工作所需的基础结构。正如我们在本章中所强调的,如果要在 PHP 应用程序中直接访问 C 数据结构,就需要使用 FFI 扩展。因此,如果需要用 PHP unset() 语句释放内存,或用 PHP include() 语句包含外部程序代码,那么 FFI 扩展的基础结构方法就是连接本地 C 数据和 PHP 的桥梁。

Table 4. Table 4.4 – FFI class infrastructural methods
FFI 方法 Returns 描述

addr()

FFI\CData

创建指向 C 数据的指针

cast()

FFI\CData

执行 C 语言类型转换

free()

void

释放一个 FFI 指针,PHP 中没有直接对应的函数

memcpy()

void

将指定字节从一个 FFI\CData 实例复制到另一个。

memset()

void

为 FFI\CData 实例填充由 $value 组成的 $size 字节。

让我们先看看 FFI::addr()free()memset()memcpy()

使用 FFI::addr()、free()、memset() 和 memcpy()

PHP 开发人员经常通过引用为变量赋值。这样,一个变量的变化就会自动反映到另一个变量中。在向函数或方法传递参数时,如果需要返回一个以上的值,使用引用尤其有用。通过引用传递,函数或方法可以返回无限多的值。

FFI::addr() 方法创建了一个指向现有 FFI\CData 实例的 C 指针。与 PHP 引用一样,对指针相关数据所做的任何更改也会被更改。

在使用 FFI::addr() 方法创建示例的过程中,我们还向您介绍了 FFI::memset()。这个函数很像 str_repeat() PHP 函数,它(FFI::memset())用一个特定值填充指定数量的字节。在本示例中,我们使用 FFI::memset() 将字母填充到 C 字符串中。

在本小节中,我们还将学习 FFI::memcpy()。该函数用于将数据从一个 FFI\CData 实例复制到另一个。与 FFI::addr() 方法不同的是,FFI::memcpy() 创建的克隆与复制数据的来源没有任何联系。此外,我们还引入了 FFI::free(),这是一种用于释放使用 FFI::addr() 创建的指针的方法。

下面让我们看看如何使用这些 FFI 扩展方法:

  1. 首先,创建一个 FFI\CData 实例 $arr,它由一个包含六个字符的 C 字符串组成。请注意,在下面的代码片段中,使用了 FFI::memset()(另一种基础方法),将美国信息交换标准码(ASCII)代码 65(字母 A)填充到字符串中:

    // /repo/ch04/php8_ffi_addr_free_memset_memcpy.php
    $size = 6;
    $arr = FFI::new(FFI::type("char[$size]"));
    FFI::memset($arr, 65, $size);
    echo FFI::string($arr, $size);
  2. 使用 FFI::string() 方法得到的回显结果如图所示:

    AAAAAA
  3. 从输出中可以看到,出现了六个 ASCII 码 65(字母 A)的实例。然后,我们创建另一个 FFI\CData 实例 $arr2 并使用 FFI::memcpy() 将六个字符从一个实例复制到另一个实例,如下所示:

    $arr2 = FFI::new(FFI::type("char[$size]"));
    FFI::memcpy($arr2, $arr, $size);
    echo FFI::string($arr2, $size);
  4. 不出所料,输出结果与步骤 2 的输出结果完全相同,我们可以在这里看到:

    AAAAAA
  5. 接下来,我们创建一个指向 $arr 的 C 指针。请注意,当指针被赋值时,它们会以数组元素的形式出现在本地 PHP var_dump() 函数中。然后,我们可以更改数组元素 0 的值,并使用 FFI::memset() 填充字母 B:

    $ref = FFI::addr($arr);
    FFI::memset($ref[0], 66, 6);
    echo FFI::string($arr, $size);
    var_dump($ref, $arr, $arr2);
  6. 下面是与步骤 5 中显示的剩余代码相关的输出结果:

    BBBBBB
    object(FFI\CData:char(*)[6])#2 (1) {
        [0]=> object(FFI\CData:char[6])#4 (6) {
            [0]=> string(1) "B"
            [1]=> string(1) "B"
            [2]=> string(1) "B"
            [3]=> string(1) "B"
            [4]=> string(1) "B"
            [5]=> string(1) "B"
        }
    }
    object(FFI\CData:char[6])#3 (6) {
        [0]=> string(1) "B"
        [1]=> string(1) "B"
        [2]=> string(1) "B"
        [3]=> string(1) "B"
        [4]=> string(1) "B"
        [5]=> string(1) "B"
    }
    object(FFI\CData:char[6])#4 (6) {
        [0]=> string(1) "A"
        [1]=> string(1) "A"
        [2]=> string(1) "A"
        [3]=> string(1) "A"
        [4]=> string(1) "A"
        [5]=> string(1) "A"
    }

    从输出结果中可以看到,我们首先得到了一个 BBBBBB 字符串。可以看到指针是 PHP 数组的形式。原始 FFI\CData 实例 $arr 现在已变为字母 B。不过,前面的输出也清楚地表明,副本 $arr2 不受对 $arr 或其 $ref[0] 指针所作更改的影响。

  7. 最后,为了释放使用 FFI::addr() 创建的指针,我们使用 FFI::free()。这个方法很像 PHP 本地的 unset() 函数,但它是为 C 指针设计的。下面是添加到示例中的最后一行代码:

    FFI::free($ref);

既然你已经了解了如何使用 C 指针以及如何为 C 数据填充信息,那么让我们来看看如何使用 FFI\CData 实例进行类型转换。

学习 FFI::cast()

在 PHP 中,类型转换的过程非常频繁。当要求 PHP 执行涉及不同数据类型的操作时,就会用到类型转换。下面的代码块就是一个典型的例子:

$a = 123;
$b = "456";
echo $a + $b;

在这个微不足道的例子中,$a 的数据类型为 int(整数),$b 的数据类型为字符串。echo 语句要求 PHP 首先将 $b 类型转换为 int,执行加法运算,然后将结果类型转换为字符串。

本地 PHP 还允许开发人员通过在变量或表达式前面的括号中预置所需的数据类型来强制数据类型。前面代码片段中的重写示例可能如下所示:

$a = 123;
$b = "456";
echo (string) ($a + (int) $b);

强制类型转换可以让使用您代码的其他开发人员非常清楚地了解您的意图。它还能保证结果,因为强制类型转换能对代码的流程施加更大的控制,而不是依赖于 PHP 的默认行为。

FFI 扩展以 FFI::cast() 方法的形式提供了类似的功能。正如您在本章中所看到的,FFI 扩展数据与 PHP 隔离,不受 PHP 类型转换的影响。为了强制数据类型,可以根据需要使用 FFI::cast() 返回并行的 FFI\CData 类型。让我们在下面的步骤中看看如何做到这一点:

  1. 在本例中,我们创建了一个 int 类型的 FFI/CData 实例 $int1。我们使用它的 cdata 属性赋值 123,如下所示:

    // /repo/ch04/php8_ffi_cast.php
    // not all lines are shown
    $patt = "%2d : %16s\n";
    $int1 = FFI::new("int");
    $int1->cdata = 123;
    $bool = FFI::cast(FFI::type("bool"), $int1);
    printf($patt, __LINE__, (string) $int1->cdata);
    printf($patt, __LINE__, (string) $bool->cdata);
  2. 从此处显示的输出中可以看到,当类型转换为 bool(布尔)时,123 的整数值在输出中显示为 1:

    8 : 123
    9 : 1
  3. 接下来,我们创建一个 int 类型的 FFI\CData 实例 $int2 并赋值 123。然后,我们将其类型转换为 float,再将其类型转换为 int,如下代码片段所示:

    $int2 = FFI::new("int");
    $int2->cdata = 123;
    $float1 = FFI::cast(FFI::type("float"), $int2);
    $int3 = FFI::cast(FFI::type("int"), $float1);
    printf($patt, __LINE__, (string) $int2->cdata);
    printf($patt, __LINE__, (string) $float1->cdata);
    printf($patt, __LINE__, (string) $int3->cdata);
  4. 最后三行的输出结果非常令人满意。我们看到原值 123 被表示为 1.7235971111195E-43。当类型转换回 int 时,我们的原始值就恢复了。下面是最后三行的输出结果:

    15 : 123
    16 : 1.7235971111195E-43
    17 : 123
  5. FFI 扩展和一般的 C 语言一样,不允许转换所有类型。例如,在最后一个代码块中,我们试图将浮点类型的 FFI\CData 实例 $float2 类型转换为 char 类型,如下所示:

    try {
        $float2 = FFI::new("float");
        $float2->cdata = 22/7;
        $char1 = FFI::cast(FFI::type("char[20]"), $float2);
        printf($patt, __LINE__, (string) $float2->cdata);
        printf($patt, __LINE__, (string) $char1->cdata);
    } catch (Throwable $t) {
        echo get_class($t) . ':' . $t->getMessage();
    }
  6. 结果是灾难性的!从此处显示的输出中可以看到,系统抛出了一个 FFI\Exception 异常:

    FFI\Exception:attempt to cast to larger type

在本节中,我们介绍了一系列 FFI 扩展方法,这些方法可以创建 FFI 扩展对象实例、比较值、收集信息并与创建的 C 数据基础结构协同工作。我们还学到了一些 FFI 扩展方法,它们与本地 PHP 语言中的这些功能如出一辙。在下一节中,我们将回顾一个使用 FFI 扩展将 C 函数库纳入 PHP 脚本的实际示例。