PHP FFI真解
PHP的扩展方式
传统扩展
PHP 传统扩展
是通过 C 语言 编写的,用于扩展 PHP 功能的动态链接库(如 .so
或 .dll
文件)。这些扩展允许 PHP 调用 C 代码、操作底层系统资源(如文件、内存等)或集成第三方库(如 curl
、openssl
、gd
等)。
这需要用PHP专用的生成配置文件的工具phpize
需要用 C 语言编写 PHP 扩展函数或类,编写好扩展代码后,使用 phpize
生成配置文件,并通过 ./configure
、make
、make install
将扩展编译为 .so
文件。
加载扩展:在 php.ini
中添加扩展路径,PHP 在运行时会加载这个 C 扩展库。
传统扩展的核心概念
- Zend 引擎:PHP 的内核是 Zend 引擎,它负责 PHP 的编译、执行和内存管理。编写扩展时,开发者需要与 Zend 引擎进行交互,理解它的生命周期和 API。
- PHP 扩展宏:PHP 提供了大量的宏和函数来简化 C 扩展的开发,比如
PHP_FUNCTION()
用于定义一个 PHP 函数,ZEND_BEGIN_ARG_INFO()
用于描述参数等。 - 内存管理:由于 PHP 使用了自己的内存管理机制(垃圾回收机制等),开发者在编写 C 扩展时需要特别注意内存分配和释放,避免内存泄漏或不安全的操作。
场景:
- 性能需求:需要极高性能时,如处理大量数据或复杂算法,C 代码的执行速度比纯 PHP 快得多。
- 与操作系统交互:需要直接操作文件系统、网络、数据库等底层资源。
- 与第三方库集成:将已有的 C/C++ 库(如
curl
、openssl
)集成到 PHP 中。
缺点:
只可惜开发难度太高,不深入理解zend和C无法写出
每次修改扩展后都需要重新编译、打包和部署,并且在不同的平台上可能需要不同的编译配置。
PHP 版本兼容性:由于 Zend 引擎的变化,扩展可能在不同版本的 PHP 中不兼容,需要手动调整代码。
新的FFI扩展(PHP7.3以后)
FFI 是 PHP 7.4 引入的扩展,允许 PHP 直接调用本地的 C 函数、使用 C 结构体,并进行内存操作,而无需像传统扩展那样编写、编译和安装 C 扩展。FFI 提供了一种运行时的方式,PHP 可以直接通过字符串定义和操作 C 语言中的符号。
更简单的流程
- 声明C代码接口:可以用
FFI::cdef()
函数直接定义要调用的C函数或结构体的接口。这些接口可以是共享库(如.so
或.dll
文件)中定义的函数。 - 加载共享库:通过
FFI::load()
加载系统中的共享库文件。然后在 PHP 中可以像调用普通 PHP 函数一样调用 C 函数。 - 调用 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 中的 ::
主要用于以下场景:
- 访问静态属性或静态方法:如
ClassName::staticMethod()
或ClassName::$staticVar
。 - 访问类常量:如
ClassName::CONSTANT_NAME
。 - 调用父类方法:如
parent::methodName()
。 - 延迟静态绑定:如
static::methodName()
。 - 匿名类中的静态成员访问:如
$anonClass::staticMethod()
。
FFI的妙用
FFI::new()
1 | // FFI::new()创建一个新的 C 数据结构(如结构体、数组、基本类型等) |
**
type
**:C 数据类型,可以是基本类型(如int
)、结构体(如struct timeval
)或数组(如int[10]
)。**
$owned
**:默认为true
,表示 PHP 拥有该对象的内存,当 PHP 脚本结束时,自动释放该内存。如果为false
,表示外部(C 代码)拥有内存,PHP 不会释放它。**
$persistent
**:用于指定是否分配持久内存。默认是false
,表示对象是临时的。
FFI::addr()
这个方法用于获取 C 数据结构的地址(类似于 C 语言中的 &
操作符)。
语法
1
FFI::addr($c_data);
- **
$c_data
**:C 数据结构的对象,它可以是结构体、数组或基本类型。这个方法返回该对象的指针,可以用于传递给需要指针的 C 函数。
- **
FFI::cdef()
cdef()
是 FFI 的核心方法之一,它用于定义 C 函数、类型和结构体,并将它们与共享库链接起来。
语法:
1
FFI::cdef("C_declarations", "shared_library_path");
- **
C_declarations
**:C 函数和结构体的声明。这个字符串包含标准 C 函数或结构体的声明。 - **
shared_library_path
**:指定要链接的共享库路径(如libc.so.6
或/usr/lib/libmylib.so
)。
- **
FFI::cast()
cast()
方法用于将一个 C 数据指针或值转换为另一种 C 类型。类似于 C 语言中的类型转换。
语法:
1
FFI::cast("type", $value);
- **
type
**:目标 C 类型(如int*
或float
)。 - **
$value
**:可以是一个指针、数字或结构体,FFI 会将其转换为指定类型。
示例:
1
2$ptr = FFI::new("int*");
$int_ptr = FFI::cast("int*", $ptr);- **
FFI::sizeof()
sizeof()方法用于返回指定 C 类型或对象的字节大小,类似于 C 语言中的
sizeof()
- **
$type_or_object
**:可以是一个 C 类型字符串或 C 数据结构实例,FFI 会返回该类型或对象的字节大小。
示例:
1 | $size = FFI::sizeof("int"); // 返回 int 的字节大小,通常为 4 |
FFI::alignof()
alignof()
方法返回给定 C 类型或对象的对齐要求。对齐是指数据在内存中的排列方式,通常与 CPU 的架构相关。
FFI::memcpy()
memcpy()
方法是一个内存复制函数,类似于 C 语言中的 memcpy()
,用于将内存区域从源复制到目标。
语法:
1
FFI::memcpy($dst, $src, $size);
- **
$dst
**:目标地址,表示数据要复制到的内存位置。 - **
$src
**:源地址,表示数据来源的内存位置。 - **
$size
**:要复制的字节大小。
示例:
1
2
3$dst = FFI::new("char[20]");
$src = "Hello";
FFI::memcpy($dst, $src, strlen($src) + 1); // 将字符串复制到 dst- **
FFI::free()
free()
方法用于手动释放通过 FFI::new()
分配的内存。如果分配时设置 $owned = false
,则必须通过 free()
来释放内存。
语法:
1 | FFI::free($c_data); |
- **
$c_data
**:通过FFI::new()
分配的 C 数据对象。
示例:
1 | $ptr = FFI::new("int[10]"); |
FFI::type()
type()
方法用于定义一个新的 C 类型,类似于在 C 语言中定义 typedef
。
语法:
1
FFI::type("type");
- **
type
**:你想定义的 C 类型。
示例:
1
$int_ptr_type = FFI::type("int*"); // 定义 int* 类型
- **
FFI::isNull()
isNull()
方法用于检查一个 C 指针是否为空指针(NULL
)。这在处理指针时非常有用。
语法:
1
FFI::isNull($ptr);
- **
$ptr
**:一个指针对象,用于检查是否为空指针。
示例:
1
2
3
4$ptr = FFI::new("int*");
if (FFI::isNull($ptr)) {
echo "Pointer is NULL";
}- **
环境:
kali hyper-V
1 | sudo apt install php-codesniffer |
然后设置json
1 | "phpcs.executablePath": "/usr/bin/phpcs" |
检查是否配置ffi
1 | php -m | grep FFI |
先来复现一下官方文档的
hello!!
先找一下libc的路径:
1 | ldconfig -p | grep libc.so.6 |
1 |
|
curl
首先下载php的curl扩展
1 | sudo apt install php-curl |
1 |
|
gettimeofday()
1 |
|
C变量的操作
1 |
|
ZEND_FFI_SYM_TYPE
:默认值是 0
,因为它是第一个枚举项,没有明确指定值。
ZEND_FFI_SYM_CONST = 2
:显式指定为 2
,跳过了 1
。
ZEND_FFI_SYM_VAR
:由于 ZEND_FFI_SYM_CONST
是 2
,因此 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.