首页 > 作文

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

更新时间:2023-04-08 13:24:16 阅读: 评论:0

随着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来调用libcurl,来抓取一个网页的内容,为什么要用libcurl呢?php不是已经有了curl扩展了么?嗯,首先因为libcurl的api我比较熟,其次呢,正是因为有了,才好对比,传统扩展方式as和ffi方式直接的易用性不是?

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

<?php  $url = "/d/file/titlepic/5475.html$ch = curl_init();  curl_topt($ch, curlopt_url, $url);curl_topt($ch, curlopt_ssl_verifypeer, 0);  curl_exec($ch);  curl_clo($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会par它,了解到我们要在字符串$lib这个库中调用的函数的签名是啥样的,在这个例子中,我们用到三一个libcurl的函数,它们的申明我们都可以在libcurl的文档里找到,某些关于curl_easy_init

具体到这个例子,我们写一个curl.php,包含所有要申明的东西,代码如下:

$libcurl = ffi::cdef(<<<ctypevoid *curl_easy_init();int curl_easy_topt(void *curl, int option, ...);int curl_easy_perform(void *curl);void curl_easy_cleanup(void *handle);ctype , "libcurl.so" );

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

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

<?phpconst curlopt_url = 10002;const curlopt_ssl_verifypeer = 64;  $libcurl = ffi::cdef(<<<ctypevoid *curl_easy_init();int curl_easy_topt(void *curl, int option, ...);int curl_easy_perform(void *curl);void curl_easy_cleanup(void *handle);ctype , "libcurl.so" );

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

<?ph朋友圈日常文案prequire "curl.php";  $url = "/d/file/titlepic/5475.html  $ch = $libcurl->curl_easy_init();$libcurl->curl_easy_topt($ch, curlopt_url, $url);$libcurl->curl_easy_topt($ch, curlopt_ssl_verifypeer, 0);  $libcurl->curl_easy_perform($ch);  $libcurl->curl_easy_cleanup($ch);

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

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

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

#define ffi_lib "libcurl.so"  void *curl_easy_init();int curl_easy_topt(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是一个标准库函数,它早就存在了。

好,现在整个代码会是:

<?phpconst curlopt_url = 10002;const curlopt_ssl_verifypeer = 64;const curlopt_writedata = 10001;  $libc = ffi::load("file.h");$libcurl = ffi::load("curl.h");  $url = "/d/file/titlepic/5475.html$tmpfile = "/tmp/tmpfile.out";  $ch = $libcurl->curl_easy_init();$fp = $libc->fopen($tmpfile, "a");  $libcurl->curl_easy_topt($ch, curlopt_url, $url);$libcurl->curl_easy_topt($ch, curlopt_ssl_verifypeer, 0);$libcurl->curl_easy_topt($ch, curlopt_writedata, $fp);$libcurl->curl_easy_perform($ch);  $libcurl->curl_easy_cleanup($ch);  $libc->fclo($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); } el { 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

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

<?phpconst 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 = "/d/file/titlepic/5475.html  $data = $write->new("own_write_data");  $ch = $libcurl->curl_easy_init();  $libcurl->curl_easy_topt($ch, curlopt_url, $url);$libcurl->curl_easy_topt($ch, curlopt_ssl_verifypeer, 0);$libcurl->curl_easy_topt($ch, curlopt_writedata, ffi::addr($data));$libcurl->curl_easy_topt($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 = fal]]): ffi\cdata 

$own表示这个内存管理是否采用php的内存管理,有时的情况下,我们申请的内存会经过php的生命周期管理,不需要主动释放,但是有的时候你也可能希望自己管理,那么可以设置$ownfla,那么在适当的时候,你需要调用ff五岳位置i :: 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=1opcache.preload=ffi_preload.incffi_preload.inc:<?phpffi::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_topt(void *curl, int option, ...);int curl_easy_perform(void *curl);void curl_easy_cleanup(void *handle);

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

<?phpconst 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 = "/d/file/titlepic/5475.html  $data = $write->new("own_write_data");  $ch = $libcurl->curl_easy_init();  $libcurl->curl_easy_topt($ch, curlopt_url, $url);$libcurl->curl_easy_topt($ch, curlopt_ssl_verifypeer, 0);$libcurl->curl_easy_topt($ch, curlopt_writedata, ffi::addr($data));$libcurl->curl_easy_topt($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,引用对应的函数。

staticfunctionscope(string $name): ffi;

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

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

<?phpclass 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");}  func超市手推车广告tion 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 par_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 = "/d/file/titlepic/5475.html    $ch = $libcurl->curl_easy_init();  $libcurl->curl_easy_topt($ch, curlopt::url, $url);$libcurl->curl_easy_topt($ch, curlopt::ssl_verifypeer, 0);$libcurl->curl_easy_topt($ch, curlopt::writedata, get_data_addr($data));$libcurl->curl_easy_topt($ch, curlopt::writefunction, $write->init());$libcurl->curl_easy_perform($ch);  $libcurl->curl_easy_cleanup($ch);  $ret = par_libcurl_ret($data); 

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

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

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

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

本文发布于:2023-04-08 13:24:13,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/zuowen/814a5344ffdbed5b23a7afda4997dd98.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

本文word下载地址:PHP7.4 全新扩展方式 FFI 详解.doc

本文 PDF 下载地址:PHP7.4 全新扩展方式 FFI 详解.pdf

标签:函数   方式   好了   加载
相关文章
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图