欧美亚洲中文,在线国自产视频,欧洲一区在线观看视频,亚洲综合中文字幕在线观看

      1. <dfn id="rfwes"></dfn>
          <object id="rfwes"></object>
        1. 站長資訊網(wǎng)
          最全最豐富的資訊網(wǎng)站

          PHP7.4 全新擴(kuò)展方式 FFI 詳解

          隨著PHP7.4而來的有一個我認(rèn)為非常有用的一個擴(kuò)展: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.

          是的,F(xiàn)FI提供了高級語言直接的互相調(diào)用,而對于PHP而言,F(xiàn)FI讓我們可以方便的調(diào)用C語言寫的各種庫。

          其實(shí)現(xiàn)有大量的PHP擴(kuò)展是對一些已有的C庫的包裝,某些常用的mysqli,curl,gettext等,PECL中也有大量的類似擴(kuò)展。

          傳統(tǒng)的方式,當(dāng)我們需要用一些已有的C語言的庫的能力的時候,我們需要用C語言寫包裝器,把他們包裝成擴(kuò)展,這個過程中就需要大家去學(xué)習(xí)PHP的擴(kuò)展怎么寫,當(dāng)然現(xiàn)在也有一些方便的方式,某種Zephir。但總還是有一些學(xué)習(xí)成本的,而有了FFI之后,我們就可以直接在PHP腳本中調(diào)用C語言寫的庫中的函數(shù)了。

          而C語言幾十年的歷史中,積累積累的優(yōu)秀的庫,F(xiàn)FI直接讓我們可以方便的享受這個龐大的資源了。

          言歸正傳,今天我用一個例子來介紹,我們?nèi)绾问褂肞HP來調(diào)用libcurl,來抓取一個網(wǎng)頁的內(nèi)容,為什么要用libcurl呢?PHP不是已經(jīng)有了curl擴(kuò)展了么?嗯,首先因?yàn)閘ibcurl的api我比較熟,其次呢,正是因?yàn)橛辛?,才好對比,傳統(tǒng)擴(kuò)展方式AS和FFI方式直接的易用性不是?

          首先,某些我們就拿當(dāng)前你看的這篇文章為例,我現(xiàn)在需要寫一段代碼來抓取它的內(nèi)容,如果用傳統(tǒng)的PHP的curl擴(kuò)展,我們大概會這么寫:

          <?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);

          (因?yàn)槲业木W(wǎng)站是https的,所以會多一個設(shè)置SSL_VERIFYPEER的操作)那如果是用FFI呢?

          首先要啟用PHP7.4的ext / ffi,需要注意的是PHP-FFI要求libffi-3以上。

          然后,我們需要告訴PHP FFI我們要調(diào)用的函數(shù)原型是咋樣的,這個我們可以使用FFI :: cdef,它的原型是:

          FFI::cdef([string $cdef = "" [, string $lib = null]]): FFI

          在字符串$cdef中,我們可以寫C語言函數(shù)式申明,F(xiàn)FI會parse它,了解到我們要在字符串$lib這個庫中調(diào)用的函數(shù)的簽名是啥樣的,在這個例子中,我們用到三一個libcurl的函數(shù),它們的申明我們都可以在libcurl的文檔里找到,某些關(guān)于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 *,但事實(shí)上因?yàn)槲覀兊氖纠胁粫庖盟?,只是傳遞,那就避免麻煩就用void *代替。

          然而還有個麻煩的事情是,PHP預(yù)定義好了:

          <?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"  );

          好了,定義部分就算完成了,現(xiàn)在我們完成實(shí)際邏輯部分,整個下來的代碼會是:

          <?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擴(kuò)展的方式,是不是一樣簡練呢?

          接下來,我們稍微弄的復(fù)雜一點(diǎn),也直到,如果我們不想要結(jié)果直接輸出,而是返回成一個字符串呢,對于PHP的curl擴(kuò)展來說,我們只需要調(diào)用curl_setopCURLOPT_RETURNTRANSFER為1,但在libcurl中其實(shí)并沒有直接返回字符串的能力,或者提供了一個WRITEFUNCTION的替代函數(shù),在有數(shù)據(jù)返回的時候,libcurl會調(diào)用這個函數(shù),實(shí)際上PHP curl擴(kuò)展也是這樣做的。

          目前我們并不能直接把一個PHP函數(shù)作為附加函數(shù)通過FFI傳遞給libcurl,那我們都有倆種方式來做:

          1.采用WRITEDATA,默認(rèn)的libcurl會調(diào)用fwrite作為一個變量函數(shù),而我們可以通過WRITEDATA給libcurl一個fd,讓它不要寫入stdout,而是寫入到這個fd

          2.我們自己編寫一個C到簡單函數(shù),通過FFI日期進(jìn)來,傳遞給libcurl。

          我們先用第一種方式,首先我們需要使用fopen,這次我們通過定義一個C的頭文件來申明原型(file.h):

          void *fopen(char *filename, char *mode); void fclose(void * fp);

          file.h一樣,我們把所有的libcurl的函數(shù)申明也放到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加載那個對應(yīng)的庫呢?如上面,我們通過定義了一個FFI_LIB的宏,來告訴FFI這些函數(shù)來自libcurl.so,當(dāng)我們用FFI :: load加載這個h文件的時候,PHP FFI就會自動加載libcurl.so

          那為什么fopen不需要指定加載庫呢,那是因?yàn)镕FI也會在變量符號表中查找符號,而fopen是一個標(biāo)準(zhǔn)庫函數(shù),它早就存在了。

          好,現(xiàn)在整個代碼會是:

          <?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);

          但這種方式呢就是需要一個臨時的中轉(zhuǎn)文件,還是不夠優(yōu)雅,現(xiàn)在我們用第二種方式,要用第二種方式,我們需要自己用C寫一個替代函數(shù)傳遞給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; }

          注意此處的初始函數(shù),因?yàn)樵赑HP FFI中,就目前的版本(2020-03-11)我們沒有辦法直接獲得一個函數(shù)指針,所以我們定義了這個函數(shù),返回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函數(shù)為一個動態(tài)庫:

          gcc -O2 -fPIC -shared  -g  write.c -o write.so

          好了,現(xiàn)在整個的代碼會變成:

          <?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)來分配了一個結(jié)構(gòu)_write_data的內(nèi)存:

          function FFI::new(mixed $type [, bool $own = true [, bool $persistent = false]]): FFICData

          $own表示這個內(nèi)存管理是否采用PHP的內(nèi)存管理,有時的情況下,我們申請的內(nèi)存會經(jīng)過PHP的生命周期管理,不需要主動釋放,但是有的時候你也可能希望自己管理,那么可以設(shè)置$ownflase,那么在適當(dāng)?shù)臅r候,你需要調(diào)用FFI :: free去主動釋放。

          然后我們把$data作為WRITEDATA傳遞給libcurl,這里我們使用了FFI :: addr來獲取$data的實(shí)際內(nèi)存地址:

          static function addr(FFICData $cdata): FFICData;

          然后我們把own_write_func作為WRITEFUNCTION傳遞給了libcurl,這樣再有返回的時候,libcurl就會調(diào)用我們的own_write_func來處理返回,同時會把write_data作為自定義參數(shù)傳遞給我們的替代函數(shù)。

          最后我們使用了FFI :: string來把一段內(nèi)存轉(zhuǎn)換成PHP的string

          static function FFI::string(FFICData $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);

          對應(yīng)的我們給write.h也加入FFI_SCOPE為“ write”,然后我們的腳本現(xiàn)在看起來應(yīng)該是這樣的:

          <?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);

          也就是,我們現(xiàn)在使用FFI :: scope來代替FFI :: load,引用對應(yīng)的函數(shù)。

          static function scope(string $name): FFI;

          然后還有另外一個問題,F(xiàn)FI雖然給了我們很大的規(guī)模,但是畢竟直接調(diào)用C庫函數(shù),還是非常具有風(fēng)險性的,我們應(yīng)該只允許用戶調(diào)用我們確認(rèn)過的函數(shù),于是,ffi.enable = preload就該上場了,當(dāng)我們設(shè)置ffi.enable = preload的話,那就只有在opcache.preload的腳本中的函數(shù)才能調(diào)用FFI,而用戶寫的函數(shù)是沒有辦法直接調(diào)用的。

          我們稍微修改下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) : FFICData {      return $write->new("own_write_data"); }   function get_write() : FFI {      return FFI::scope("write"); }   function get_data_addr($data) : FFICData {      return FFI::addr($data); }   function paser_libcurl_ret($data) :string{      return FFI::string($data->buf, $data->size); }

          也就是,我們把所有會調(diào)用FFI API的函數(shù)都定義在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腳本調(diào)用,用戶不能直接調(diào)用。從而我們可以在這些函數(shù)內(nèi)部做好適當(dāng)?shù)陌踩WC工作,從而保證一定的安全性。

          好了,經(jīng)歷了這個例子,大家應(yīng)該對FFI有一個比較深入的理解了,詳細(xì)的PHP API說明,大家可以參考:PHP-FFI Manual,有興趣的話,就去找一個C庫,試試吧?

          本文的例子,你可以在我的github上下載到:FFI example

          最后還是多說一句,例子只是為了演示功能,所以省掉了很多錯誤分支的判斷捕獲,大家自己寫的時候還是要加入。畢竟使用FFI的話,會讓你會有1000種方式讓PHP segfault crash,所以be careful

          推薦PHP教程《PHP7》

          贊(0)
          分享到: 更多 (0)
          網(wǎng)站地圖   滬ICP備18035694號-2    滬公網(wǎng)安備31011702889846號