February 26, 2024

FFI真解

PHP FFI真解

PHP的扩展方式

传统扩展

PHP 传统扩展是通过 C 语言 编写的,用于扩展 PHP 功能的动态链接库(如 .so.dll 文件)。这些扩展允许 PHP 调用 C 代码、操作底层系统资源(如文件、内存等)或集成第三方库(如 curlopensslgd 等)。

这需要用PHP专用的生成配置文件的工具phpize需要用 C 语言编写 PHP 扩展函数或类,编写好扩展代码后,使用 phpize 生成配置文件,并通过 ./configuremakemake install 将扩展编译为 .so 文件。

加载扩展:在 php.ini 中添加扩展路径,PHP 在运行时会加载这个 C 扩展库。

传统扩展的核心概念

  1. Zend 引擎:PHP 的内核是 Zend 引擎,它负责 PHP 的编译、执行和内存管理。编写扩展时,开发者需要与 Zend 引擎进行交互,理解它的生命周期和 API。
  2. PHP 扩展宏:PHP 提供了大量的宏和函数来简化 C 扩展的开发,比如 PHP_FUNCTION() 用于定义一个 PHP 函数,ZEND_BEGIN_ARG_INFO() 用于描述参数等。
  3. 内存管理:由于 PHP 使用了自己的内存管理机制(垃圾回收机制等),开发者在编写 C 扩展时需要特别注意内存分配和释放,避免内存泄漏或不安全的操作。

场景:

缺点:

  1. 只可惜开发难度太高,不深入理解zend和C无法写出

  2. 每次修改扩展后都需要重新编译、打包和部署,并且在不同的平台上可能需要不同的编译配置。

  3. PHP 版本兼容性:由于 Zend 引擎的变化,扩展可能在不同版本的 PHP 中不兼容,需要手动调整代码。

新的FFI扩展(PHP7.3以后)

FFI 是 PHP 7.4 引入的扩展,允许 PHP 直接调用本地的 C 函数、使用 C 结构体,并进行内存操作,而无需像传统扩展那样编写、编译和安装 C 扩展。FFI 提供了一种运行时的方式,PHP 可以直接通过字符串定义和操作 C 语言中的符号。

更简单的流程

  1. 声明C代码接口:可以用FFI::cdef()函数直接定义要调用的C函数或结构体的接口。这些接口可以是共享库(如 .so.dll 文件)中定义的函数。
  2. 加载共享库:通过 FFI::load() 加载系统中的共享库文件。然后在 PHP 中可以像调用普通 PHP 函数一样调用 C 函数。
  3. 调用 C 函数:一旦共享库加载成功,PHP 就可以调用该库中的函数,并可以操作 C 语言的变量和结构体。

核心概念

C 代码接口:通过 FFI::cdef() 声明 C 函数和结构体的签名,PHP 会基于这些签名与底层库进行交互。

共享库:FFI 需要指定一个共享库(.so.dll 文件)来加载这些符号。这与传统扩展不同,传统扩展是直接将 C 代码编译为 PHP 扩展,而 FFI 是通过动态加载共享库。

内存操作:FFI 允许直接操作底层的内存地址,甚至可以通过 PHP 操作指针和内存块。这对高级开发者很有用,但也存在潜在的安全隐患。

场景:

快速原型开发:如果需要快速集成某些本地库或调用系统函数,FFI 非常方便,无需编写扩展、编译和安装。

灵活性:在不需要极致性能的情况下,FFI 提供了一种动态的方式调用 C 函数,尤其适合那些不熟悉 C 扩展开发的 PHP 开发者。

简单函数调用:适合调用少量的 C 函数,或者做一些简单的系统交互。

实例

前置知识:

在 PHP 中,:: 是一种 范围解析操作符(Scope Resolution Operator),用于访问类中的 静态成员常量方法 或者用于调用 父类的方法。它是一个非常重要的符号,广泛应用于面向对象编程(OOP)中。下面详细介绍 :: 的几种主要用途。

PHP 中的 :: 主要用于以下场景:

  1. 访问静态属性或静态方法:如 ClassName::staticMethod()ClassName::$staticVar
  2. 访问类常量:如 ClassName::CONSTANT_NAME
  3. 调用父类方法:如 parent::methodName()
  4. 延迟静态绑定:如 static::methodName()
  5. 匿名类中的静态成员访问:如 $anonClass::staticMethod()

FFI的妙用

FFI::new()

1
2
3
// FFI::new()创建一个新的 C 数据结构(如结构体、数组、基本类型等)
//签名
FFI::new("type", bool $owned = true, bool $persistent = false);

FFI::addr()

这个方法用于获取 C 数据结构的地址(类似于 C 语言中的 & 操作符)。

FFI::cdef()

cdef() 是 FFI 的核心方法之一,它用于定义 C 函数、类型和结构体,并将它们与共享库链接起来。

FFI::cast()

