传统的方法,当咱们需求用一些已有的 C 言语的库的才能的时候,咱们需求用 C 言语写 wrapper,把他们包装成扩展,这个过程中就需求我们去学习 PHP 的扩展怎么写,当然现在也有一些方便的方法,比如 Zephir. 但总还是有一些学习本钱的,而有了 FFI 今后,咱们就能够直接在 PHP 脚本中调用 C 言语写的库中的函数了。
而 C 言语几十年的历史中,积累了大量的优秀的库,FFI 直接让咱们能够方便的享受这个巨大的资源了。言归正传,今日我用一个比如来介绍,咱们如何运用 PHP 来调用 libcurl,来抓取一个网页的内容,为什么要用 libcurl 呢?PHP 不是已经有了 curl 扩展了么?嗯,首要由于 libcurl 的 api 我比较熟,其次
呢,正是由于有了,才好比照,传统扩展方法和 FFI 方法直接的易用性不是?
首要,比如咱们就拿当时你看的这篇文章为例,我现在需求写一段代码来抓取它的内容,假如用传统的 PHP 的 curl 扩展,咱们大概会这么写:
<?php
$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(<<<CTYPE
void*curl_easy_init();
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 的头文件,找到对应的值,然后咱们把值给加进去:
<?php
constCURLOPT_URL=10002;
constCURLOPT_SSL_VERIFYPEER=64;
$libcurl=FFI::cdef(<<<CTYPE
void*curl_easy_init();
intcurl_easy_setopt(void*curl,intoption,…);
intcurl_easy_perform(void*curl);
voidcurl_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 函数作为回调函数经过 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, 好,现在整个代码会是:
<?php
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<stdlib.h>
#include<string.h>
#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
好了,现在整个的代码会变成:
<?php
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:
<?php
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”, 然后咱们的脚本现在看起来应该是这样:
<?php
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 库,试试吧?