PHP FFI(Foreign Function interface)
,援用一段PHP FFI RFC中的一段形容:For PHP, FFI opens a way to write PHP extensions and bindings to C libraries in pure PHP.
是的,FFI提供了初级言语间接的相互挪用,而关于PHP而言,FFI让咱们能够不便的挪用C言语写的各类库。
其完成有年夜量的PHP扩大是对一些已有的C库的包装,某些罕用的mysqli
,curl,gettext
等,PECL中也有年夜量的相似扩大。
传统的形式,当咱们需求用一些已有的C言语的库的才能的时分,咱们需求用C言语写包装器,把他们包装成扩大,这个进程中就需求各人去学习PHP的扩大怎样写,当然如今也有一些不便的形式,某种Zephir
。但总仍是有一些学习老本的,而有了FFI之后,咱们就能够间接正在PHP剧本中挪用C言语写的库中的函数了。
而C言语几十年的汗青中,积攒积攒的优秀的库,FFI间接让咱们能够不便的享用这个宏大的资本了。
言归正传,明天我用一个例子来引见,咱们若何应用PHP来挪用libcurl,来抓取一个网页的内容,为何要用libcurl呢?PHP没有是曾经有了curl扩大了么?嗯,起首由于libcurl的api我比拟熟,其次呢,恰是由于有了,才好比照,传统扩大形式AS以及FFI形式间接的易用性没有是?
起首,某些咱们就拿以后你看的这篇文章为例,我如今需求写一段代码来抓取它的内容,假如用传统的PHP的curl扩大,咱们大略会这么写:
<?php $url = "https://www.laruence.com/2020/03/11/5475.html"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); curl_exec($ch); curl_close($ch);
(由于我的网站是https的,以是会多一个设置SSL_VERIFYPEER
的操作)那假如是用FFI呢?
起首要启用PHP7.4的ext / ffi,需求留意的是PHP-FFI要求libffi-3以上。
而后,咱们需求通知PHP FFI咱们要挪用的函数原型是咋样的,这个咱们能够应用FFI :: cdef
,它的原型是:
FFI::cdef([string $cdef = "" [, string $lib = null]]): FFI
正在字符串$cdef
中,咱们能够写C言语函数式声明,FFI会parse
它,理解到咱们要正在字符串$lib
这个库中挪用的函数的署名是啥样的,正在这个例子中,咱们用到三一个libcurl的函数,它们的声明咱们均可以正在libcurl的文档里找到,某些对于curl_easy_init
。
详细到这个例子,咱们写一个curl.php
,蕴含一切要声明的货色,代码以下:
$libcurl = FFI::cdef(<<<CTYPE void *curl_easy_init(); int curl_easy_setopt(void *curl, int option, ...); int curl_easy_perform(void *curl); void curl_easy_cleanup(void *handle); CTYPE , "libcurl.so" );
这里有个中央是,文档中写的是前往值是CURL *
,但现实上由于咱们的示例中没有会解援用它,只是通报,那就防止费事就用void *
替代。
但是另有个费事的事件是,PHP预约义好了:
<?php const CURLOPT_URL = 10002; const CURLOPT_SSL_VERIFYPEER = 64; $libcurl = FFI::cdef(<<<CTYPE void *curl_easy_init(); int curl_easy_setopt(void *curl, int option, ...); int curl_easy_perform(void *curl); void curl_easy_cleanup(void *handle); CTYPE , "libcurl.so" );
好了,界说局部就算实现了,如今咱们实现实际逻辑局部,整个上去的代码会是:
<?php require "curl.php"; $url = "https://www.laruence.com/2020/03/11/5475.html"; $ch = $libcurl->curl_easy_init(); $libcurl->curl_easy_setopt($ch, CURLOPT_URL, $url); $libcurl->curl_easy_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); $libcurl->curl_easy_perform($ch); $libcurl->curl_easy_cleanup($ch);
怎样样,比例应用curl扩大的形式,是否是同样精练呢?
接上去,咱们略微弄的复杂一点,也直到,假如咱们没有想要后果间接输入,而是前往成一个字符串呢,关于PHP的curl扩大来讲,咱们只要要挪用curl_setop
把CURLOPT_RETURNTRANSFER
为1,但正在libcurl中其实并无间接前往字符串的才能,或许提供了一个WRITEFUNCTION
的代替函数,正在无数据前往的时分,libcurl会挪用这个函数,实际上PHP curl扩大也是这样做的。
今朝咱们其实不能间接把一个PHP函数作为附加函数经过FFI通报给libcurl,那咱们都有俩种形式来做:
1.采纳WRITEDATA
,默许的libcurl会挪用fwrite
作为一个变量函数,而咱们能够经过WRITEDATA
给libcurl一个fd,让它没有要写入stdout
,而是写入到这个fd
2.咱们本人编写一个C到简略函数,经过FFI日期出去,通报给libcurl。
咱们先用第一种形式,起首咱们需求应用fopen
,此次咱们经过界说一个C的头文件来声明原型(file.h
):
void *fopen(char *filename, char *mode); void fclose(void * fp);
像file.h
同样,咱们把一切的libcurl的函数声明也放到curl.h
中去
#define FFI_LIB "libcurl.so" void *curl_easy_init(); int curl_easy_setopt(void *curl, int option, ...); int curl_easy_perform(void *curl); void curl_easy_cleanup(CURL *handle);
而后咱们就能够应用FFI :: load
来加载.h文件:
static function load(string $filename): FFI;
然而怎样通知FFI加载阿谁对应的库呢?如下面,咱们经过界说了一个FFI_LIB
的宏,来通知FFI这些函数来自libcurl.so
,当咱们用FFI :: load
加载这个h文件的时分,PHP FFI就会主动加载libcurl.so
那为何fopen
没有需求指定加载库呢,那是由于FFI也会正在变量符号表中查找符号,而fopen
是一个规范库函数,它早就存正在了。
好,如今整个代码会是:
<?php const CURLOPT_URL = 10002; const CURLOPT_SSL_VERIFYPEER = 64; const CURLOPT_WRITEDATA = 10001; $libc = FFI::load("file.h"); $libcurl = FFI::load("curl.h"); $url = "https://www.laruence.com/2020/03/11/5475.html"; $tmpfile = "/tmp/tmpfile.out"; $ch = $libcurl->curl_easy_init(); $fp = $libc->fopen($tmpfile, "a"); $libcurl->curl_easy_setopt($ch, CURLOPT_URL, $url); $libcurl->curl_easy_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); $libcurl->curl_easy_setopt($ch, CURLOPT_WRITEDATA, $fp); $libcurl->curl_easy_perform($ch); $libcurl->curl_easy_cleanup($ch); $libc->fclose($fp); $ret = file_get_contents($tmpfile); @unlink($tmpfile);
但这类形式呢就是需求一个暂时的直达文件,仍是不敷优雅,如今咱们用第二种形式,要用第二种形式,咱们需求本人用C写一个代替函数通报给libcurl:
#include <stdlib.h> #include <string.h> #include "write.h" size_t own_writefunc(void *ptr, size_t size, size_t nmember, void *data) { own_write_data *d = (own_write_data*)data; size_t total = size * nmember; if (d->buf == NULL) { d->buf = malloc(total); if (d->buf == NULL) { return 0; } d->size = total; memcpy(d->buf, ptr, total); } else { d->buf = realloc(d->buf, d->size + total); if (d->buf == NULL) { return 0; } memcpy(d->buf + d->size, ptr, total); d->size += total; } return total; } void * init() { return &own_writefunc; }
留意此处的初始函数,由于正在PHP FFI中,就今朝的版本(2020-03-11)咱们不方法间接取得一个函数指针,以是咱们界说了这个函数,前往own_writefunc
的地点。
最初咱们界说下面用到的头文件write.h
:
#define FFI_LIB "write.so" typedef struct _writedata { void *buf; size_t size; } own_write_data; void *init();
留意到咱们正在头文件中也界说了FFI_LIB
,这样这个头文件就能够同时被write.c
以及接上去咱们的PHP FFI
独特应用了。
而后咱们编译write
函数为一个静态库:
gcc -O2 -fPIC -shared -g write.c -o write.so
好了,如今整个的代码会变为:
<?php const CURLOPT_URL = 10002; const CURLOPT_SSL_VERIFYPEER = 64; const CURLOPT_WRITEDATA = 10001; const CURLOPT_WRITEFUNCTION = 20011; $libcurl = FFI::load("curl.h"); $write = FFI::load("write.h"); $url = "https://www.laruence.com/2020/03/11/5475.html"; $data = $write->new("own_write_data"); $ch = $libcurl->curl_easy_init(); $libcurl->curl_easy_setopt($ch, CURLOPT_URL, $url); $libcurl->curl_easy_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); $libcurl->curl_easy_setopt($ch, CURLOPT_WRITEDATA, FFI::addr($data)); $libcurl->curl_easy_setopt($ch, CURLOPT_WRITEFUNCTION, $write->init()); $libcurl->curl_easy_perform($ch); $libcurl->curl_easy_cleanup($ch); ret = FFI::string($data->buf, $data->size);
此处,咱们应用FFI :: new($ write-> new)
来调配了一个构造_write_data
的内存:
function FFI::new(mixed $type [, bool $own = true [, bool $persistent = false]]): FFI\CData
$own
示意这个内存治理能否采纳PHP的内存治理,有时的状况下,咱们请求的内存会通过PHP的生命周期治理,没有需求自动开释,然而有的时分你也可能心愿本人治理,那末能够设置$own
为flase
,那末正在适当的时分,你需求挪用FFI :: free
去自动开释。
而后咱们把$data
作为WRITEDATA
通报给libcurl,这里咱们应用了FFI :: addr
来猎取$data
的实际内存地点:
static function addr(FFI\CData $cdata): FFI\CData;
而后咱们把own_write_func
作为WRITEFUNCTION
通报给了libcurl,这样再有前往的时分,libcurl就会挪用咱们的own_write_func
来解决前往,同时会把write_data
作为自界说参数通报给咱们的代替函数。
最初咱们应用了FFI :: string
来把一段内存转换成PHP的string
:
static function FFI::string(FFI\CData $src [, int $size]): string
好了,跑一下吧?
但是究竟结果间接正在PHP中每一次申请都加载so的话,会是一个很年夜的功能成绩,以是咱们也能够采纳preload
的形式,这类模式下,咱们经过opcache.preload
来正在PHP启动的时分就加载好:
ffi.enable=1 opcache.preload=ffi_preload.inc
ffi_preload.inc:
<?php FFI::load("curl.h"); FFI::load("write.h");
但咱们援用加载的FFI呢?因而咱们需求修正一下这俩个.h头文件,退出FFI_SCOPE
,比方curl.h
:
#define FFI_LIB "libcurl.so" #define FFI_SCOPE "libcurl" void *curl_easy_init(); int curl_easy_setopt(void *curl, int option, ...); int curl_easy_perform(void *curl); void curl_easy_cleanup(void *handle);
对应的咱们给write.h
也退出FFI_SCOPE
为“ write”,而后咱们的剧本如今看起来应该是这样的:
<?php const CURLOPT_URL = 10002; const CURLOPT_SSL_VERIFYPEER = 64; const CURLOPT_WRITEDATA = 10001; const CURLOPT_WRITEFUNCTION = 20011; $libcurl = FFI::scope("libcurl"); $write = FFI::scope("write"); $url = "https://www.laruence.com/2020/03/11/5475.html"; $data = $write->new("own_write_data"); $ch = $libcurl->curl_easy_init(); $libcurl->curl_easy_setopt($ch, CURLOPT_URL, $url); $libcurl->curl_easy_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); $libcurl->curl_easy_setopt($ch, CURLOPT_WRITEDATA, FFI::addr($data)); $libcurl->curl_easy_setopt($ch, CURLOPT_WRITEFUNCTION, $write->init()); $libcurl->curl_easy_perform($ch); $libcurl->curl_easy_cleanup($ch); ret = FFI::string($data->buf, $data->size);
也就是,咱们如今应用FFI :: scope
来替代FFI :: load
,援用对应的函数。
static function scope(string $name): FFI;
而后另有另一个成绩,FFI尽管给了咱们很年夜的规模,然而究竟结果间接挪用C库函数,仍是十分具备危险性的,咱们应该只容许用户挪用咱们确认过的函数,于是,ffi.enable = preload
就该上场了,当咱们设置ffi.enable = preload
的话,那就只有正在opcache.preload
的剧本中的函数能力挪用FFI,而用户写的函数是不方法间接挪用的。
咱们略微修正下ffi_preload.inc
变为ffi_safe_preload.inc
<?php class CURLOPT { const URL = 10002; const SSL_VERIFYHOST = 81; const SSL_VERIFYPEER = 64; const WRITEDATA = 10001; const WRITEFUNCTION = 20011; } FFI::load("curl.h"); FFI::load("write.h"); function get_libcurl() : FFI { return FFI::scope("libcurl"); } function get_write_data($write) : FFI\CData { return $write->new("own_write_data"); } function get_write() : FFI { return FFI::scope("write"); } function get_data_addr($data) : FFI\CData { return FFI::addr($data); } function paser_libcurl_ret($data) :string{ return FFI::string($data->buf, $data->size); }
也就是,咱们把一切会挪用FFI API的函数都界说正在preload
剧本中,而后咱们的示例会变为(ffi_safe.php
):
<?php $libcurl = get_libcurl(); $write = get_write(); $data = get_write_data($write); $url = "https://www.laruence.com/2020/03/11/5475.html"; $ch = $libcurl->curl_easy_init(); $libcurl->curl_easy_setopt($ch, CURLOPT::URL, $url); $libcurl->curl_easy_setopt($ch, CURLOPT::SSL_VERIFYPEER, 0); $libcurl->curl_easy_setopt($ch, CURLOPT::WRITEDATA, get_data_addr($data)); $libcurl->curl_easy_setopt($ch, CURLOPT::WRITEFUNCTION, $write->init()); $libcurl->curl_easy_perform($ch); $libcurl->curl_easy_cleanup($ch); $ret = paser_libcurl_ret($data);
这样一来经过ffi.enable = preload
,咱们就能够限度,一切的FFI API只能被咱们可管制的preload
剧本挪用,用户不克不及间接挪用。从而咱们能够正在这些函数外部做好适当的平安保障工作,从而保障肯定的平安性。
好了,经验了这个例子,各人应该对FFI有一个比拟深化的了解了,具体的PHP API阐明,各人能够参考:PHP-FFI Manual,有兴味的话,就去找一个C库,尝尝吧?
本文的例子,你能够正在我的github上下载到:FFI example
最初仍是多说一句,例子只是为了演示性能,以是免却了不少谬误分支的判别捕捉,各人本人写的时分仍是要退出。究竟结果应用FFI的话,会让你会有1000种形式让PHP segfault crash,以是be careful
保举PHP教程《PHP7》
以上就是PHP7.4 全新扩大形式 FFI 详解的具体内容,更多请存眷资源魔其它相干文章!
标签: php php7开发教程 php7开发资料 php7开发自学
抱歉,评论功能暂时关闭!