cast() 方法用于将一个 C 数据指针或值转换为另一种 C 类型。类似于 C 语言中的类型转换。

FFI::sizeof()

sizeof()方法用于返回指定 C 类型或对象的字节大小,类似于 C 语言中的sizeof()

示例

1
$size = FFI::sizeof("int");  // 返回 int 的字节大小,通常为 4

FFI::alignof()

alignof() 方法返回给定 C 类型或对象的对齐要求。对齐是指数据在内存中的排列方式,通常与 CPU 的架构相关。

FFI::memcpy()

memcpy() 方法是一个内存复制函数,类似于 C 语言中的 memcpy(),用于将内存区域从源复制到目标。

FFI::free()

free() 方法用于手动释放通过 FFI::new() 分配的内存。如果分配时设置 $owned = false,则必须通过 free() 来释放内存。

语法

1
FFI::free($c_data);

示例

1
2
$ptr = FFI::new("int[10]");
FFI::free($ptr); // 手动释放内存

FFI::type()

type() 方法用于定义一个新的 C 类型,类似于在 C 语言中定义 typedef

FFI::isNull()

isNull() 方法用于检查一个 C 指针是否为空指针(NULL)。这在处理指针时非常有用。

环境:

kali hyper-V

1
2
sudo apt install php-codesniffer
code-oss --no-sanbox

然后设置json

1
"phpcs.executablePath": "/usr/bin/phpcs"

检查是否配置ffi

1
2
3
php -m | grep FFI
vim /etc/php/(版本)/cli/php.ini
ffi.enable = true

先来复现一下官方文档的

hello!!

先找一下libc的路径:

1
ldconfig -p | grep libc.so.6
1
2
3
4
5
6
7
8
<?php
$ffi = FFI::cdef(
"int printf(const char *format, ...);
","/path/libc.so.6"
);
$fii->printf("This is %s !!!", "FFI");
// This is FFI !!!
//... 表示的是 可变参数

curl

首先下载php的curl扩展

1
sudo apt install php-curl
1
2
3
4
5
6
7
8
9
10
11
<?php
$url = "https://www.baidu.com/"
$ch = curl_init();
//curl_init()函数用于初始化一个新的 cURL 会话,并返回一个句柄(资源类型),通过这个句柄 $ch 可以配置和执行具体的 cURL 操作。

curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);

curl_exec($ch);
curl_close($ch);
// https的,所以会多一个设置SSL_VERIFYPEER的操作,意思是不校验ssl证书

gettimeofday()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
// 创建 gettimeofday() 绑定
$ffi = FFI::cdef("
typedef unsigned int time_t;
typedef unsigned int suseconds_t;

struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};

struct timezone {
int tz_minuteswest;
int tz_dsttime;
};

int gettimeofday(struct timeval *tv, struct timezone *tz);
", "libc.so.6");
// 创建 C 数据结构
$tv = $ffi->new("struct timeval");
$tz = $ffi->new("struct timezone");
// 调用 C 的 gettimeofday()
var_dump($ffi->gettimeofday(FFI::addr($tv), FFI::addr($tz)));
// 访问 C 数据结构的字段
var_dump($tv->tv_sec);
// 打印完整数据结构
var_dump($tz);
?>

C变量的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
// 创建新的 C int 变量
$x = FFI::new("int");
var_dump($x->cdata);

// 简单赋值
$x->cdata = 5;
var_dump($x->cdata);

// 复合赋值
$x->cdata += 2;
var_dump($x->cdata);

// 创建 C 数据结构
$a = FFI::new("long[1024]");
// 使用它就像使用常规数组
for ($i = 0; $i < count($a); $i++) {
$a[$i] = $i;
}
var_dump($a[25]);
$sum = 0;
foreach ($a as $n) {
$sum += $n;
}
var_dump($sum);
var_dump(count($a));
var_dump(FFI::sizeof($a));

$a = FFI::cdef('typedef enum _zend_ffi_symbol_kind {
ZEND_FFI_SYM_TYPE,
ZEND_FFI_SYM_CONST = 2,
ZEND_FFI_SYM_VAR,
ZEND_FFI_SYM_FUNC
} zend_ffi_symbol_kind;
');
var_dump($a->ZEND_FFI_SYM_TYPE);
var_dump($a->ZEND_FFI_SYM_CONST);
var_dump($a->ZEND_FFI_SYM_VAR);

ZEND_FFI_SYM_TYPE:默认值是 0,因为它是第一个枚举项,没有明确指定值。

ZEND_FFI_SYM_CONST = 2:显式指定为 2,跳过了 1

ZEND_FFI_SYM_VAR:由于 ZEND_FFI_SYM_CONST2,因此 ZEND_FFI_SYM_VAR 自动取 3

ZEND_FFI_SYM_FUNC:紧随其后,自动取 4

About this Post

This post is written by void2eye, licensed under CC BY-NC 4.0.

#Web