PHP7.4 全新擴展方式 FFI 詳解

来源:https://www.cnblogs.com/a609251438/archive/2020/04/28/12797020.html
-Advertisement-
Play Games

隨著PHP7.4而來的有一個我認為非常有用的一個擴展:PHP FFI(Foreign Function interface),引用一段PHP FFI RFC中的一段描述 For PHP, FFI opens a way to write PHP extensions and bindings to ...


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

是的,FFI提供了高級語言直接的互相調用,而對於PHP而言,FFI讓我們可以方便的調用C語言寫的各種庫。

其實現有大量的PHP擴展是對一些已有的C庫的包裝,某些常用的mysqli,curl,gettext等,PECL中也有大量的類似擴展。

傳統的方式,當我們需要用一些已有的C語言的庫的能力的時候,我們需要用C語言寫包裝器,把他們包裝成擴展,這個過程中就需要大家去學習PHP的擴展怎麼寫,當然現在也有一些方便的方式,某種Zephir。但總還是有一些學習成本的,而有了FFI之後,我們就可以直接在PHP腳本中調用C語言寫的庫中的函數了。

而C語言幾十年的歷史中,積累積累的優秀的庫,FFI直接讓我們可以方便的享受這個龐大的資源了。

言歸正傳,今天我用一個例子來介紹,我們如何使用PHP來調用libcurl,來抓取一個網頁的內容,為什麼要用libcurl呢?PHP不是已經有了curl擴展了麽?嗯,首先因為libcurl的api我比較熟,其次呢,正是因為有了,才好對比,傳統擴展方式AS和FFI方式直接的易用性不是?

首先,某些我們就拿當前你看的這篇文章為例,我現在需要寫一段代碼來抓取它的內容,如果用傳統的PHP的curl擴展,我們大概會這麼寫:

<?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呢?

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

然後,我們需要告訴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
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 *,但事實上因為我們的示例中不會解引用它,只是傳遞,那就避免麻煩就用void *代替。

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

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

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

<?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_setopCURLOPT_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 *filename, char *mode);
void fclose(void * fp);

file.h一樣,我們把所有的libcurl的函數申明也放到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載入那個對應的庫呢?如上面,我們通過定義了一個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*)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;
}

註意此處的初始函數,因為在PHP FFI中,就目前的版本(2020-03-11)我們沒有辦法直接獲得一個函數指針,所以我們定義了這個函數,返回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函數為一個動態庫:

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::string($data->buf, $data->size);

此處,我們使用FFI :: new($ write-> new)來分配了一個結構_write_data的記憶體:

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

$own表示這個記憶體管理是否採用PHP的記憶體管理,有時的情況下,我們申請的記憶體會經過PHP的生命周期管理,不需要主動釋放,但是有的時候你也可能希望自己管理,那麼可以設置$ownflase,那麼在適當的時候,你需要調用FFI :: free去主動釋放。

然後我們把$data作為WRITEDATA傳遞給libcurl,這裡我們使用了FFI :: addr來獲取$data的實際記憶體地址:

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

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

最後我們使用了FFI :: string來把一段記憶體轉換成PHP的string

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

對應的我們給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::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,引用對應的函數。

static function scope(string $name): FFI;

然後還有另外一個問題,FFI雖然給了我們很大的規模,但是畢竟直接調用C庫函數,還是非常具有風險性的,我們應該只允許用戶調用我們確認過的函數,於是,ffi.enable = preload就該上場了,當我們設置ffi.enable = preload的話,那就只有在opcache.preload的腳本中的函數才能調用FFI,而用戶寫的函數是沒有辦法直接調用的。

我們稍微修改下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) : FFI\CData {
 return $write->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);
}

也就是,我們把所有會調用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 crash,所以be careful


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 概述 在使用Python或者其他的編程語言,都會多多少少遇到編碼錯誤,處理起來非常痛苦。在Stack Overflow和其他的編程問答網站上,UnicodeDecodeError和UnicodeEncodeError也經常被提及。本篇教程希望能幫你認識Python編碼,並能夠從容的處理編碼問題。 這 ...
  • 一丶簡介 Topic Exchange 將路由鍵和某模式進行匹配。此時隊列需要綁定要一個模式上。符號“#”匹配一個或多個詞,符號“*”匹配不多不少一個詞。因此“audit.#”能夠匹配到“audit.irs.corporate”,但是“audit.*” 只會匹配到“audit.irs”。 業務場景: ...
  • 場景 我們用Django的Model時,有時候需要關聯外鍵。關聯外鍵時,參數: 的幾個配置選項到底是幹嘛的呢,你知道嗎? 參數介紹 級聯刪除。Django會模擬SQL約束的行為,在刪除此條數據時,同事刪除外鍵關聯的對象。 比如:用戶的有一個外鍵關聯的是用戶的健康記錄表,當用戶刪除時,配置了這個參數的 ...
  • 【目錄】 一 IO模型介紹 二 阻塞IO(blocking IO) 三 非阻塞IO(non-blocking IO) 四 多路復用IO(IO multiplexing) 五 非同步IO(Asynchronous I/O) 六 IO模型比較分析 七 selectors模塊 本文討論的背景是Linux環境 ...
  • 一、代碼實現 1.數組轉換成List String[] deviceIdAy = buildingDto.getChannelId().split(Symbol.COMMA);//設備idList<String> deviceIdList = Arrays.asList(deviceIdAy); 2 ...
  • 目錄 pyecharts模塊 簡介 Echarts 是一個由百度開源的數據可視化,憑藉著良好的交互性,精巧的圖表設計,得到了眾多開發者的認可。而 Python 是一門富有表達力的語言,很適合用於數據處理。當數據分析遇上數據可視化時,pyecharts 誕生了。 如果想要掌握pyecharts,可以閱 ...
  • 最近在項目中遇到一個需要用線程池來處理任務的需求,於是我用 來實現,但是在實現過程中我發現提交大量任務時它的處理邏輯是這樣的(提交任務還有一個 方法內部也調用了 方法): java public void execute(Runnable command) { if (command == null ...
  • 堆記憶體常見的分配策略 針對的是Serial 加 Serial Old 客戶端預設收集器組合下的記憶體分配和回收策略 經典的垃圾收集器 CMS 收集器 CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的垃圾收集器。從名字可以看出,CMS 是基於標記-清除演算法的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...