PHP FFI详解——一种全新的PHP扩展方法

随着PHP7.4而来的有一个我认为非常有用的一个扩展:PHP FFI(Foreign Function interface),引用一段PHP FFI RFC中的一段描述:

对于PHP,FFI提供了一种在纯PHP中编写PHP扩展和对C库的绑定的方法。

是的,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我比较熟,其次呢,正是因为有了,才好对比,传统扩展方式和FFI方式直接的易用性不是?

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

1<?php 2 3$ url = “ https://www.laruence.com/2020/03/11/5475.html” ; 4$ ch = curl_init (); 5 6curl_setopt ($ ch , CURLOPT_URL , $ url ); 7curl_setopt ($ ch , CURLOPT_SSL_VERIFYPEER0; 8 9curl_exec ($ ch ); 10 11curl_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.php,包含所有要申明的东西,代码如下:

1$ libcurl = FFI :: cdef (<<< CTYPE 2无效* curl_easy_init (); 3int curl_easy_setopt ( void * curl , int选项, ...; 4int curl_easy_perform ( void * curl ); 5void curl_easy_cleanup ( void * handle ); 6类型 7 , “ libcurl.so” 8;

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

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

1<?php 2const CURLOPT_URL = 10002 ; 3const CURLOPT_SSL_VERIFYPEER = 64 ; 4 5$ libcurl = FFI :: cdef (<<< CTYPE 6无效* curl_easy_init (); 7int curl_easy_setopt ( void * curl , int选项, ...; 8int curl_easy_perform ( void * curl ); 9void curl_easy_cleanup ( void * handle ); 10类型 11 , “ libcurl.so” 12;

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

1<?php 2需要 “ curl.php”3 4$ url = “ https://www.laruence.com/2020/03/11/5475.html” ; 5 6$ ch = $ libcurl- > curl_easy_init (); 7$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_URL , $ url ); 8$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_SSL_VERIFYPEER0; 9 10$ libcurl- > curl_easy_perform ($ ch ); 11 12$ libcurl- > curl_easy_cleanup ($ ch );

怎么样,比例使用curl扩展的方式,是不是一样简练呢?

接下来,我们稍微弄的复杂一点,也直到,如果我们不想要结果直接输出,而是返回成一个字符串呢,对于PHP的curl扩展来说,我们只需要调用curl_setop把CURLOPT_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):

1void * fopen ( char *文件名, char *模式); 2void fclose ( void * fp );

像file.h一样,我们把所有的libcurl的函数申明也放到curl.h中去

1#定义 FFI_LIB “libcurl.so” 2 3无效 * curl_easy_init (); 4int curl_easy_setopt (void * curl , int选项, ...; 5int curl_easy_perform (void * curl ); 6void curl_easy_cleanup (CURL * handle );

然后我们就可以使用FFI :: load来加载.h文件:

静态 函数 加载(字符串$ filename ): FFI ;

但是怎么告诉FFI加载那个对应的库呢?如上面,我们通过定义了一个FFI_LIB的宏,来告诉FFI这些函数来自libcurl.so,当我们用FFI :: load加载这个h文件的时候,PHP FFI就会自动加载libcurl.so

那为什么fopen不需要指定加载库呢,那是因为FFI也会在变量符号表中查找符号,而fopen是一个标准库函数,它早就存在了。

好,现在整个代码会是:

1<?php 2const CURLOPT_URL = 10002 ; 3const CURLOPT_SSL_VERIFYPEER = 64 ; 4const CURLOPT_WRITEDATA = 10001 ; 5 6$ libc = FFI :: load (“ file.h”; 7$ libcurl = FFI :: load (“ curl.h”; 8 9$ url = “ https://www.laruence.com/2020/03/11/5475.html” ; 10$ tmpfile =/tmp/tmpfile.out” ; 11 12$ ch = $ libcurl- > curl_easy_init (); 13$ fp = $ libc- > fopen ($ tmpfile , “ a” ); 14 15$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_URL , $ url ); 16$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_SSL_VERIFYPEER0; 17$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_WRITEDATA , $ fp ); 18$ libcurl- > curl_easy_perform ($ ch ); 19 20$ libcurl- > curl_easy_cleanup ($ ch ); 21 22$ libc- > fclose ($ fp ); 23 24$ ret = file_get_contents ($ tmpfile ); 25@unlink ($ tmpfile );

但这种方式呢就是需要一个临时的中转文件,还是不够优雅,现在我们用第二种方式,要用第二种方式,我们需要自己用C写一个替代函数传递给libcurl:

1#include <stdlib.h> 2#include <string.h> 3#include “ write.h” 4 5size_t own_writefunc (void * ptr ,size_t size ,size_t nmember ,void * data ){ 6 own_write_data * d = ( own_write_data *)数据; 7 size_t total =大小* nmember ; 8 9 如果 ( d- > buf == NULL{ 10 d- > buf = malloc ( total ); 11 如果 ( d- > buf == NULL{ 12 返回 0 ; 13 } 14 d- > size = total ; 15 memcpy ( d- > buf , ptr , total ); 16 } 其他 { 17 d- > buf =重新 分配( d- > buf , d- > size + total ); 18 如果 ( d- > buf == NULL{ 19 返回 0 ; 20 } 21 memcpy ( d- > buf + d- > size , ptr , total ); 22 d- > size + = total ; 23 } 24 25 回报总额; 26} 27 28无效 * init () { 29 return & own_writefunc ; 30}

注意此处的初始函数,因为在PHP FFI中,就目前的版本(2020-03-11)我们没有办法直接获得一个函数指针,所以我们定义了这个函数,返回own_writefunc的地址。

最后我们定义上面用到的头文件write.h:

1#定义 FFI_LIB “write.so” 2 3typedef struct _writedata { 4 无效 * buf ; 5 size_t 大小; 6} own_write_data ; 7 8无效 * init ();

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

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

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

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

1<?php 2const CURLOPT_URL = 10002 ; 3const CURLOPT_SSL_VERIFYPEER = 64 ; 4const CURLOPT_WRITEDATA = 10001 ; 5const CURLOPT_WRITEFUNCTION = 20011 ; 6 7$ libcurl = FFI :: load (“ curl.h”; 8$ write = FFI :: load (“ write.h”; 9 10$ url = “ https://www.laruence.com/2020/03/11/5475.html” ; 11 12$ data = $ write- > new (“ own_write_data” ); 13 14$ ch = $ libcurl- > curl_easy_init (); 15 16$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_URL , $ url ); 17$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_SSL_VERIFYPEER0; 18$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_WRITEDATAFFI :: addr ($ data )); 19$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_WRITEFUNCTION , $ write- > init ()); 20$ libcurl- > curl_easy_perform ($ ch ); 21 22$ libcurl- > curl_easy_cleanup ($ ch ); 23 24ret = FFI :: 字符串($ data- > buf , $ data- > size );

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

函数 FFI :: 新(混合$ type  [, bool $ own  = true [, bool $ persistent  = false ]]): FFI \ CData

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

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

静态 函数 地址( FFI \ CData $ cdata ): FFI \ CData ;

然后我们把own_write_func作为WRITEFUNCTION传递给了libcurl,这样再有返回的时候,libcurl就会调用我们的own_write_func来处理返回,同时会把write_data作为自定义参数传递给我们的替代函数。

最后我们使用了FFI :: string来把一段内存转换成PHP的string:

静态 函数 FFI :: 字符串( FFI \ CData $ src  [, int $ size ]):字符串复制代码

当不提供$ size的时候,FFI :: string会在遇到Null-byte的时候停止。

好了,跑一下吧?

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

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

ffi_preload.inc:

1<?php 2FFI :: load (“ curl.h”; 3FFI :: load (“ write.h”;

但我们引用加载的FFI呢?因此我们需要修改一下这俩个.h头文件,加入FFI_SCOPE,比如curl.h:

1#定义 FFI_LIB “libcurl.so” 2#定义 FFI_SCOPE “的libcurl” 3 4无效 * curl_easy_init (); 5int curl_easy_setopt (void * curl , int选项, ...; 6int curl_easy_perform (void * curl ) 7void curl_easy_cleanup (void * handle );

对应的我们给write.h也加入FFI_SCOPE为“ write”,然后我们的脚本现在看起来应该是这样的:

1<?php 2const CURLOPT_URL = 10002 ; 3const CURLOPT_SSL_VERIFYPEER = 64 ; 4const CURLOPT_WRITEDATA = 10001 ; 5const CURLOPT_WRITEFUNCTION = 20011 ; 6 7$ libcurl = FFI :: 范围(“ libcurl” ); 8$ write = FFI :: 范围(“ write” ); 9 10$ url = “ https://www.laruence.com/2020/03/11/5475.html” ; 11 12$ data = $ write- > new (“ own_write_data” ); 13 14$ ch = $ libcurl- > curl_easy_init (); 15 16$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_URL , $ url ); 17$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_SSL_VERIFYPEER0; 18$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_WRITEDATAFFI :: addr ($ data )); 复制代码 19$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_WRITEFUNCTION , $ write- > init ()); 20$ libcurl- > curl_easy_perform ($ ch ); 21 22$ libcurl- > curl_easy_cleanup ($ ch ); 23 24ret = FFI :: 字符串($ data- > buf , $ data- > size );

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

静态 函数 范围(字符串$ name ): FFI ;

然后还有另外一个问题,FFI虽然给了我们很大的规模,但是毕竟直接调用C库函数,还是非常具有风险性的,我们应该只允许用户调用我们确认过的函数,于是,ffi.enable = preload就该上场了,当我们设置ffi.enable = preload的话,那就只有在opcache.preload的脚本中的函数才能调用FFI,而用户写的函数是没有办法直接调用的。

我们稍微修改下ffi_preload.inc变成ffi_safe_preload.inc

1<?php 2CURLOPT{ 3 const URL = 10002 ; 4 const SSL_VERIFYHOST = 81 ; 5 const SSL_VERIFYPEER = 64 ; 6 const WRITEDATA = 10001 ; 7 const WRITEFUNCTION = 20011 ; 8} 9 10FFI :: load (“ curl.h”; 11FFI :: load (“ write.h”; 12 13函数 get_libcurl () : FFI { 14 返回 FFI :: 范围(“ libcurl” ); 15} 16 17函数 get_write_data ($ write ) : FFI \ CData { 18 返回 $ write- > new (“ own_write_data” ); 19} 20 21函数 get_write () : FFI { 22 返回 FFI :: 范围(“ write” ); 23} 24 25函数 get_data_addr ($ data ) : FFI \ CData { 26 返回 FFI :: addr ($ data ); 27} 28 29函数 paser_libcurl_ret ($ data ) :字符串{ 30 返回 FFI :: 字符串($ data- > buf , $ data- > size ); 31}

也就是,我们把所有会调用FFI API的函数都定义在preload脚本中,然后我们的示例会变成(ffi_safe.php):

1<?php 2$ libcurl = get_libcurl (); 3$ write = get_write (); 4$ data = get_write_data ($ write ); 5 6$ url = “ https://www.laruence.com/2020/03/11/5475.html” ; 7 8 9$ ch = $ libcurl- > curl_easy_init (); 10 11$ libcurl- > curl_easy_setopt ($ ch , CURLOPT :: URL , $ url );复制代码 12$ libcurl- > curl_easy_setopt ($ ch , CURLOPT :: SSL_VERIFYPEER0; 13$ libcurl- > curl_easy_setopt ($ ch , CURLOPT :: WRITEDATA , get_data_addr ($ data ));复制代码 14$ libcurl- > curl_easy_setopt ($ ch , CURLOPT :: WRITEFUNCTION , $ write- > init ()); 15$ libcurl- > curl_easy_perform ($ ch ); 16 17$ libcurl- > curl_easy_cleanup ($ ch ); 18 19$ ret = paser_libcurl_ret ($ data );

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

attachments-2020-03-FGVbxdNU5e8162d2985b8.jpg

作者:PHP开源社区
链接:https://juejin.im/post/5e81a015f265da47c35d69fc
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

PHP FFI详解——一种全新的PHP扩展方法 - HelloWorld