Maison >développement back-end >PHP7 >PHP7.4 nouvelle méthode d'extension FFI Explication détaillée

PHP7.4 nouvelle méthode d'extension FFI Explication détaillée

Guanhui
Guanhuiavant
2020-04-28 13:24:422924parcourir

Avec PHP7.4 vient une extension que je pense très utile : PHP FFI(Foreign Function interface), citant une description dans le PHP FFI RFC :

Pour PHP, FFI ouvre un moyen d'écrire des extensions PHP et des liaisons aux bibliothèques C en PHP pur.

Oui, FFI fournit des langages de haut niveau pour s'appeler directement, et pour PHP, FFI nous permet d'appeler facilement C Diverses bibliothèques écrites dans le langue.

En fait, il existe un grand nombre d'extensions PHP qui sont des packages de certaines bibliothèques C existantes, certaines couramment utilisées mysqli, curl, gettext, etc. Il existe également un grand nombre d'extensions similaires en PECL.

De manière traditionnelle, lorsque nous devons utiliser les capacités de certaines bibliothèques de langage C existantes, nous devons écrire des wrappers en langage C et les empaqueter dans des extensions. Ce processus nécessite que tout le monde apprenne comment écrire des extensions PHP. ? Bien sûr, il existe maintenant des moyens pratiques, des sortes de Zephir. Mais il y a quand même un coût d'apprentissage, et avec FFI, on peut appeler directement des fonctions dans des bibliothèques écrites en langage C dans des scripts PHP.

Au cours des décennies d'histoire du langage C, d'excellentes bibliothèques ont été accumulées, et FFI nous permet directement de profiter facilement de cette énorme ressource.

Pour en revenir au sujet, aujourd'hui, je vais utiliser un exemple pour présenter comment nous utilisons PHP pour appeler libcurl afin d'explorer le contenu d'une page Web. Pourquoi utiliser libcurl ? PHP n'a-t-il pas déjà une extension curl ? Eh bien, tout d'abord, je connais l'API de libcurl. Deuxièmement, c'est précisément grâce à elle que je peux la comparer. Les méthodes d'expansion traditionnelles AS et FFI ne sont-elles pas directement plus faciles à utiliser ?

Tout d'abord, prenons comme exemple l'article que vous lisez actuellement. J'ai maintenant besoin d'écrire un morceau de code pour capturer son contenu. Si nous utilisons l'extension PHP curl traditionnelle, nous écrirons probablement comme. this :

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

(Comme mon site Web est https, il y aura un paramètre supplémentaire SSL_VERIFYPEER opération) Et si j'utilise FFI ?

Tout d'abord, activez ext/ffi de PHP7.4. Il convient de noter que PHP-FFI nécessite libffi-3 ou supérieur.

Ensuite, nous devons dire à PHP FFI à quoi ressemble le prototype de la fonction que nous voulons appeler. Nous pouvons utiliser FFI :: cdef pour cela, et son prototype est :

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

dans le fichier. string $cdef , nous pouvons écrire une déclaration fonctionnelle en langage C, FFI la parse et comprendre à quoi ressemble la signature de la fonction que nous voulons appeler dans la bibliothèque string $lib Dans cet exemple, nous en utilisons trois. Une fonction libcurl, leurs déclarations peuvent être trouvées dans la documentation libcurl, quelques informations sur curl_easy_init.

Pour cet exemple, nous écrivons un curl.php qui contient tout ce qui doit être déclaré. Le code est le suivant :

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

Il y a un endroit ici où est la valeur de retour écrite dans le document. CURL *, mais en fait puisqu'il n'est pas déréférencé dans notre exemple, juste passé, utilisez plutôt void * pour éviter les ennuis.

Cependant, un autre problème est que PHP est prédéfini :

D'accord, même si la partie définition est terminée, maintenant nous complétons la partie logique proprement dite, et tout le Code sera :

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

Que diriez-vous d'utiliser l'expansion curl à la place, est-ce tout aussi concis ?

Ensuite, nous compliquons un peu les choses, jusqu'à ce que, si nous ne voulons pas que le résultat soit affiché directement, mais renvoyé sous forme de chaîne, pour l'extension curl de PHP, il suffit d'appeler curl_setop Définissez CURLOPT_RETURNTRANSFER sur 1, mais libcurl n'a pas la capacité de renvoyer directement une chaîne, ou fournit une fonction alternative à WRITEFUNCTION Lorsque les données sont renvoyées, libcurl appellera cette fonction. En fait, les extensions PHP curl le font comme. Bien.

Actuellement, nous ne pouvons pas passer directement une fonction PHP comme fonction supplémentaire à libcurl via FFI, alors nous avons deux façons de le faire :

1 Utiliser WRITEDATA, la libcurl par défaut sera appelé comme fonction variable, et nous pouvons donner à libcurl un fd via fwrite pour qu'il n'écrive pas dans WRITEDATA, mais dans ce fd stdout

2. Nous en écrivons un nous-mêmes C dans un fonction simple qui arrive via la date FFI et la transmet à libcurl.

Utilisons d'abord la première méthode. Nous devons d'abord utiliser

Cette fois, nous déclarons le prototype (fopen) en définissant un fichier d'en-tête C : file.h

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

LikeDe même, nous mettons toutes les déclarations de fonction libcurl dans

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

Ensuite, nous pouvons utiliser

pour charger le fichier .h :

static function load(string $filename): FFI;
FFI :: loadMais comment dire à FFI de charger la bibliothèque correspondante ? Comme ci-dessus, nous définissons une macro

pour indiquer à FFI que ces fonctions proviennent de

Lorsque nous utilisons FFI_LIB pour charger ce fichier h, PHP FFI chargera automatiquement libcurl.solibcurl.soFFI :: loadAlors pourquoi.

n'a pas besoin de spécifier la bibliothèque de chargement ? C'est parce que FFI recherchera également des symboles dans la table des symboles variables, et

est une fonction de bibliothèque standard qui existe déjà. fopenfopenD'accord, maintenant le code entier sera :

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

推荐PHP教程《PHP7

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer