0

0

PHP7.4 全新扩展方式 FFI 详解

Guanhui

Guanhui

发布时间:2020-04-28 13:24:42

|

3195人浏览过

|

来源于laruence

转载

随着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免费学习笔记(深入)”;

言归正传,今天我用一个例子来介绍,我们如何使用PHP来调用libcurl,来抓取一个网页的内容,为什么要用libcurl呢?PHP不是已经有了curl扩展了么?嗯,首先因为libcurl的api我比较熟,其次呢,正是因为有了,才好对比,传统扩展方式AS和FFI方式直接的易用性不是?

首先,某些我们就拿当前你看的这篇文章为例,我现在需要写一段代码来抓取它的内容,如果用传统的PHP的curl扩展,我们大概会这么写:

(因为我的网站是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(<<

这里有个地方是,文档中写的是返回值是CURL *,但事实上因为我们的示例中不会解引用它,只是传递,那就避免麻烦就用void *代替。

然而还有个麻烦的事情是,PHP预定义好了:

好了,定义部分就算完成了,现在我们完成实际逻辑部分,整个下来的代码会是:

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是一个标准库函数,它早就存在了。

好,现在整个代码会是:

落叶冰点万能企业网站生成系统9.1 (带标签帮助)
落叶冰点万能企业网站生成系统9.1 (带标签帮助)

新动软万能网站内容管理cms系统采用自行研发的全新的模板标签系统内核,致力于万能性和实用性而设计开发,是各种网站应用的最佳解决方案。其后台提供的万能式的功能设计框架和界面设计框架,使之适合从个人到企业,政府等各方面应用的要求,灵活的可扩展性和强大的兼容性是本系统的一大特点。

下载
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 
#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;
}

注意此处的初始函数,因为在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

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

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:

但我们引用加载的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”,然后我们的脚本现在看起来应该是这样的:

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

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

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

相关文章

PHP速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
php文件怎么打开
php文件怎么打开

打开php文件步骤:1、选择文本编辑器;2、在选择的文本编辑器中,创建一个新的文件,并将其保存为.php文件;3、在创建的PHP文件中,编写PHP代码;4、要在本地计算机上运行PHP文件,需要设置一个服务器环境;5、安装服务器环境后,需要将PHP文件放入服务器目录中;6、一旦将PHP文件放入服务器目录中,就可以通过浏览器来运行它。

2544

2023.09.01

php怎么取出数组的前几个元素
php怎么取出数组的前几个元素

取出php数组的前几个元素的方法有使用array_slice()函数、使用array_splice()函数、使用循环遍历、使用array_slice()函数和array_values()函数等。本专题为大家提供php数组相关的文章、下载、课程内容,供大家免费下载体验。

1611

2023.10.11

php反序列化失败怎么办
php反序列化失败怎么办

php反序列化失败的解决办法检查序列化数据。检查类定义、检查错误日志、更新PHP版本和应用安全措施等。本专题为大家提供php反序列化相关的文章、下载、课程内容,供大家免费下载体验。

1501

2023.10.11

php怎么连接mssql数据库
php怎么连接mssql数据库

连接方法:1、通过mssql_系列函数;2、通过sqlsrv_系列函数;3、通过odbc方式连接;4、通过PDO方式;5、通过COM方式连接。想了解php怎么连接mssql数据库的详细内容,可以访问下面的文章。

952

2023.10.23

php连接mssql数据库的方法
php连接mssql数据库的方法

php连接mssql数据库的方法有使用PHP的MSSQL扩展、使用PDO等。想了解更多php连接mssql数据库相关内容,可以阅读本专题下面的文章。

1417

2023.10.23

html怎么上传
html怎么上传

html通过使用HTML表单、JavaScript和PHP上传。更多关于html的问题详细请看本专题下面的文章。php中文网欢迎大家前来学习。

1234

2023.11.03

PHP出现乱码怎么解决
PHP出现乱码怎么解决

PHP出现乱码可以通过修改PHP文件头部的字符编码设置、检查PHP文件的编码格式、检查数据库连接设置和检查HTML页面的字符编码设置来解决。更多关于php乱码的问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

1446

2023.11.09

php文件怎么在手机上打开
php文件怎么在手机上打开

php文件在手机上打开需要在手机上搭建一个能够运行php的服务器环境,并将php文件上传到服务器上。再在手机上的浏览器中输入服务器的IP地址或域名,加上php文件的路径,即可打开php文件并查看其内容。更多关于php相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

1306

2023.11.13

C++ 单元测试与代码质量保障
C++ 单元测试与代码质量保障

本专题系统讲解 C++ 在单元测试与代码质量保障方面的实战方法,包括测试驱动开发理念、Google Test/Google Mock 的使用、测试用例设计、边界条件验证、持续集成中的自动化测试流程,以及常见代码质量问题的发现与修复。通过工程化示例,帮助开发者建立 可测试、可维护、高质量的 C++ 项目体系。

1

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PHP课程
PHP课程

共137课时 | 8.7万人学习

JavaScript ES5基础线上课程教学
JavaScript ES5基础线上课程教学

共6课时 | 7万人学习

PHP新手语法线上课程教学
PHP新手语法线上课程教学

共13课时 | 0.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号