Heim > Backend-Entwicklung > PHP7 > Hauptteil

PHP7.4 neue Erweiterungsmethode FFI ausführliche Erklärung

Guanhui
Freigeben: 2023-02-17 14:36:01
nach vorne
2883 Leute haben es durchsucht

Mit PHP7.4 kommt eine Erweiterung, die ich für sehr nützlich halte: PHP FFI(Foreign Function interface), unter Berufung auf eine Beschreibung im PHP FFI RFC:

Für PHP eröffnet FFI eine Möglichkeit, PHP-Erweiterungen zu schreiben und Bindungen an C-Bibliotheken in reinem PHP.

Ja, FFI bietet Hochsprachen für den direkten gegenseitigen Aufruf, und für PHP ermöglicht uns FFI den einfachen Aufruf verschiedener in C geschriebener Bibliotheken Sprache.

Tatsächlich gibt es eine große Anzahl von PHP-Erweiterungen, die Pakete einiger vorhandener C-Bibliotheken sind, einige häufig verwendete mysqli, Curl, gettext usw. Es gibt auch eine große Anzahl ähnlicher Erweiterungen in PECL.

Wenn wir auf herkömmliche Weise die Funktionen einiger vorhandener C-Sprachbibliotheken nutzen müssen, müssen wir Wrapper in C-Sprache schreiben und sie in Erweiterungen packen. Dieser Prozess erfordert, dass jeder lernt, wie man PHP-Erweiterungen schreibt ? Natürlich gibt es jetzt einige bequeme Möglichkeiten, eine Art Zephir. Es fallen jedoch noch einige Lernkosten an, und mit FFI können wir Funktionen in Bibliotheken, die in C-Sprache geschrieben sind, direkt in PHP-Skripten aufrufen.

In der jahrzehntelangen Geschichte der C-Sprache wurden hervorragende Bibliotheken angesammelt, und FFI ermöglicht es uns direkt, diese riesige Ressource bequem zu nutzen.

Um auf das Thema zurückzukommen, werde ich heute anhand eines Beispiels vorstellen, wie wir PHP verwenden, um libcurl aufzurufen, um den Inhalt einer Webseite zu crawlen. Verfügt PHP nicht bereits über eine Curl-Erweiterung? Nun, erstens bin ich mit der API von libcurl vertraut. Zweitens ist es nicht gerade deshalb einfacher, die traditionellen Erweiterungsmethoden AS und FFI zu verwenden.

Nehmen wir zunächst den Artikel, den Sie gerade lesen, als Beispiel. Ich muss jetzt einen Code schreiben, um seinen Inhalt zu erfassen. Wenn wir die traditionelle PHP-Curl-Erweiterung verwenden, werden wir wahrscheinlich so schreiben dies:

<?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);
Nach dem Login kopieren

(Da meine Website https ist, wird es einen zusätzlichen Einstellungsvorgang SSL_VERIFYPEER geben) Was ist, wenn ich FFI verwende?

Aktivieren Sie zunächst ext/ffi von PHP7.4. Beachten Sie, dass PHP-FFI libffi-3 oder höher erfordert.

Dann müssen wir PHP FFI mitteilen, wie der Prototyp der Funktion aussieht, die wir aufrufen möchten. Dazu können wir FFI :: cdef verwenden, und sein Prototyp lautet:

FFI::cdef([string $cdef = "" [, string $lib = null]]): FFI
Nach dem Login kopieren

string $cdef können wir eine C-Sprachfunktionsdeklaration schreiben, FFI wird sie parse erstellen und verstehen, wie die Signatur der Funktion aussieht, die wir in der String $lib-Bibliothek aufrufen möchten. In diesem Beispiel verwenden wir drei Eine libcurl-Funktion, ihre Deklarationen finden Sie in der libcurl-Dokumentation, einige Informationen zu curl_easy_init.

Für dieses Beispiel schreiben wir ein curl.php, das alles enthält, was deklariert werden soll. Der Code lautet wie folgt:

$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"
 );
Nach dem Login kopieren

Hier gibt es eine Stelle, an der der Rückgabewert im Dokument steht CURL *, aber da wir es in unserem Beispiel nicht dereferenzieren, geben Sie es einfach weiter und verwenden Sie stattdessen einfach void *, um Ärger zu vermeiden.

Eine weitere problematische Sache ist jedoch, dass PHP vordefiniert ist:

Nach dem Login kopieren

Okay, auch wenn der Definitionsteil abgeschlossen ist, vervollständigen wir jetzt den eigentlichen Logikteil und der gesamte Code wird sein:

<?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);
Nach dem Login kopieren

Wie wäre es stattdessen mit der Lockenerweiterung? Ist das genauso prägnant?

Als nächstes machen wir es etwas komplizierter, bis wir, wenn wir nicht möchten, dass das Ergebnis direkt ausgegeben, sondern als String zurückgegeben wird, für die Curl-Erweiterung von PHP nur noch curl_setop aufrufen müssen Setzen Sie CURLOPT_RETURNTRANSFER auf 1, aber libcurl kann keine Zeichenfolge direkt zurückgeben oder bietet eine alternative Funktion zu WRITEFUNCTION. Wenn Daten zurückgegeben werden, ruft libcurl diese Funktion tatsächlich auf Also.

