PHP7.4中FFI的介绍(代码示例)
来源:不言
发布时间:2019-03-05 17:18:56
阅读量:1556
FFI扩展已经通过RFC,正式成为PHP 7.4核心扩展。
什么是FFI
FFI(Foreign Function Interface),即外部函数接口,是指在一种语言里调用另一种语言代码的技术。PHP的FFI扩展就是一个让你在PHP里调用C代码的技术。
FFI的使用非常简单,只用声明和调用两步就可以,对于有C语言经验,但是不了解Zend引擎的程序员来说,这简直是打开了新世界的大门,可以快速地使用C类库进行原型试验。
(此处有图:溜了溜了,要懂C的……)
下面通过3个例子,看一下FFI是怎样使用的。
Libbloom
libbloom是一个C实现的bloom filter,比较知名的用户有Shadowsocks-libev,下面看一下怎样通过FFI在PHP里调用libbloom。
第一步,从头文件bloom.h把主要的数据结构和函数声明复制出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | $ffi = FFI::cdef("
struct bloom
{
int entries;
double error;
int bits;
int bytes;
int hashes;
double bpe;
unsigned char * bf;
int ready;
};
int bloom_init(struct bloom * bloom, int entries, double error);
int bloom_check(struct bloom * bloom, const void * buffer, int len);
int bloom_add(struct bloom * bloom, const void * buffer, int len);
void bloom_free(struct bloom * bloom);
", " libbloom.so.1.5");
|
FFI目前不支持预处理器(除了FFI_LIB
和FFI_SCOPE
),所以宏定义要自己展开。
之后就可以通过$ffi
创建已声明的数据结构和调用函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | $bloom = FFI::addr( $ffi -> new ( "struct bloom" ));
$ffi ->bloom_init( $bloom , 10000, 0.01);
$ffi ->bloom_add( $bloom , "PHP" , 3);
$ffi ->bloom_add( $bloom , "C" , 1);
var_dump( $ffi ->bloom_check( $bloom , "PHP" , 3));
var_dump( $ffi ->bloom_check( $bloom , "Laravel" , 7));
$ffi ->bloom_free( $bloom );
$bloom = null;
|
Linux Namespace
Linux命名空间是容器技术的基石之一,通过FFI可以直接调用glibc的对应系统调用封装,从而通过PHP实现容器。下面是一个让bash在一个新的命名空间里运行的例子。
首先是一些常量,可以从Linux的头文件得到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | const CLONE_NEWNS = 0x00020000;
const CLONE_NEWCGROUP = 0x02000000;
const CLONE_NEWUTS = 0x04000000;
const CLONE_NEWIPC = 0x08000000;
const CLONE_NEWUSER = 0x10000000;
const CLONE_NEWPID = 0x20000000;
const CLONE_NEWNET = 0x40000000;
const MS_NOSUID = 2;
const MS_NODEV = 4;
const MS_NOEXEC = 8;
const MS_PRIVATE = 1 << 18;
const MS_REC = 16384;
|
接着时我们要用到的函数声明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $cdef ="
int clone (int (*fn)(void *), void *child_stack, int flags, void *arg);
int mount( const char *source, const char *target, const char *filesystemtype,
unsigned long mountflags, const void *data);
int setgid(int gid);
int setuid(int uid);
int sethostname(char *name, unsigned int len);
";
$libc = FFI::cdef( $cdef , "libc.so.6" );
|
定义我们的子进程:
1 2 3 4 5 6 7 8 9 10 11 12 | $containerId = sha1(random_bytes(8));
$childfn = function () use ( $libc , $containerId ) {
usleep(1000);
$libc ->mount( "proc" , "/proc" , "proc" , MS_NOSUID | MS_NODEV | MS_NOEXEC, null);
$libc ->setuid(0);
$libc ->setgid(0);
$libc ->sethostname( $containerId , strlen ( $containerId ));
pcntl_exec( "/bin/sh" );
};
|
在子进程里,我们重新挂载了/proc
,设置了uid、gid和hostname,然后启动/bin/sh
。
父进程通过clone函数,创建子进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | $child_stack = FFI:: new ( "char[1024 * 4]" );
$child_stack = FFI::cast( 'void *' , FFI::addr( $child_stack )) - 1024 * 4;
$pid = $libc -> clone ( $childfn , $child_stack , CLONE_NEWUSER
| CLONE_NEWNS
| CLONE_NEWPID
| CLONE_NEWUTS
| CLONE_NEWIPC
| CLONE_NEWNET
| CLONE_NEWCGROUP
| SIGCHLD, null);
$uid = getmyuid ();
$gid = getmyuid ();
file_put_contents ( "/proc/$pid/uid_map" , "0 $uid 1" );
file_put_contents ( "/proc/$pid/setgroups" , "deny" );
file_put_contents ( "/proc/$pid/gid_map" , "0 $gid 1" );
pcntl_wait( $pid );
|
glibc的clone函数是clone系统调用的封装,它需要一个函数指针作为子进程/线程的执行体,我们可以直接把PHP的闭包和匿名函数当作函数指针使用。
运行效果:
1 2 3 4 5 6 7 8 9 10 11 12 | $ php container.php
sh-5.0# id # 在容器内是root
uid=0(root) gid=0(root) groups=0(root),65534(nobody)
sh-5.0# ps aux # 独立的PID进程空间
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 10524 4124 pts/1 S 10:19 0:00 /bin/sh
root 3 0.0 0.0 15864 3076 pts/1 R+ 10:19 0:00 ps aux
sh-5.0# ip a # 独立的网络命名空间
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
|
raylib
raylib是个特性丰富而且易用的游戏库,经过简单的封装就可以在PHP里使用。下面这个例子实现了一个跟随鼠标的圆:
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 | <?php
include __DIR__ . "/../../RayLib.php" ;
RayLib::init();
RayLib::InitWindow(400, 300, "raylib example" );
$ballPosition = RayLib::Vector2(-100.0, 100.0);
while (!RayLib::WindowShouldClose())
{
$ballPosition = RayLib::GetMousePosition();
RayLib::BeginDrawing();
RayLib::ClearBackground(RayLib:: $RAYWHITE );
RayLib::DrawCircleV( $ballPosition , 40, RayLib:: $RED );
RayLib::DrawFPS(10, 10);
RayLib::EndDrawing();
}
RayLib::CloseWindow();
|
不足
性能
C类库性能可能很高,但是FFI调用的消耗也非常大,通过FFI访问数据要比PHP访问对象和数组慢两倍,所以用FFI不一定能提高性能,RFC里给出的一个测试结果:
就算用了JIT,还是比不上不用JIT的PHP。
功能
目前(20190301)FFI扩展还没实现的一些功能:
返回struct/union和数组
嵌套的struct(我写了个简单的补丁)
使用这些功能的时候,会抛出异常,提示功能未实现,所以只用等等或者马上贡献代码就好:)
参考
专栏
Oraoto的日常
文章详情
oraoto 4.4k 发布于 Oraoto的日常
1 天前发布
PHP 7.4 前瞻:FFI
212 次阅读 · 读完需要 19 分钟
6
FFI扩展已经通过RFC,正式成为PHP 7.4核心扩展。
什么是FFI
FFI(Foreign Function Interface),即外部函数接口,是指在一种语言里调用另一种语言代码的技术。PHP的FFI扩展就是一个让你在PHP里调用C代码的技术。
FFI的使用非常简单,只用声明和调用两步就可以,对于有C语言经验,但是不了解Zend引擎的程序员来说,这简直是打开了新世界的大门,可以快速地使用C类库进行原型试验。
(此处有图:溜了溜了,要懂C的……)
下面通过3个例子,看一下FFI是怎样使用的。
Libbloom
libbloom是一个C实现的bloom filter,比较知名的用户有Shadowsocks-libev,下面看一下怎样通过FFI在PHP里调用libbloom。
第一步,从头文件bloom.h把主要的数据结构和函数声明复制出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | $ffi = FFI::cdef("
struct bloom
{
int entries;
double error;
int bits;
int bytes;
int hashes;
double bpe;
unsigned char * bf;
int ready;
};
int bloom_init(struct bloom * bloom, int entries, double error);
int bloom_check(struct bloom * bloom, const void * buffer, int len);
int bloom_add(struct bloom * bloom, const void * buffer, int len);
void bloom_free(struct bloom * bloom);
", " libbloom.so.1.5");
|
FFI目前不支持预处理器(除了FFI_LIB
和FFI_SCOPE
),所以宏定义要自己展开。
之后就可以通过$ffi
创建已声明的数据结构和调用函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | $bloom = FFI::addr( $ffi -> new ( "struct bloom" ));
$ffi ->bloom_init( $bloom , 10000, 0.01);
$ffi ->bloom_add( $bloom , "PHP" , 3);
$ffi ->bloom_add( $bloom , "C" , 1);
var_dump( $ffi ->bloom_check( $bloom , "PHP" , 3));
var_dump( $ffi ->bloom_check( $bloom , "Laravel" , 7));
$ffi ->bloom_free( $bloom );
$bloom = null;
|
Linux Namespace
Linux命名空间是容器技术的基石之一,通过FFI可以直接调用glibc的对应系统调用封装,从而通过PHP实现容器。下面是一个让bash在一个新的命名空间里运行的例子。
首先是一些常量,可以从Linux的头文件得到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | const CLONE_NEWNS = 0x00020000;
const CLONE_NEWCGROUP = 0x02000000;
const CLONE_NEWUTS = 0x04000000;
const CLONE_NEWIPC = 0x08000000;
const CLONE_NEWUSER = 0x10000000;
const CLONE_NEWPID = 0x20000000;
const CLONE_NEWNET = 0x40000000;
const MS_NOSUID = 2;
const MS_NODEV = 4;
const MS_NOEXEC = 8;
const MS_PRIVATE = 1 << 18;
const MS_REC = 16384;
|
接着时我们要用到的函数声明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $cdef ="
int clone (int (*fn)(void *), void *child_stack, int flags, void *arg);
int mount( const char *source, const char *target, const char *filesystemtype,
unsigned long mountflags, const void *data);
int setgid(int gid);
int setuid(int uid);
int sethostname(char *name, unsigned int len);
";
$libc = FFI::cdef( $cdef , "libc.so.6" );
|
定义我们的子进程:
1 2 3 4 5 6 7 8 9 10 11 12 | $containerId = sha1(random_bytes(8));
$childfn = function () use ( $libc , $containerId ) {
usleep(1000);
$libc ->mount( "proc" , "/proc" , "proc" , MS_NOSUID | MS_NODEV | MS_NOEXEC, null);
$libc ->setuid(0);
$libc ->setgid(0);
$libc ->sethostname( $containerId , strlen ( $containerId ));
pcntl_exec( "/bin/sh" );
};
|
在子进程里,我们重新挂载了/proc
,设置了uid、gid和hostname,然后启动/bin/sh
。
父进程通过clone函数,创建子进程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | $child_stack = FFI:: new ( "char[1024 * 4]" );
$child_stack = FFI::cast( 'void *' , FFI::addr( $child_stack )) - 1024 * 4;
$pid = $libc -> clone ( $childfn , $child_stack , CLONE_NEWUSER
| CLONE_NEWNS
| CLONE_NEWPID
| CLONE_NEWUTS
| CLONE_NEWIPC
| CLONE_NEWNET
| CLONE_NEWCGROUP
| SIGCHLD, null);
$uid = getmyuid ();
$gid = getmyuid ();
file_put_contents ( "/proc/$pid/uid_map" , "0 $uid 1" );
file_put_contents ( "/proc/$pid/setgroups" , "deny" );
file_put_contents ( "/proc/$pid/gid_map" , "0 $gid 1" );
pcntl_wait( $pid );
|
glibc的clone函数是clone系统调用的封装,它需要一个函数指针作为子进程/线程的执行体,我们可以直接把PHP的闭包和匿名函数当作函数指针使用。
运行效果:
1 2 3 4 5 6 7 8 9 10 11 12 | $ php container.php
sh-5.0# id # 在容器内是root
uid=0(root) gid=0(root) groups=0(root),65534(nobody)
sh-5.0# ps aux # 独立的PID进程空间
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 10524 4124 pts/1 S 10:19 0:00 /bin/sh
root 3 0.0 0.0 15864 3076 pts/1 R+ 10:19 0:00 ps aux
sh-5.0# ip a # 独立的网络命名空间
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
|
raylib
raylib是个特性丰富而且易用的游戏库,经过简单的封装就可以在PHP里使用。下面这个例子实现了一个跟随鼠标的圆:
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 | <?php
include __DIR__ . "/../../RayLib.php" ;
RayLib::init();
RayLib::InitWindow(400, 300, "raylib example" );
$ballPosition = RayLib::Vector2(-100.0, 100.0);
while (!RayLib::WindowShouldClose())
{
$ballPosition = RayLib::GetMousePosition();
RayLib::BeginDrawing();
RayLib::ClearBackground(RayLib:: $RAYWHITE );
RayLib::DrawCircleV( $ballPosition , 40, RayLib:: $RED );
RayLib::DrawFPS(10, 10);
RayLib::EndDrawing();
}
RayLib::CloseWindow();
|
不足
性能
C类库性能可能很高,但是FFI调用的消耗也非常大,通过FFI访问数据要比PHP访问对象和数组慢两倍,所以用FFI不一定能提高性能,RFC里给出的一个测试结果:
就算用了JIT,还是比不上不用JIT的PHP。
功能
目前(20190301)FFI扩展还没实现的一些功能:
返回struct/union和数组
嵌套的struct(我写了个简单的补丁)
使用这些功能的时候,会抛出异常,提示功能未实现,所以只用等等或者马上贡献代码就好:)
参考
你可能感兴趣的
netstu
我觉得这是在瞎整,用zephir来编写C扩展已经非常方便了,可以避免很多问题,本来php就4不像的,这样搞只能把php搞的臃肿而且八不像的
已赞。
Zephir也好,PHP-X也好,都少不了一个编译过程,而FFI不用编译,改完脚本就能刷新执行,这就是一个快速迭代和快速实验的优势,就像这篇文章的一样玩玩各种C类库是非常方便的。不过,因为性能原因,我也不会在生产环境用FFI。
而且FFI只是个扩展,技术上和其他PHP扩展没本质区别,只是有PHP官方维护而已,对PHP核心根本没影响,谈不上让PHP更臃肿,不需要的大可不用。