PHP7.4 全新扩展方式 FFI 详解-PHP7

资源魔 44 0
跟着PHP7.4而来的有一个我以为十分有用的一个扩大: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_setopCURLOPT_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的生命周期治理,没有需求自动开释,然而有的时分你也可能心愿本人治理,那末能够设置$ownflase,那末正在适当的时分,你需求挪用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开发自学

抱歉,评论功能暂时关闭!