- 本文地址: https://www.laruence.com/2020/03/11/5475.html
- 转载请注明出处
随着PHP7.4而来的有一个我认为很是有用的一个扩展:PHP FFI(Foreign Function interface),引用一段PHP FFI RFC中的一段描述:php
对于PHP,FFI提供了一种在纯PHP中编写PHP扩展和对C库的绑定的方法。
是的,FFI提供了高级语言直接的互相调用,而对于PHP而言,FFI让咱们能够方便的调用C语言写的各类库。html
其实现有大量的PHP扩展是对一些已有的C库的包装,某些经常使用的mysqli,curl,gettext等,PECL中也有大量的相似扩展。mysql
传统的方式,当咱们须要用一些已有的C语言的库的能力的时候,咱们须要用C语言写包装器,把他们包装成扩展,这个过程当中就须要你们去学习PHP的扩展怎么写,固然如今也有一些方便的方式,某种Zephir。但总仍是有一些学习成本的,而有了FFI以后,咱们就能够直接在PHP脚本中调用C语言写的库中的函数了。laravel
而C语言几十年的历史中,积累积累的优秀的库,FFI直接让咱们能够方便的享受这个庞大的资源了。git
言归正传,今天我用一个例子来介绍,咱们如何使用PHP来调用libcurl,来抓取一个网页的内容,为何要用libcurl呢?PHP不是已经有了curl扩展了么?嗯,首先由于libcurl的api我比较熟,其次呢,正是由于有了,才好对比,传统扩展方式和FFI方式直接的易用性不是?github
首先,某些咱们就拿当前你看的这篇文章为例,我如今须要写一段代码来抓取它的内容,若是用传统的PHP的curl扩展,咱们大概会这么写:sql
<?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呢?shell
首先要启用PHP7.4的ext / ffi,须要注意的是PHP-FFI要求libffi-3以上。api
而后,咱们须要告诉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 无效* curl_easy_init (); int curl_easy_setopt ( void * curl , int选项, ...); int curl_easy_perform ( void * curl ); void curl_easy_cleanup ( void * handle ); 类型 , “ libcurl.so” );
这里有个地方是,文档中写的是返回值是CURL *,但事实上由于咱们的示例中不会解引用它,只是传递,那就避免麻烦就用void *代替。
然而还有个麻烦的事情是,PHP预约义好了:
<?php const CURLOPT_URL = 10002 ; const CURLOPT_SSL_VERIFYPEER = 64 ; $ libcurl = FFI :: cdef (<<< CTYPE 无效* curl_easy_init (); int curl_easy_setopt ( void * curl , int选项, ...); int curl_easy_perform ( void * curl ); void curl_easy_cleanup ( void * handle ); 类型 , “ libcurl.so” );
好了,定义部分就算完成了,如今咱们完成实际逻辑部分,整个下来的代码会是:
<?php 须要 “ 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_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):
void * fopen ( char *文件名, char *模式); void fclose ( void * fp );
像file.h同样,咱们把全部的libcurl的函数申明也放到curl.h中去
#定义 FFI_LIB “libcurl.so” 无效 * curl_easy_init (); int curl_easy_setopt (void * curl , int选项, ...); int curl_easy_perform (void * curl ); void 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是一个标准库函数,它早就存在了。
好,如今整个代码会是:
<?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 *)数据; size_t total =大小* nmember ; 若是 ( d- > buf == NULL ) { d- > buf = malloc ( total ); 若是 ( d- > buf == NULL ) { 返回 0 ; } d- > size = total ; memcpy ( d- > buf , ptr , total ); } 其余 { d- > buf =从新 分配( d- > buf , d- > size + total ); 若是 ( d- > buf == NULL ) { 返回 0 ; } memcpy ( d- > buf + d- > size , ptr , total ); d- > size + = total ; } 回报总额; } 无效 * init () { return & own_writefunc ; }
注意此处的初始函数,由于在PHP FFI中,就目前的版本(2020-03-11)咱们没有办法直接得到一个函数指针,因此咱们定义了这个函数,返回own_writefunc的地址。
最后咱们定义上面用到的头文件write.h:
#定义 FFI_LIB “write.so” typedef struct _writedata { 无效 * buf ; size_t 大小; } own_write_data ; 无效 * 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 :: 字符串($ 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启动的时候就加载好:
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:
#定义 FFI_LIB “libcurl.so” #定义 FFI_SCOPE “的libcurl” 无效 * curl_easy_init (); int curl_easy_setopt (void * curl , int选项, ...); 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 :: 范围(“ libcurl” ); $ write = FFI :: 范围(“ 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 :: 字符串($ 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
<?php 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” ); 函数 get_libcurl () : FFI { 返回 FFI :: 范围(“ libcurl” ); } 函数 get_write_data ($ write ) : FFI \ CData { 返回 $ write- > new (“ own_write_data” ); } 函数 get_write () : FFI { 返回 FFI :: 范围(“ write” ); } 函数 get_data_addr ($ data ) : FFI \ CData { 返回 FFI :: addr ($ data ); } 函数 paser_libcurl_ret ($ data ) :字符串{ 返回 FFI :: 字符串($ 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崩溃,因此要当心。
以上内容但愿帮助到你们,不少PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提高,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货须要的能够免费分享给你们,须要请戳这里