PHP 7.4 前瞻:FFI

Oraoto發表於2019-03-03

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: <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裡使用。下面這個例子實現了一個跟隨滑鼠的圓:

file

<?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裡給出的一個測試結果:

    file

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

  2. 功能
    目前(20190301)FFI擴充套件還沒實現的一些功能:
    1. 返回struct/union和陣列
    2. 巢狀的struct(我寫了個簡單的補丁)
      使用這些功能的時候,會丟擲異常,提示功能未實現,所以只用等等或者馬上貢獻程式碼就好:)

參考

相關文章