PHP7.4中FFI的介绍(代码示例)
本篇文章給大家?guī)淼膬?nèi)容是關(guān)于PHP7.4中FFI的介紹(代碼示例),有一定的參考價(jià)值,有需要的朋友可以參考一下,希望對你有所幫助。
FFI擴(kuò)展已經(jīng)通過RFC,正式成為PHP 7.4核心擴(kuò)展。
什么是FFI
FFI(Foreign Function Interface),即外部函數(shù)接口,是指在一種語言里調(diào)用另一種語言代碼的技術(shù)。PHP的FFI擴(kuò)展就是一個(gè)讓你在PHP里調(diào)用C代碼的技術(shù)。
FFI的使用非常簡單,只用聲明和調(diào)用兩步就可以,對于有C語言經(jīng)驗(yàn),但是不了解Zend引擎的程序員來說,這簡直是打開了新世界的大門,可以快速地使用C類庫進(jìn)行原型試驗(yàn)。
(此處有圖:溜了溜了,要懂C的……)
下面通過3個(gè)例子,看一下FFI是怎樣使用的。
Libbloom
libbloom是一個(gè)C實(shí)現(xiàn)的bloom filter,比較知名的用戶有Shadowsocks-libev,下面看一下怎樣通過FFI在PHP里調(diào)用libbloom。
第一步,從頭文件bloom.h把主要的數(shù)據(jù)結(jié)構(gòu)和函數(shù)聲明復(fù)制出來:
$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");
登錄后復(fù)制
FFI目前不支持預(yù)處理器(除了FFI_LIB和FFI_SCOPE),所以宏定義要自己展開。
之后就可以通過$ffi創(chuàng)建已聲明的數(shù)據(jù)結(jié)構(gòu)和調(diào)用函數(shù):
// 創(chuàng)建一個(gè)bloom結(jié)構(gòu)體,然后用FFI::addr取地址
// libbloom的函數(shù)都是使用bloom結(jié)構(gòu)體的指針
$bloom = FFI::addr($ffi->new("struct bloom"));
// 調(diào)用libbloom的初始化函數(shù)
$ffi->bloom_init($bloom, 10000, 0.01);
// 添加數(shù)據(jù)
$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;
登錄后復(fù)制登錄后復(fù)制
Linux Namespace
Linux命名空間是容器技術(shù)的基石之一,通過FFI可以直接調(diào)用glibc的對應(yīng)系統(tǒng)調(diào)用封裝,從而通過PHP實(shí)現(xiàn)容器。下面是一個(gè)讓bash在一個(gè)新的命名空間里運(yùn)行的例子。
首先是一些常量,可以從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;
登錄后復(fù)制登錄后復(fù)制
接著時(shí)我們要用到的函數(shù)聲明:
$cdef="
// fork進(jìn)程
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
// 掛載文件系統(tǒng)
int mount(const char *source, const char *target, const char *filesystemtype,
unsigned long mountflags, const void *data);
// 設(shè)置gid
int setgid(int gid);
// 設(shè)置uid
int setuid(int uid);
// 設(shè)置hostname
int sethostname(char *name, unsigned int len);
";
$libc = FFI::cdef($cdef, "libc.so.6");
登錄后復(fù)制登錄后復(fù)制
定義我們的子進(jìn)程:
// 生成一個(gè)容器ID
$containerId = sha1(random_bytes(8));
// 定義子進(jìn)程
$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");
};
登錄后復(fù)制登錄后復(fù)制
在子進(jìn)程里,我們重新掛載了/proc,設(shè)置了uid、gid和hostname,然后啟動(dòng)/bin/sh。
父進(jìn)程通過clone函數(shù),創(chuàng)建子進(jìn)程:
// 分配子進(jìn)程的棧
$child_stack = FFI::new("char[1024 * 4]");
$child_stack = FFI::cast('void *', FFI::addr($child_stack)) - 1024 * 4;
// fork子進(jìn)程
$pid = $libc->clone($childfn, $child_stack, CLONE_NEWUSER
| CLONE_NEWNS
| CLONE_NEWPID
| CLONE_NEWUTS
| CLONE_NEWIPC
| CLONE_NEWNET
| CLONE_NEWCGROUP
| SIGCHLD, null);
// 設(shè)置UID、GID映射,把容器內(nèi)的root映射到當(dāng)前用戶
$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");
// 等待子進(jìn)程
pcntl_wait($pid);
登錄后復(fù)制登錄后復(fù)制
glibc的clone函數(shù)是clone系統(tǒng)調(diào)用的封裝,它需要一個(gè)函數(shù)指針作為子進(jìn)程/線程的執(zhí)行體,我們可以直接把PHP的閉包和匿名函數(shù)當(dāng)作函數(shù)指針使用。
運(yùn)行效果:
$ php container.php
sh-5.0# id # 在容器內(nèi)是root
uid=0(root) gid=0(root) groups=0(root),65534(nobody)
sh-5.0# ps aux # 獨(dú)立的PID進(jìn)程空間
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 # 獨(dú)立的網(wǎng)絡(luò)命名空間
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
登錄后復(fù)制登錄后復(fù)制
raylib
raylib是個(gè)特性豐富而且易用的游戲庫,經(jīng)過簡單的封裝就可以在PHP里使用。下面這個(gè)例子實(shí)現(xiàn)了一個(gè)跟隨鼠標(biāo)的圓:
<?php
include __DIR__ . "/../../RayLib.php";
// 初始化
RayLib::init(); // 初始化FFI和“常量”
RayLib::InitWindow(400, 300, "raylib example");
// 狀態(tài):球的位置
$ballPosition = RayLib::Vector2(-100.0, 100.0);
// 主循環(huán)
while (!RayLib::WindowShouldClose())
{
// 狀態(tài)更新
$ballPosition = RayLib::GetMousePosition(); // 獲取鼠標(biāo)位置
// 渲染
RayLib::BeginDrawing();
RayLib::ClearBackground(RayLib::$RAYWHITE); // 清除背景顏色
RayLib::DrawCircleV($ballPosition, 40, RayLib::$RED); // 畫個(gè)圈圈
RayLib::DrawFPS(10, 10); // 顯示FPS
RayLib::EndDrawing();
}
// 釋放
RayLib::CloseWindow();
登錄后復(fù)制登錄后復(fù)制
不足
- 性能
C類庫性能可能很高,但是FFI調(diào)用的消耗也非常大,通過FFI訪問數(shù)據(jù)要比PHP訪問對象和數(shù)組慢兩倍,所以用FFI不一定能提高性能,RFC里給出的一個(gè)測試結(jié)果:就算用了JIT,還是比不上不用JIT的PHP。
-
功能
目前(20190301)FFI擴(kuò)展還沒實(shí)現(xiàn)的一些功能:- 返回struct/union和數(shù)組
- 嵌套的struct(我寫了個(gè)簡單的補(bǔ)丁)
使用這些功能的時(shí)候,會(huì)拋出異常,提示功能未實(shí)現(xiàn),所以只用等等或者馬上貢獻(xiàn)代碼就好:)
參考
- PHP RFC: FFI - Foreign Function Interface:RFC文檔,有比較完整的API和設(shè)計(jì)目的
4.4k
發(fā)布于
Oraoto的日常
1 天前發(fā)布
PHP 7.4 前瞻:FFI
- php
- ffi
- c
212 次閱讀
·
讀完需要 19 分鐘
6
FFI擴(kuò)展已經(jīng)通過RFC,正式成為PHP 7.4核心擴(kuò)展。
什么是FFI
FFI(Foreign Function Interface),即外部函數(shù)接口,是指在一種語言里調(diào)用另一種語言代碼的技術(shù)。PHP的FFI擴(kuò)展就是一個(gè)讓你在PHP里調(diào)用C代碼的技術(shù)。
FFI的使用非常簡單,只用聲明和調(diào)用兩步就可以,對于有C語言經(jīng)驗(yàn),但是不了解Zend引擎的程序員來說,這簡直是打開了新世界的大門,可以快速地使用C類庫進(jìn)行原型試驗(yàn)。
(此處有圖:溜了溜了,要懂C的……)
下面通過3個(gè)例子,看一下FFI是怎樣使用的。
Libbloom
libbloom是一個(gè)C實(shí)現(xiàn)的bloom filter,比較知名的用戶有Shadowsocks-libev,下面看一下怎樣通過FFI在PHP里調(diào)用libbloom。
第一步,從頭文件bloom.h把主要的數(shù)據(jù)結(jié)構(gòu)和函數(shù)聲明復(fù)制出來:
$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");
登錄后復(fù)制
FFI目前不支持預(yù)處理器(除了FFI_LIB和FFI_SCOPE),所以宏定義要自己展開。
之后就可以通過$ffi創(chuàng)建已聲明的數(shù)據(jù)結(jié)構(gòu)和調(diào)用函數(shù):
// 創(chuàng)建一個(gè)bloom結(jié)構(gòu)體,然后用FFI::addr取地址
// libbloom的函數(shù)都是使用bloom結(jié)構(gòu)體的指針
$bloom = FFI::addr($ffi->new("struct bloom"));
// 調(diào)用libbloom的初始化函數(shù)
$ffi->bloom_init($bloom, 10000, 0.01);
// 添加數(shù)據(jù)
$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;
登錄后復(fù)制登錄后復(fù)制
Linux Namespace
Linux命名空間是容器技術(shù)的基石之一,通過FFI可以直接調(diào)用glibc的對應(yīng)系統(tǒng)調(diào)用封裝,從而通過PHP實(shí)現(xiàn)容器。下面是一個(gè)讓bash在一個(gè)新的命名空間里運(yùn)行的例子。
首先是一些常量,可以從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;
登錄后復(fù)制登錄后復(fù)制
接著時(shí)我們要用到的函數(shù)聲明:
$cdef="
// fork進(jìn)程
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
// 掛載文件系統(tǒng)
int mount(const char *source, const char *target, const char *filesystemtype,
unsigned long mountflags, const void *data);
// 設(shè)置gid
int setgid(int gid);
// 設(shè)置uid
int setuid(int uid);
// 設(shè)置hostname
int sethostname(char *name, unsigned int len);
";
$libc = FFI::cdef($cdef, "libc.so.6");
登錄后復(fù)制登錄后復(fù)制
定義我們的子進(jìn)程:
// 生成一個(gè)容器ID
$containerId = sha1(random_bytes(8));
// 定義子進(jìn)程
$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");
};
登錄后復(fù)制登錄后復(fù)制
在子進(jìn)程里,我們重新掛載了/proc,設(shè)置了uid、gid和hostname,然后啟動(dòng)/bin/sh。
父進(jìn)程通過clone函數(shù),創(chuàng)建子進(jìn)程:
// 分配子進(jìn)程的棧
$child_stack = FFI::new("char[1024 * 4]");
$child_stack = FFI::cast('void *', FFI::addr($child_stack)) - 1024 * 4;
// fork子進(jìn)程
$pid = $libc->clone($childfn, $child_stack, CLONE_NEWUSER
| CLONE_NEWNS
| CLONE_NEWPID
| CLONE_NEWUTS
| CLONE_NEWIPC
| CLONE_NEWNET
| CLONE_NEWCGROUP
| SIGCHLD, null);
// 設(shè)置UID、GID映射,把容器內(nèi)的root映射到當(dāng)前用戶
$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");
// 等待子進(jìn)程
pcntl_wait($pid);
登錄后復(fù)制登錄后復(fù)制
glibc的clone函數(shù)是clone系統(tǒng)調(diào)用的封裝,它需要一個(gè)函數(shù)指針作為子進(jìn)程/線程的執(zhí)行體,我們可以直接把PHP的閉包和匿名函數(shù)當(dāng)作函數(shù)指針使用。
運(yùn)行效果:
$ php container.php
sh-5.0# id # 在容器內(nèi)是root
uid=0(root) gid=0(root) groups=0(root),65534(nobody)
sh-5.0# ps aux # 獨(dú)立的PID進(jìn)程空間
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 # 獨(dú)立的網(wǎng)絡(luò)命名空間
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
登錄后復(fù)制登錄后復(fù)制
raylib
raylib是個(gè)特性豐富而且易用的游戲庫,經(jīng)過簡單的封裝就可以在PHP里使用。下面這個(gè)例子實(shí)現(xiàn)了一個(gè)跟隨鼠標(biāo)的圓:
<?php
include __DIR__ . "/../../RayLib.php";
// 初始化
RayLib::init(); // 初始化FFI和“常量”
RayLib::InitWindow(400, 300, "raylib example");
// 狀態(tài):球的位置
$ballPosition = RayLib::Vector2(-100.0, 100.0);
// 主循環(huán)
while (!RayLib::WindowShouldClose())
{
// 狀態(tài)更新
$ballPosition = RayLib::GetMousePosition(); // 獲取鼠標(biāo)位置
// 渲染
RayLib::BeginDrawing();
RayLib::ClearBackground(RayLib::$RAYWHITE); // 清除背景顏色
RayLib::DrawCircleV($ballPosition, 40, RayLib::$RED); // 畫個(gè)圈圈
RayLib::DrawFPS(10, 10); // 顯示FPS
RayLib::EndDrawing();
}
// 釋放
RayLib::CloseWindow();
登錄后復(fù)制登錄后復(fù)制
不足
- 性能
C類庫性能可能很高,但是FFI調(diào)用的消耗也非常大,通過FFI訪問數(shù)據(jù)要比PHP訪問對象和數(shù)組慢兩倍,所以用FFI不一定能提高性能,RFC里給出的一個(gè)測試結(jié)果:就算用了JIT,還是比不上不用JIT的PHP。
-
功能
目前(20190301)FFI擴(kuò)展還沒實(shí)現(xiàn)的一些功能:- 返回struct/union和數(shù)組
- 嵌套的struct(我寫了個(gè)簡單的補(bǔ)丁)
使用這些功能的時(shí)候,會(huì)拋出異常,提示功能未實(shí)現(xiàn),所以只用等等或者馬上貢獻(xiàn)代碼就好:)
參考
- PHP RFC: FFI - Foreign Function Interface:RFC文檔,有比較完整的API和設(shè)計(jì)目的
你可能感興趣的
2 條評論
netstu
· 16 小時(shí)前
我覺得這是在瞎整,用zephir來編寫C擴(kuò)展已經(jīng)非常方便了,可以避免很多問題,本來php就4不像的,這樣搞只能把php搞的臃腫而且八不像的
贊
+2
回復(fù)
已贊。
Zephir也好,PHP-X也好,都少不了一個(gè)編譯過程,而FFI不用編譯,改完腳本就能刷新執(zhí)行,這就是一個(gè)快速迭代和快速實(shí)驗(yàn)的優(yōu)勢,就像這篇文章的一樣玩玩各種C類庫是非常方便的。不過,因?yàn)樾阅茉颍乙膊粫?huì)在生產(chǎn)環(huán)境用FFI。
而且FFI只是個(gè)擴(kuò)展,技術(shù)上和其他PHP擴(kuò)展沒本質(zhì)區(qū)別,只是有PHP官方維護(hù)而已,對PHP核心根本沒影響,談不上讓PHP更臃腫,不需要的大可不用。
Zephir也好,PHP-X也好,都少不了一個(gè)編譯過程,而FFI不用編譯,改完腳本就能刷新執(zhí)行,這就是一個(gè)快速迭代和快速實(shí)驗(yàn)的優(yōu)勢,就像這篇文章的一樣玩玩各種C類庫是非常方便的。不過,因?yàn)樾阅茉颍乙膊粫?huì)在生產(chǎn)環(huán)境用FFI。
而且FFI只是個(gè)擴(kuò)展,技術(shù)上和其他PHP擴(kuò)展沒本質(zhì)區(qū)別,只是有PHP官方維護(hù)而已,對PHP核心根本沒影響,談不上讓PHP更臃腫,不需要的大可不用。
—
oraoto
作者
· 15 小時(shí)前
以上就是PHP7.4中FFI的介紹(代碼示例)的詳細(xì)內(nèi)容,更多請關(guān)注風(fēng)君子博客其它相關(guān)文章!
總結(jié)
以上是生活随笔為你收集整理的PHP7.4中FFI的介绍(代码示例)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 笔记本玩lol要什么配置
- 下一篇: frd-al00是什么型号