• 技术文章 >后端开发 >php教程

    PHP7.4中FFI的介绍(代码示例)

    不言不言2019-03-04 14:44:17转载5751
    本篇文章给大家带来的内容是关于PHP7.4中FFI的介绍(代码示例),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。

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

    clipboard.png

    <?php
    
    include __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();

    不足

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

      clipboard.png

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

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

      1. 返回struct/union和数组
      2. 嵌套的struct(我写了个简单的补丁)

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

    参考







    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把主要的数据结构和函数声明复制出来:

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

    clipboard.png

    <?php
    
    include __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();

    不足

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

      clipboard.png

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

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

      1. 返回struct/union和数组
      2. 嵌套的struct(我写了个简单的补丁)

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

    参考


    你可能感兴趣的



    2 条评论






    netstu · 16 小时前


    我觉得这是在瞎整,用zephir来编写C扩展已经非常方便了,可以避免很多问题,本来php就4不像的,这样搞只能把php搞的臃肿而且八不像的

    +2 回复


    0



    已赞。

    Zephir也好,PHP-X也好,都少不了一个编译过程,而FFI不用编译,改完脚本就能刷新执行,这就是一个快速迭代和快速实验的优势,就像这篇文章的一样玩玩各种C类库是非常方便的。不过,因为性能原因,我也不会在生产环境用FFI。

    而且FFI只是个扩展,技术上和其他PHP扩展没本质区别,只是有PHP官方维护而已,对PHP核心根本没影响,谈不上让PHP更臃肿,不需要的大可不用。

    oraoto 作者 · 15 小时前

    添加回复

    载入中...

    显示更多评论


    以上就是PHP7.4中FFI的介绍(代码示例)的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:segmentfault,如有侵犯,请联系admin@php.cn删除
    专题推荐:php ffi c
    上一篇:php底层原理的垃圾回收机制的介绍(代码示例) 下一篇:Mac下搭建PHP 开发环境的教程(详细步骤)
    大前端线上培训班

    相关文章推荐

    • PHP如何使用phpinfo()获取PHP配置信息?(代码示例)• PHP实现一致性哈希算法的详细介绍(代码示例)• PHP如何反转数组中的键与值的位置?(代码示例)• FastCGI在PHP与Nginx之间的作用介绍

    全部评论我要评论

  • 取消发布评论发送
  • 1/1

    PHP中文网