傳統的方法,當我們需求用一些已有的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庫,試試吧?php