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把主要的数据结构和函数声明复制出来:
$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创建已声明的数据结构和调用函数:
// 创建一个bloom结构体,然后用FFI::addr取地址// libbloom的函数都是使用bloom结构体的指针$bloom = FFI::addr($ffi- new( struct bloom ));// 调用libbloom的初始化函数$ffi- bloom_init($bloom, 10000, 0.01);// 添加数据$ffi- bloom_add($bloom, PHP , 3);$ffi- bloom_add($bloom, C , 1);// PHP可能存在var_dump($ffi- bloom_check($bloom, PHP , 3)); // 1// Laravel不存在var_dump($ffi- bloom_check($bloom, Laravel , 7)); // 0// 释放$ffi- bloom_free($bloom);$bloom = null;Linux Namespace
Linux命名空间是容器技术的基石之一,通过FFI可以直接调用glibc的对应系统调用封装,从而通过PHP实现容器。下面是一个让bash在一个新的命名空间里运行的例子。
首先是一些常量,可以从Linux的头文件得到:
// cloneconst CLONE_NEWNS = 0x00020000; // mount namespaceconst CLONE_NEWCGROUP = 0x02000000; // cgroup namespaceconst CLONE_NEWUTS = 0x04000000; // utsname namespaceconst CLONE_NEWIPC = 0x08000000; // ipc namespaceconst CLONE_NEWUSER = 0x10000000; // user namespaceconst CLONE_NEWPID = 0x20000000; // pid namespaceconst CLONE_NEWNET = 0x40000000; // network namespace// mountconst MS_NOSUID = 2;const MS_NODEV = 4;const MS_NOEXEC = 8;const MS_PRIVATE = 1 18;const MS_REC = 16384;
接着时我们要用到的函数声明:
$cdef= // fork进程 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); // 设置gid int setgid(int gid); // 设置uid int setuid(int uid); // 设置hostname int sethostname(char *name, unsigned int len);$libc = FFI::cdef($cdef, libc.so.6
定义我们的子进程:
// 生成一个容器ID$containerId = sha1(random_bytes(8));// 定义子进程$childfn = function() use ($libc, $containerId) { usleep(1000); // wait for uid/gid map $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函数,创建子进程:
// 分配子进程的栈$child_stack = FFI::new( char[1024 * 4] $child_stack = FFI::cast( void * , FFI::addr($child_stack)) - 1024 * 4;// fork子进程$pid = $libc- clone($childfn, $child_stack, CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWNET | CLONE_NEWCGROUP | SIGCHLD, null);// 设置UID、GID映射,把容器内的root映射到当前用户$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的闭包和匿名函数当作函数指针使用。
运行效果:
$ php container.phpsh-5.0# id # 在容器内是rootuid=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 COMMANDroot 1 0.0 0.1 10524 4124 pts/1 S 10:19 0:00 /bin/shroot 3 0.0 0.0 15864 3076 pts/1 R+ 10:19 0:00 ps auxsh-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:00raylib
raylib是个特性丰富而且易用的游戏库,经过简单的封装就可以在PHP里使用。下面这个例子实现了一个跟随鼠标的圆:
?phpinclude __DIR__ . /../../RayLib.php // 初始化RayLib::init(); // 初始化FFI和“常量”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); // 显示FPS RayLib::EndDrawing();// 释放RayLib::CloseWindow();不足性能
就算用了JIT,还是比不上不用JIT的PHP。
功能
目前(20190301)FFI扩展还没实现的一些功能:
使用这些功能的时候,会抛出异常,提示功能未实现,所以只用等等或者马上贡献代码就好:)
参考PHP RFC: FFI - Foreign Function Interface:RFC文档,有比较完整的API和设计目的专栏Oraoto的日常文章详情
oraoto
FFI扩展已经通过RFC,正式成为PHP 7.4核心扩展。
什么是FFIFFI(Foreign Function Interface),即外部函数接口,是指在一种语言里调用另一种语言代码的技术。PHP的FFI扩展就是一个让你在PHP里调用C代码的技术。
FFI的使用非常简单,只用声明和调用两步就可以,对于有C语言经验,但是不了解Zend引擎的程序员来说,这简直是打开了新世界的大门,可以快速地使用C类库进行原型试验。
(此处有图:溜了溜了,要懂C的……)
下面通过3个例子,看一下FFI是怎样使用的。
Libbloomlibbloom是一个C实现的bloom filter,比较知名的用户有Shadowsocks-libev,下面看一下怎样通过FFI在PHP里调用libbloom。
第一步,从头文件bloom.h把主要的数据结构和函数声明复制出来:
$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创建已声明的数据结构和调用函数:
// 创建一个bloom结构体,然后用FFI::addr取地址// libbloom的函数都是使用bloom结构体的指针$bloom = FFI::addr($ffi- new( struct bloom ));// 调用libbloom的初始化函数$ffi- bloom_init($bloom, 10000, 0.01);// 添加数据$ffi- bloom_add($bloom, PHP , 3);$ffi- bloom_add($bloom, C , 1);// PHP可能存在var_dump($ffi- bloom_check($bloom, PHP , 3)); // 1// Laravel不存在var_dump($ffi- bloom_check($bloom, Laravel , 7)); // 0// 释放$ffi- bloom_free($bloom);$bloom = null;Linux Namespace
Linux命名空间是容器技术的基石之一,通过FFI可以直接调用glibc的对应系统调用封装,从而通过PHP实现容器。下面是一个让bash在一个新的命名空间里运行的例子。
首先是一些常量,可以从Linux的头文件得到:
// cloneconst CLONE_NEWNS = 0x00020000; // mount namespaceconst CLONE_NEWCGROUP = 0x02000000; // cgroup namespaceconst CLONE_NEWUTS = 0x04000000; // utsname namespaceconst CLONE_NEWIPC = 0x08000000; // ipc namespaceconst CLONE_NEWUSER = 0x10000000; // user namespaceconst CLONE_NEWPID = 0x20000000; // pid namespaceconst CLONE_NEWNET = 0x40000000; // network namespace// mountconst MS_NOSUID = 2;const MS_NODEV = 4;const MS_NOEXEC = 8;const MS_PRIVATE = 1 18;const MS_REC = 16384;
接着时我们要用到的函数声明:
$cdef= // fork进程 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); // 设置gid int setgid(int gid); // 设置uid int setuid(int uid); // 设置hostname int sethostname(char *name, unsigned int len);$libc = FFI::cdef($cdef, libc.so.6
定义我们的子进程:
// 生成一个容器ID$containerId = sha1(random_bytes(8));// 定义子进程$childfn = function() use ($libc, $containerId) { usleep(1000); // wait for uid/gid map $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函数,创建子进程:
// 分配子进程的栈$child_stack = FFI::new( char[1024 * 4] $child_stack = FFI::cast( void * , FFI::addr($child_stack)) - 1024 * 4;// fork子进程$pid = $libc- clone($childfn, $child_stack, CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWNET | CLONE_NEWCGROUP | SIGCHLD, null);// 设置UID、GID映射,把容器内的root映射到当前用户$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的闭包和匿名函数当作函数指针使用。
运行效果:
$ php container.phpsh-5.0# id # 在容器内是rootuid=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 COMMANDroot 1 0.0 0.1 10524 4124 pts/1 S 10:19 0:00 /bin/shroot 3 0.0 0.0 15864 3076 pts/1 R+ 10:19 0:00 ps auxsh-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:00raylib
raylib是个特性丰富而且易用的游戏库,经过简单的封装就可以在PHP里使用。下面这个例子实现了一个跟随鼠标的圆:
?phpinclude __DIR__ . /../../RayLib.php // 初始化RayLib::init(); // 初始化FFI和“常量”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); // 显示FPS RayLib::EndDrawing();// 释放RayLib::CloseWindow();不足性能
就算用了JIT,还是比不上不用JIT的PHP。
功能
目前(20190301)FFI扩展还没实现的一些功能:
使用这些功能的时候,会抛出异常,提示功能未实现,所以只用等等或者马上贡献代码就好:)
参考PHP RFC: FFI - Foreign Function Interface:RFC文档,有比较完整的API和设计目的赞%20|%206%20收藏%20|%204
你可能感兴趣的2%20条评论%20
默认排序%20时间排序
netstu · 16 小时前
我觉得这是在瞎整,用zephir来编写C扩展已经非常方便了,可以避免很多问题,本来php就4不像的,这样搞只能把php搞的臃肿而且八不像的
已赞。
Zephir也好,PHP-X也好,都少不了一个编译过程,而FFI不用编译,改完脚本就能刷新执行,这就是一个快速迭代和快速实验的优势,就像这篇文章的一样玩玩各种C类库是非常方便的。不过,因为性能原因,我也不会在生产环境用FFI。
而且FFI只是个扩展,技术上和其他PHP扩展没本质区别,只是有PHP官方维护而已,对PHP核心根本没影响,谈不上让PHP更臃肿,不需要的大可不用。
独孤九贱(5)_ThinkPHP5视频教程
ThinkPHP是国内最流行的中文PHP开发框架,也是您Web项目的最佳选择。《VeVb.com独孤九贱(5)-ThinkPHP5视频教程》课程以ThinkPHP5最新版本为例,从最基本的框架常识开始,将...
江湖传言:PHP是世界上最好的编程语言。真的是这样吗?这个梗究竟是从哪来的?学会本课程,你就会明白了。PHP 出品的PHP入门系统教学视频,完全从初学者的角度出发,绝不玩虚的,一切以实用、有用...
《VeVb.com原创html5视频教程》课程特色:php 原创幽默段子系列课程,以恶搞,段子为主题风格的php视频教程!轻松的教学风格,简短的教学模式,让同学们在不知不觉中,学会了HTML知识。...
本套教程,以一个真实的学校教学管理系统为案例,手把手教会您如何在一张白纸上,从零开始,一步一步的用ThinkPHP5框架快速开发出一个商业项目。
所有计算机语言的学习都要从基础开始,《PHP入门视频教程之一周学会PHP》不仅是PHP的基础部分更主要的是PHP语言的核心技术,是学习PHP必须掌握的内容,任何PHP项目的实现都离不开这部分的内容,通...
PHP教程郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。
新闻热点
疑难解答