Derzeit können wir eine PHP-Funktion nicht direkt als zusätzliche Funktion an libcurl über FFI übergeben, dann haben wir zwei Möglichkeiten, dies zu tun:

1. Verwenden Sie WRITEDATA, die Standard-Libcurl fwrite wird als variable Funktion aufgerufen, und wir können libcurl über WRITEDATA ein fd geben, sodass es nicht auf stdout schreibt, sondern auf dieses fd

2. Wir schreiben selbst ein C in a einfache Funktion, die über das FFI-Datum eingeht und es an libcurl übergibt.

Zuerst müssen wir fopen verwenden, indem wir eine C-Header-Datei definieren: file.h

void *fopen(char *filename, char *mode);
void fclose(void * fp);
Nach dem Login kopieren

Like

In ähnlicher Weise fügen wir alle libcurl-Funktionsdeklarationen in file.hcurl.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);
Nach dem Login kopieren

ein. Dann können wir

verwenden, um die .h-Datei zu laden: FFI :: load

static function load(string $filename): FFI;
Nach dem Login kopieren

Aber wie sagt man FFI dazu die entsprechende Bibliothek laden? Wie oben definieren wir ein

-Makro, um FFI mitzuteilen, dass diese Funktionen von FFI_LIB stammen. Wenn wir libcurl.so verwenden, um diese h-Datei zu laden, lädt PHP FFI automatisch libcurl.so FFI :: load

Warum dann

muss die Ladebibliothek nicht angeben? Das liegt daran, dass FFI auch in der Variablensymboltabelle nach Symbolen sucht und fopen eine Standardbibliotheksfunktion ist, die bereits vorhanden ist. fopen

Okay, jetzt lautet der gesamte Code:

<?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);
Nach dem Login kopieren

但这种方式呢就是需要一个临时的中转文件,还是不够优雅,现在我们用第二种方式,要用第二种方式,我们需要自己用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;
}
Nach dem Login kopieren

注意此处的初始函数,因为在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();
Nach dem Login kopieren

注意到我们在头文件中也定义了FFI_LIB,这样这个头文件就可以同时被write.c和接下来我们的PHP FFI共同使用了。

然后我们编译write函数为一个动态库:

gcc -O2 -fPIC -shared  -g  write.c -o write.so
Nach dem Login kopieren

好了,现在整个的代码会变成:

<?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);
Nach dem Login kopieren

此处,我们使用FFI :: new($ write-> new)来分配了一个结构_write_data的内存:

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

$own表示这个内存管理是否采用PHP的内存管理,有时的情况下,我们申请的内存会经过PHP的生命周期管理,不需要主动释放,但是有的时候你也可能希望自己管理,那么可以设置$ownflase,那么在适当的时候,你需要调用FFI :: free去主动释放。

然后我们把$data作为WRITEDATA传递给libcurl,这里我们使用了FFI :: addr来获取$data的实际内存地址:

static function addr(FFI\CData $cdata): FFI\CData;
Nach dem Login kopieren

然后我们把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
Nach dem Login kopieren

好了,跑一下吧?

然而毕竟直接在PHP中每次请求都加载so的话,会是一个很大的性能问题,所以我们也可以采用preload的方式,这种模式下,我们通过opcache.preload来在PHP启动的时候就加载好:

ffi.enable=1
opcache.preload=ffi_preload.inc
Nach dem Login kopieren

ffi_preload.inc:

<?php
FFI::load("curl.h");
FFI::load("write.h");
Nach dem Login kopieren

但我们引用加载的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);
Nach dem Login kopieren

对应的我们给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);
Nach dem Login kopieren

也就是,我们现在使用FFI :: scope来代替FFI :: load,引用对应的函数。

static function scope(string $name): FFI;
Nach dem Login kopieren

然后还有另外一个问题,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);
}
Nach dem Login kopieren

也就是,我们把所有会调用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);
Nach dem Login kopieren

这样一来通过ffi.enable = preload,我们就可以限制,所有的FFI API只能被我们可控制的preload脚本调用,用户不能直接调用。从而我们可以在这些函数内部做好适当的安全保证工作,从而保证一定的安全性。

好了,经历了这个例子,大家应该对FFI有一个比较深入的理解了,详细的PHP API说明,大家可以参考:PHP-FFI Manual,有兴趣的话,就去找一个C库,试试吧?

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

最后还是多说一句,例子只是为了演示功能,所以省掉了很多错误分支的判断捕获,大家自己写的时候还是要加入。毕竟使用FFI的话,会让你会有1000种方式让PHP segfault crash,所以be careful

推荐PHP教程《PHP7

Das obige ist der detaillierte Inhalt vonPHP7.4 neue Erweiterungsmethode FFI ausführliche Erklärung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Verwandte Etiketten:
php
Quelle:laruence.com
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage