资讯专栏INFORMATION COLUMN

PHP 7.4 前瞻:FFI

CompileYouth / 2680人阅读

摘要:扩展已经通过,正式成为的捆绑扩展库。第一步,从头文件把主要的数据结构和函数声明复制出来目前不支持预处理器除了和,所以宏定义要自己展开。

FFI扩展已经通过RFC,正式成为PHP 7.4的捆绑扩展库(Bundled Extensions)。

什么是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_LIBFFI_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的头文件得到:

// clone
const CLONE_NEWNS     = 0x00020000; // mount namespace
const CLONE_NEWCGROUP =    0x02000000; // cgroup namespace
const CLONE_NEWUTS    = 0x04000000; // utsname namespace
const CLONE_NEWIPC    = 0x08000000; // ipc namespace
const CLONE_NEWUSER   = 0x10000000; // user namespace
const CLONE_NEWPID    = 0x20000000; // pid namespace
const CLONE_NEWNET    = 0x40000000; // network namespace

// mount
const 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.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:  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里使用。下面这个例子实现了一个跟随鼠标的圆:


不足

性能
C类库性能可能很高,但是FFI调用的消耗也非常大,通过FFI访问数据要比PHP访问对象和数组慢两倍,所以用FFI不一定能提高性能,RFC里给出的一个测试结果:

就算用了JIT,还是比不上不用JIT的PHP。

功能
目前(20190301)FFI扩展还没实现的一些功能:

返回struct/union和数组

嵌套的struct(我写了个简单的补丁)

使用这些功能的时候,会抛出异常,提示功能未实现,所以只用等等或者马上贡献代码就好:)

参考

PHP RFC: FFI - Foreign Function Interface:RFC文档,有比较完整的API和设计目的

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/30912.html

相关文章

  • php7.4都要来了

    摘要:性能提升当然需要付出代价如果预加载文件的来源发生变化,则必须重新启动服务器。应该指出,这是一个复杂的主题。默认情况下不启用由于不再维护,核心团队决定使用删除其默认安装。将在错误情况下抛出异常。请注意,强制转换不受影响。 新特性 预加载预加载是PHP核心的一个惊人的补充,可以带来一些重大的性能改进。简而言之:如果您今天使用的是框架,则必须在每次请求时加载和重新编译其文件。 预加载允许服务...

    zhangrxiang 评论0 收藏0
  • PHP 7.4 新特性

    摘要:原文来源预计在年年末就会正式发布了,本文先来看看一下的新特性。预加载预加载的实现理论上是可以为带来很大的性能提升的。最后,你需要注意的向后不兼容特性,可以通过此链接来查看详细内容 原文来源:https://geixue.com/blogs/chan... PHP 7.4 预计在 2019 年年末就会正式发布了,本文先来看看一下 PHP 7.4 的新特性。 1.预加载预加载的实现理论上是...

    megatron 评论0 收藏0
  • PHP 7.4 新特性

    摘要:预加载在框架启动时在内存中加载文件,而且在后续请求中永久有效。缺点性能的提升会在其他方面花费很大的代价,每次预加载的文件发生改变时,框架需要重新启动。 PHP 7.4 计划在2019年11月21日发布,它主要新增了以下几个特性: 短闭包函数(short closure) 预加载提交性能 属性类型限定 Improved type variance(不会翻译) 三元运算简写 数组展开运...

    CastlePeaK 评论0 收藏0
  • NodeJS和NW通过ffi调用dll/so动态库

    摘要:指针和引用假设动态库中有函数如下第二个参数为结构体指针,第三个参数是一个引用。我这里选择的是然后找到,下载替换掉重编译和输入版本号,这里实用的是为或者参考资料通过在中调用动态链接库文件厚颜无耻加上自己的博客 0x01. 使用的 npm 包 首先要安装 node-gyp, 用来重新编译依赖包。 npm instal -g node-gyp 然后主要用到下面三个包: node-ffi -...

    lavnFan 评论0 收藏0

发表评论

0条评论

CompileYouth

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<