传统的方法,当咱们需求用一些已有的C言语的库的才能的时候,咱们需求用C言语写wrapper,把他们包装成扩展,这个过程中就需求我们去学习PHP的扩展怎么写,当然现在也有一些方便的方法,比如Zephir.但总还是有一些学习本钱的,而有了FFI今后,咱们就能够直接在PHP脚本中调用C言语写的库中的函数了。
而C言语几十年的历史中,积累了大量的优秀的库,FFI直接让咱们能够方便的享受这个巨大的资源了。言归正传,今日我用一个比如来介绍,咱们如何运用PHP来调用libcurl,来抓取一个网页的内容,为什么要用libcurl呢?PHP不是已经有了curl扩展了么?嗯,首要由于libcurl的api我比较熟,其次
呢,正是由于有了,才好比照,传统扩展方法和FFI方法直接的易用性不是?
首要,比如咱们就拿当时你看的这篇文章为例,我现在需求写一段代码来抓取它的内容,假如用传统的PHP的curl扩展,咱们大概会这么写:
$url="https://www.nxmrx.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呢?
首要咱们下载PHP-FFI,编译安装,PHP-FFI需求PHP-7.4以及libffi-3以上。
然后,咱们需求告知PHPFFI咱们要调用的函数原型是咋样的,这个咱们能够运用FFI::cdef,它的原型是:
FFI::cdef([string$cdef=""[,string$lib=null]]):FFI
具体到这个比如,咱们写一个curl.php,包含一切要声明的东西,代码如下:
$libcurl=FFI::cdef(<<
intcurl_easy_setopt(void*curl,intoption,...);
intcurl_easy_perform(void*curl);
voidcurl_easy_cleanup(void*handle);
CTYPE
,"libcurl.so"
);
在string$cdef中,咱们能够写C言语函数式声明,FFI会parse它,了解到咱们要在string$lib这个库中调用的函数的签名是啥样的,在这个比如中,咱们用到三个libcurl的函数,它们的声明咱们都能够在libcurl的文档里找到,比如关于curl_easy_init.
这里有个当地是,文档中写的是回来值是CURL,但事实上由于咱们的比如中不会引证它,只是传递,那就避免麻烦就用void替代。
但是还有个麻烦的工作是,PHP预界说好了CURLOPT_等option的值,但现在咱们需求自己界说,简单的方法便是检查curl的头文件,找到对应的值,然后咱们把值给加进去:
constCURLOPT_URL=10002;
constCURLOPT_SSL_VERIFYPEER=64;
$libcurl=FFI::cdef(<<
intcurl_easy_setopt(void*curl,intoption,...);
intcurl_easy_perform(void*curl);
voidcurl_easy_cleanup(void*handle);
CTYPE
,"libcurl.so"
);
好了,界说部分就算完结了,现在咱们完结实际逻辑部分,整个下来的代码会是:
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函数作为回调函数经过FFI传递给libcurl,那咱们会有俩种方法来做:
1.选用WRITEDATA,默许的libcurl会调用fwrite作为回调函数,而咱们能够经过WRITEDATA给libcurl一个fd,让它不要写入stdout,而是写入到这个fd2.咱们自己编写一个C到简单函数,经过FFI引
入进来,传递给libcurl.
咱们先用第一种方法,首要咱们需求运用fopen,这次咱们经过界说个C的头文件来声明原型(file.h):
voidfopen(charfilename,char*mode);
voidfclose(void*fp);
像file.h相同,咱们把一切的libcurl的函数声明也放到curl.h中去
#defineFFI_LIB"libcurl.so"
void*curl_easy_init();
intcurl_easy_setopt(void*curl,intoption,...);
intcurl_easy_perform(void*curl);
voidcurl_easy_cleanup(CURL*handle);
留意,咱们经过界说了一个FFI_LIB的宏,来告知FFI这些函数来自libcurl.so,当咱们用FFI::load加载这个h文件的时候,PHPFFI就会主动载入libcurl.so,好,现在整个代码会是:
constCURLOPT_URL=10002;
constCURLOPT_SSL_VERIFYPEER=64;
constCURLOPT_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
#include
#include"write.h"
size_town_writefunc(voidptr,size_tsize,size_tnmember,voiddata){
own_write_datad=(own_write_data)data;
size_ttotal=size*nmember;
if(d->buf==NULL){
d->buf=malloc(total);
if(d->buf==NULL){
return0;
}
d->size=total;
memcpy(d->buf,ptr,total);
}else{
d->buf=realloc(d->buf,d->size+total);
if(d->buf==NULL){
return0;
}
memcpy(d->buf+d->size,ptr,total);
d->size+=total;
}
returntotal;
}
void*init(){
return&own_writefunc;
}
留意此处的init函数,由于在PHPFFI中,就现在的版别(2020-03-11)咱们没有方法直接获得一个函数指针,所以咱们界说了这个函数,回来own_writefunc的地址。
最后咱们界说上面用到的头文件write.h:
#defineFFI_LIB"write.so"
typedefstruct_writedata{
void*buf;
size_tsize;
}own_write_data;
void*init();
留意到咱们在头文件中也界说了FFI_LIB,这样这个头文件就能够一起被write.c和接下来咱们的PHPFFI共同运用了。
然后咱们编译write函数为一个动态库:
gcc-O2-fPIC-shared-gwrite.c-owrite.so
好了,现在整个的代码会变成:
constCURLOPT_URL=10002;
constCURLOPT_SSL_VERIFYPEER=64;
constCURLOPT_WRITEDATA=10001;
constCURLOPT_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);
好了,跑一下吧?
但是究竟直接在PHP中引证外部的so,还是会有很大的安全问题的,另外你也具有了1000中方法让PHPcrash,安全起见咱们能够选用preload的方法,这种形式下,咱们不能在脚本中直接调用
FFI::cdef,FF::load,只能在经过opcache.preload:
ffi.enable=preload
opcache.preload=ffi_preload.inc
ffi_preload.inc:
FFI::load("curl.h");
FFI::load("write.h");
但咱们引证载入的FFI呢?为此咱们需求修正一下这俩个.h头文件,参加FFI_SCOPE,比如curl.h:
#defineFFI_LIB"libcurl.so"
#defineFFI_SCOPE"libcurl"
void*curl_easy_init();
intcurl_easy_setopt(void*curl,intoption,...);
intcurl_easy_perform(void*curl);
voidcurl_easy_cleanup(void*handle);
对应的咱们给write.h也参加FFI_SCOPE为"write",然后咱们的脚本现在看起来应该是这样:
constCURLOPT_URL=10002;
constCURLOPT_SSL_VERIFYPEER=64;
constCURLOPT_WRITEDATA=10001;
constCURLOPT_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,引证对应的函数。
好了,经过这个比如,我们应该对FFI有了一个比较深化的理解了,有兴趣,就去找一个C库,试试吧?