真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

PHP中FFI擴(kuò)展方式的使用方法-創(chuàng)新互聯(lián)

隨著PHP7.4而來(lái)的有一個(gè)我認(rèn)為非常有用的一個(gè)擴(kuò)展:PHP FFI(Foreign Function interface),引用一段PHP FFI RFC中的一段描述:

創(chuàng)新互聯(lián)-專(zhuān)業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性?xún)r(jià)比廬陽(yáng)網(wǎng)站開(kāi)發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫(kù),直接使用。一站式廬陽(yáng)網(wǎng)站制作公司更省心,省錢(qián),快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋廬陽(yáng)地區(qū)。費(fèi)用合理售后完善,十余年實(shí)體公司更值得信賴(lài)。

For PHP, FFI opens a way to write PHP extensions and bindings to C libraries in pure PHP.

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

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

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

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

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

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

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

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

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

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

在字符串$cdef中,我們可以寫(xiě)C語(yǔ)言函數(shù)式申明,F(xiàn)FI會(huì)parse它,了解到我們要在字符串$lib這個(gè)庫(kù)中調(diào)用的函數(shù)的簽名是啥樣的,在這個(gè)例子中,我們用到三一個(gè)libcurl的函數(shù),它們的申明我們都可以在libcurl的文檔里找到,某些關(guān)于curl_easy_init。

具體到這個(gè)例子,我們寫(xiě)一個(gè)curl.php,包含所有要申明的東西,代碼如下:

$libcurl = FFI::cdef(<<

這里有個(gè)地方是,文檔中寫(xiě)的是返回值是CURL *,但事實(shí)上因?yàn)槲覀兊氖纠胁粫?huì)解引用它,只是傳遞,那就避免麻煩就用void *代替。

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

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

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

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

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

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

2.我們自己編寫(xiě)一個(gè)C到簡(jiǎn)單函數(shù),通過(guò)FFI日期進(jìn)來(lái),傳遞給libcurl。

我們先用第一種方式,首先我們需要使用fopen,這次我們通過(guò)定義一個(gè)C的頭文件來(lái)申明原型(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來(lái)加載.h文件:

static function load(string $filename): FFI;

但是怎么告訴FFI加載那個(gè)對(duì)應(yīng)的庫(kù)呢?如上面,我們通過(guò)定義了一個(gè)FFI_LIB的宏,來(lái)告訴FFI這些函數(shù)來(lái)自libcurl.so,當(dāng)我們用FFI :: load加載這個(gè)h文件的時(shí)候,PHP FFI就會(huì)自動(dòng)加載libcurl.so

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

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

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

但這種方式呢就是需要一個(gè)臨時(shí)的中轉(zhuǎn)文件,還是不夠優(yōu)雅,現(xiàn)在我們用第二種方式,要用第二種方式,我們需要自己用C寫(xiě)一個(gè)替代函數(shù)傳遞給libcurl:

#include 
#include 
#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)我們沒(méi)有辦法直接獲得一個(gè)函數(shù)指針,所以我們定義了這個(gè)函數(shù),返回own_writefunc的地址。

最后我們定義上面用到的頭文件write.h

#define FFI_LIB "write.so"
 
typedef struct _writedata {
        void *buf;
        size_t size;
} own_write_data;
 
void *init();

注意到我們?cè)陬^文件中也定義了FFI_LIB,這樣這個(gè)頭文件就可以同時(shí)被write.c和接下來(lái)我們的PHP FFI共同使用了。

然后我們編譯write函數(shù)為一個(gè)動(dòng)態(tài)庫(kù):

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

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

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

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

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

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

static function addr(FFI\CData $cdata): FFI\CData;

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

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

static function FFI::string(FFI\CData $src [, int $size]): string

好了,跑一下吧?

然而畢竟直接在PHP中每次請(qǐng)求都加載so的話,會(huì)是一個(gè)很大的性能問(wèn)題,所以我們也可以采用preload的方式,這種模式下,我們通過(guò)opcache.preload來(lái)在PHP啟動(dòng)的時(shí)候就加載好:

ffi.enable=1
opcache.preload=ffi_preload.inc

ffi_preload.inc:

但我們引用加載的FFI呢?因此我們需要修改一下這倆個(gè).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);

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

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來(lái)代替FFI :: load,引用對(duì)應(yīng)的函數(shù)。

static function scope(string $name): FFI;

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

我們稍微修改下ffi_preload.inc變成ffi_safe_preload.inc

new("own_write_data");
}
 
function get_write() : FFI {
     return FFI::scope("write");
}
 
function get_data_addr($data) : FFI\CData {
     return FFI::addr($data);
}
 
function paser_libcurl_ret($data) :string{
     return FFI::string($data->buf, $data->size);
}

也就是,我們把所有會(huì)調(diào)用FFI API的函數(shù)都定義在preload腳本中,然后我們的示例會(huì)變成(ffi_safe.php):

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

這樣一來(lái)通過(guò)ffi.enable = preload,我們就可以限制,所有的FFI API只能被我們可控制的preload腳本調(diào)用,用戶(hù)不能直接調(diào)用。從而我們可以在這些函數(shù)內(nèi)部做好適當(dāng)?shù)陌踩WC工作,從而保證一定的安全性。

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

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

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

以上就是PHP7.4 全新擴(kuò)展方式 FFI 詳解的詳細(xì)內(nèi)容,更多請(qǐng)關(guān)注創(chuàng)新互聯(lián)成都網(wǎng)站設(shè)計(jì)公司其它相關(guān)文章!

另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務(wù)器15元起步,三天無(wú)理由+7*72小時(shí)售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國(guó)服務(wù)器、虛擬主機(jī)、免備案服務(wù)器”等云主機(jī)租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡(jiǎn)單易用、服務(wù)可用性高、性?xún)r(jià)比高”等特點(diǎn)與優(yōu)勢(shì),專(zhuān)為企業(yè)上云打造定制,能夠滿(mǎn)足用戶(hù)豐富、多元化的應(yīng)用場(chǎng)景需求。


本文名稱(chēng):PHP中FFI擴(kuò)展方式的使用方法-創(chuàng)新互聯(lián)
當(dāng)前路徑:http://weahome.cn/article/peoih.html

其他資訊

在線咨詢(xún)

微信咨詢(xún)

電話咨詢(xún)

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部