随着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 = "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呢?

首先要启用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(<<

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"

);

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

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

const CURLOPT_URL = 10002;

const CURLOPT_SSL_VERIFYPEER = 64;

$libcurl = FFI::cdef(<<

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"

);

好了,定义部分就算完成了,现在我们完成实际逻辑部分,整个下来的代码会是:<?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);

怎么样,比例使用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 *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是一个标准库函数,它早就存在了。

好,现在整个代码会是:<?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

#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

好了,现在整个的代码会变成:<?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的生命周期管理,不需要主动释放,但是有的时候你也可能希望自己管理,那么可以设置$own为flase,那么在适当的时候,你需要调用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》

php7.4 ffi,PHP7.4 全新扩展方式 FFI 详解相关推荐

  1. mcrypt cbc php7,PHP7.1中使用openssl替换mcrypt的实例详解

    在php开发中,使用mcrypt相关函数可以很方便地进行AES加.解密操作,但是PHP7.1中废弃了mcrypt扩展,所以必需寻找另一种实现.在迁移手册中已经指出了用openssl代替mcrypt,但 ...

  2. python源程序文件的扩展名_python程序文件扩展名知识点详解

    python程序文件的扩展名称是什么 python程序的扩展名有.py..pyc..pyo和.pyd..py是源文件,.pyc是源文件编译后的文件,.pyo是源文件优化编译后的文件,.pyd是其他语言 ...

  3. python打开文件的语法_python27语法Python文件打开方式实例详解【a、a+、r+、w+区别】...

    本文实例讲述了Python文件打开方式.分享给大家供大家参考,具体如下: 第一步 排除文件打开方式错误: r只读,r+读写,不创建 w新建只写,w+新建读写,二者都会将文件内容清零 (以w方式打开,不 ...

  4. JAVA classpath设置方式启动详解:java class名、java -jar

    java classpath设置方式启动详解:java className.java -jar 文章目录 java classpath设置方式启动详解:java className.java -jar ...

  5. PHP扩展代码结构详解

    PHP扩展代码结构详解 : 这个是继: 使用ext_skel和phpize构建php5扩展  内容 (拆分出来) Zend_API:深入_PHP_内核:http://cn2.php.net/manua ...

  6. java的websocket_java 实现websocket的两种方式实例详解

    一.介绍 1.两种方式,一种使用tomcat的websocket实现,一种使用spring的websocket 2.tomcat的方式需要tomcat 7.x,JEE7的支持. 3.spring与we ...

  7. Vue通信、传值的多种方式,详解

    Vue通信.传值的多种方式,详解 转自:https://blog.csdn.net/qq_35430000/article/details/79291287 一.通过路由带参数进行传值 ①两个组件 A ...

  8. python 编码解码原理_Python JSON编解码方式原理详解

    这篇文章主要介绍了Python JSON编解码方式原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 概念 JSON(JavaScript Ob ...

  9. linux 帐号 配置sftp_Linux 下sftp配置之密钥方式登录详解

    Linux下sftp配置之密钥方式登录 由于vsftp采用明文传输,用户名密码可通过抓包得到,为了安全性,需使用sftp,锁定目录且不允许sftp用户登到服务器.由于sftp使用的是ssh协议,需保证 ...

  10. 详解python运行三种方式_详解python运行三种方式

    方式一 交互式编程 交互式编程不需要创建脚本文件,是通过 Python 解释器的交互模式进来编写代码. linux上你只需要在命令行中输入 Python 命令即可启动交互式编程,提示窗口如下: $ p ...

最新文章

  1. Spring整合Disruptor3
  2. 初探Object Pascal的类(三)
  3. php异步方案,PHP的异步实现方式
  4. redis internal【转】
  5. MongoDB、ElasticSearch、Redis、HBase比较
  6. ubuntu 14.04 修改PS1提示符
  7. 如何去掉自动弹出IE9介绍页
  8. 标准模块 threading
  9. 2018年的人工智能将如何发展?看看专家怎么说
  10. 调研AutoGluon数据处理与Tabular-NN
  11. for循环中控制事务单个提交问题
  12. C语言实现任何文件的加密解密
  13. mysql视图唯一id_Mysql的视图、存储过程、函数、索引全解析
  14. Winfrom 常用控件(二)
  15. 网络狂飙2(netspeeder2) v2.3 简体中文版 怎么用
  16. 模具冲压与模具设计知识点
  17. RSA分段加密分段解密以及密钥与.Net的转化
  18. r语言remarkdown展示图_十个超级好用的R语言编程技巧,一般人绝不知道!
  19. 微信客服为什么不能人工服务器,微信客服消息格式限制及功能
  20. Elasticsearch 如何实现时间差查询?

热门文章

  1. aws ecs 通过efs挂载实现动态更新firelens日志配置
  2. 玩转aws之(二)eks 设置efs(nfs)存储卷pvc
  3. 查准率(Precision),灵敏性(Sensitivity),特异性(Specificity),F1,PR曲线,ROC,AUC
  4. Mapped Statements collection does not contain value for错误可能
  5. Spring之Introductions(简介)应用
  6. 关于Three.js 加载地面贴图的坑
  7. android 设置软键盘高度,Android 软键盘一招搞定(实践篇)
  8. 服务器尚未完成维护梦幻西游,梦幻西游维护公告
  9. 钓鱼网站与反钓鱼技术剖析(圆桌会议)
  10. Minecraft mod制作简易教程(三)——创建一个物品