- 本文地址: https://www.laruence.com/2020/03/11/5475.html
- 轉載請註明出處
隨着PHP7.4而來的有一個我認爲很是有用的一個擴展:PHP FFI(Foreign Function interface),引用一段PHP FFI RFC中的一段描述:php
對於PHP,FFI提供了一種在純PHP中編寫PHP擴展和對C庫的綁定的方法。
是的,FFI提供了高級語言直接的互相調用,而對於PHP而言,FFI讓咱們能夠方便的調用C語言寫的各類庫。html
其實現有大量的PHP擴展是對一些已有的C庫的包裝,某些經常使用的mysqli,curl,gettext等,PECL中也有大量的相似擴展。mysql
傳統的方式,當咱們須要用一些已有的C語言的庫的能力的時候,咱們須要用C語言寫包裝器,把他們包裝成擴展,這個過程當中就須要你們去學習PHP的擴展怎麼寫,固然如今也有一些方便的方式,某種Zephir。但總仍是有一些學習成本的,而有了FFI以後,咱們就能夠直接在PHP腳本中調用C語言寫的庫中的函數了。laravel
而C語言幾十年的歷史中,積累積累的優秀的庫,FFI直接讓咱們能夠方便的享受這個龐大的資源了。git
言歸正傳,今天我用一個例子來介紹,咱們如何使用PHP來調用libcurl,來抓取一個網頁的內容,爲何要用libcurl呢?PHP不是已經有了curl擴展了麼?嗯,首先由於libcurl的api我比較熟,其次呢,正是由於有了,纔好對比,傳統擴展方式和FFI方式直接的易用性不是?github
首先,某些咱們就拿當前你看的這篇文章爲例,我如今須要寫一段代碼來抓取它的內容,若是用傳統的PHP的curl擴展,咱們大概會這麼寫:sql
<?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呢?shell
首先要啓用PHP7.4的ext / ffi,須要注意的是PHP-FFI要求libffi-3以上。api
而後,咱們須要告訴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 無效* curl_easy_init (); int curl_easy_setopt ( void * curl , int選項, ...); int curl_easy_perform ( void * curl ); void curl_easy_cleanup ( void * handle ); 類型 , 「 libcurl.so」 );
這裏有個地方是,文檔中寫的是返回值是CURL *,但事實上由於咱們的示例中不會解引用它,只是傳遞,那就避免麻煩就用void *代替。
然而還有個麻煩的事情是,PHP預約義好了:
<?php const CURLOPT_URL = 10002 ; const CURLOPT_SSL_VERIFYPEER = 64 ; $ libcurl = FFI :: cdef (<<< CTYPE 無效* curl_easy_init (); int curl_easy_setopt ( void * curl , int選項, ...); int curl_easy_perform ( void * curl ); void curl_easy_cleanup ( void * handle ); 類型 , 「 libcurl.so」 );
好了,定義部分就算完成了,如今咱們完成實際邏輯部分,整個下來的代碼會是:
<?php 須要 「 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 curl擴展也是這樣作的。
目前咱們並不能直接把一個PHP函數做爲附加函數經過FFI傳遞給libcurl,那咱們都有倆種方式來作:
1.採用WRITEDATA,默認的libcurl會調用fwrite做爲一個變量函數,而咱們能夠經過WRITEDATA給libcurl一個fd,讓它不要寫入stdout,而是寫入到這個fd
2.咱們本身編寫一個C到簡單函數,經過FFI日期進來,傳遞給libcurl。
咱們先用第一種方式,首先咱們須要使用fopen,此次咱們經過定義一個C的頭文件來申明原型(file.h):
void * fopen ( char *文件名, char *模式); void fclose ( void * fp );
像file.h同樣,咱們把全部的libcurl的函數申明也放到curl.h中去
#定義 FFI_LIB 「libcurl.so」 無效 * curl_easy_init (); int curl_easy_setopt (void * curl , int選項, ...); int curl_easy_perform (void * curl ); void curl_easy_cleanup (CURL * handle );
而後咱們就可使用FFI :: load來加載.h文件:
靜態 函數 加載(字符串$ 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 *)數據; size_t total =大小* nmember ; 若是 ( d- > buf == NULL ) { d- > buf = malloc ( total ); 若是 ( d- > buf == NULL ) { 返回 0 ; } d- > size = total ; memcpy ( d- > buf , ptr , total ); } 其餘 { d- > buf =從新 分配( d- > buf , d- > size + total ); 若是 ( d- > buf == NULL ) { 返回 0 ; } memcpy ( d- > buf + d- > size , ptr , total ); d- > size + = total ; } 回報總額; } 無效 * init () { return & own_writefunc ; }
注意此處的初始函數,由於在PHP FFI中,就目前的版本(2020-03-11)咱們沒有辦法直接得到一個函數指針,因此咱們定義了這個函數,返回own_writefunc的地址。
最後咱們定義上面用到的頭文件write.h:
#定義 FFI_LIB 「write.so」 typedef struct _writedata { 無效 * buf ; size_t 大小; } own_write_data ; 無效 * 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 :: 字符串($ data- > buf , $ data- > size );
此處,咱們使用FFI :: new($ write-> new)來分配了一個結構_write_data的內存:
函數 FFI :: 新(混合$ type [, bool $ own = true [, bool $ persistent = false ]]): FFI \ CData
$ own表示這個內存管理是否採用PHP的內存管理,有時的狀況下,咱們申請的內存會通過PHP的生命週期管理,不須要主動釋放,可是有的時候你也可能但願本身管理,那麼能夠設置$ own爲flase,那麼在適當的時候,你須要調用FFI :: free去主動釋放。
而後咱們把$ data做爲WRITEDATA傳遞給libcurl,這裏咱們使用了FFI :: addr來獲取$ data的實際內存地址:
靜態 函數 地址( FFI \ CData $ cdata ): FFI \ CData ;
而後咱們把own_write_func做爲WRITEFUNCTION傳遞給了libcurl,這樣再有返回的時候,libcurl就會調用咱們的own_write_func來處理返回,同時會把write_data做爲自定義參數傳遞給咱們的替代函數。
最後咱們使用了FFI :: string來把一段內存轉換成PHP的string:
靜態 函數 FFI :: 字符串( FFI \ CData $ src [, int $ size ]):字符串
當不提供$ size的時候,FFI :: string會在遇到Null-byte的時候中止。
好了,跑一下吧?
然而畢竟直接在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:
#定義 FFI_LIB 「libcurl.so」 #定義 FFI_SCOPE 「的libcurl」 無效 * curl_easy_init (); int curl_easy_setopt (void * curl , int選項, ...); 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 :: 範圍(「 libcurl」 ); $ write = FFI :: 範圍(「 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 :: 字符串($ data- > buf , $ data- > size );
也就是,咱們如今使用FFI :: scope來代替FFI :: load,引用對應的函數。
靜態 函數 範圍(字符串$ name ): FFI ;
而後還有另一個問題,FFI雖然給了咱們很大的規模,可是畢竟直接調用C庫函數,仍是很是具備風險性的,咱們應該只容許用戶調用咱們確認過的函數,因而,ffi.enable = preload就該上場了,當咱們設置ffi.enable = preload的話,那就只有在opcache.preload的腳本中的函數才能調用FFI,而用戶寫的函數是沒有辦法直接調用的。
咱們稍微修改下ffi_preload.inc變成ffi_safe_preload.inc
<?php 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」 ); 函數 get_libcurl () : FFI { 返回 FFI :: 範圍(「 libcurl」 ); } 函數 get_write_data ($ write ) : FFI \ CData { 返回 $ write- > new (「 own_write_data」 ); } 函數 get_write () : FFI { 返回 FFI :: 範圍(「 write」 ); } 函數 get_data_addr ($ data ) : FFI \ CData { 返回 FFI :: addr ($ data ); } 函數 paser_libcurl_ret ($ data ) :字符串{ 返回 FFI :: 字符串($ 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崩潰,因此要當心。
以上內容但願幫助到你們,不少PHPer在進階的時候總會遇到一些問題和瓶頸,業務代碼寫多了沒有方向感,不知道該從那裏入手去提高,對此我整理了一些資料,包括但不限於:分佈式架構、高可擴展、高性能、高併發、服務器性能調優、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優化、shell腳本、Docker、微服務、Nginx等多個知識點高級進階乾貨須要的能夠免費分享給你們,須要請戳這裏