随着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语言写wrapper,把他们包装成扩展,这个过程中就需要大家去学习PHP的扩展怎么写,当然现在也有一些方便的方式,比如Zephir. 但总还是有一些学习成本的,而有了FFI以后,我们就可以直接在PHP脚本中调用C语言写的库中的函数了。

而C语言几十年的历史中,积累了大量的优秀的库,FFI直接让我们可以方便的享受这个庞大的资源了。

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

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

  1. <?php
  2. $url = "https://www.laruence.com/2020/03/11/5475.html";
  3. $ch = curl_init();
  4. curl_setopt($ch, CURLOPT_URL, $url);
  5. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
  6. curl_exec($ch);
  7. curl_close($ch);

(因为我的网站是https的,所以会多一个设置SSL_VERIFYPEER的操作)那如果是用FFI呢?

首先要启用PHP7.4的ext/ffi,需要注意的是PHP-FFI要求libffi-3以上。

然后,我们需要告诉PHP FFI我们要调用的函数原型是咋样的,这个我们可以使用FFI::cdef, 它的原型是:

  1. FFI::cdef([string $cdef = "" [, string $lib = null]]): FFI

在string $cdef中,我们可以写C语言函数式申明,FFI会parse它,了解到我们要在string $lib这个库中调用的函数的签名是啥样的,在这个例子中,我们用到三个libcurl的函数,它们的申明我们都可以在libcurl的文档里找到,比如对于curl_easy_init.

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

  1. $libcurl = FFI::cdef(<<<CTYPE
  2. void *curl_easy_init();
  3. int curl_easy_setopt(void *curl, int option, ...);
  4. int curl_easy_perform(void *curl);
  5. void curl_easy_cleanup(void *handle);
  6. CTYPE
  7. , "libcurl.so"
  8. );

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

然而还有个麻烦的事情是,PHP预定义好了CURLOPT_等option的值,但现在我们需要自己定义,简单的办法就是查看curl的头文件,找到对应的值,然后我们把值给加进去:

  1. <?php
  2. const CURLOPT_URL = 10002;
  3. const CURLOPT_SSL_VERIFYPEER = 64;
  4. $libcurl = FFI::cdef(<<<CTYPE
  5. void *curl_easy_init();
  6. int curl_easy_setopt(void *curl, int option, ...);
  7. int curl_easy_perform(void *curl);
  8. void curl_easy_cleanup(void *handle);
  9. CTYPE
  10. , "libcurl.so"
  11. );

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

  1. <?php
  2. require "curl.php";
  3. $url = "https://www.laruence.com/2020/03/11/5475.html";
  4. $ch = $libcurl->curl_easy_init();
  5. $libcurl->curl_easy_setopt($ch, CURLOPT_URL, $url);
  6. $libcurl->curl_easy_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
  7. $libcurl->curl_easy_perform($ch);
  8. $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):

  1. void *fopen(char *filename, char *mode);
  2. void fclose(void * fp);

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

  1. #define FFI_LIB "libcurl.so"
  2. void *curl_easy_init();
  3. int curl_easy_setopt(void *curl, int option, ...);
  4. int curl_easy_perform(void *curl);
  5. void curl_easy_cleanup(CURL *handle);

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

  1. static function load(string $filename): FFI;

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

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

好,现在整个代码会是:

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

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

  1. #include <stdlib.h>
  2. #include <string.h>
  3. #include "write.h"
  4. size_t own_writefunc(void *ptr, size_t size, size_t nmember, void *data) {
  5. own_write_data *d = (own_write_data*)data;
  6. size_t total = size * nmember;
  7. if (d->buf == NULL) {
  8. d->buf = malloc(total);
  9. if (d->buf == NULL) {
  10. return 0;
  11. }
  12. d->size = total;
  13. memcpy(d->buf, ptr, total);
  14. } else {
  15. d->buf = realloc(d->buf, d->size + total);
  16. if (d->buf == NULL) {
  17. return 0;
  18. }
  19. memcpy(d->buf + d->size, ptr, total);
  20. d->size += total;
  21. }
  22. return total;
  23. }
  24. void * init() {
  25. return &own_writefunc;
  26. }

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

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

  1. #define FFI_LIB "write.so"
  2. typedef struct _writedata {
  3. void *buf;
  4. size_t size;
  5. } own_write_data;
  6. void *init();

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

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

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

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

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

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

  1. 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的实际内存地址:

  1. static function addr(FFI\CData $cdata): FFI\CData;

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

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

  1. static function FFI::string(FFI\CData $src [, int $size]): string

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

好了,跑一下吧?

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

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

ffi_preload.inc:

  1. <?php
  2. FFI::load("curl.h");
  3. FFI::load("write.h");

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

  1. #define FFI_LIB "libcurl.so"
  2. #define FFI_SCOPE "libcurl"
  3. void *curl_easy_init();
  4. int curl_easy_setopt(void *curl, int option, ...);
  5. int curl_easy_perform(void *curl);
  6. void curl_easy_cleanup(void *handle);

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

  1. <?php
  2. const CURLOPT_URL = 10002;
  3. const CURLOPT_SSL_VERIFYPEER = 64;
  4. const CURLOPT_WRITEDATA = 10001;
  5. const CURLOPT_WRITEFUNCTION = 20011;
  6. $libcurl = FFI::scope("libcurl");
  7. $write = FFI::scope("write");
  8. $url = "https://www.laruence.com/2020/03/11/5475.html";
  9. $data = $write->new("own_write_data");
  10. $ch = $libcurl->curl_easy_init();
  11. $libcurl->curl_easy_setopt($ch, CURLOPT_URL, $url);
  12. $libcurl->curl_easy_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
  13. $libcurl->curl_easy_setopt($ch, CURLOPT_WRITEDATA, FFI::addr($data));
  14. $libcurl->curl_easy_setopt($ch, CURLOPT_WRITEFUNCTION, $write->init());
  15. $libcurl->curl_easy_perform($ch);
  16. $libcurl->curl_easy_cleanup($ch);
  17. ret = FFI::string($data->buf, $data->size);

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

  1. static function scope(string $name): FFI;

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

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

  1. <?php
  2. class CURLOPT {
  3. const URL = 10002;
  4. const SSL_VERIFYHOST = 81;
  5. const SSL_VERIFYPEER = 64;
  6. const WRITEDATA = 10001;
  7. const WRITEFUNCTION = 20011;
  8. }
  9. FFI::load("curl.h");
  10. FFI::load("write.h");
  11. function get_libcurl() : FFI {
  12. return FFI::scope("libcurl");
  13. }
  14. function get_write_data($write) : FFI\CData {
  15. return $write->new("own_write_data");
  16. }
  17. function get_write() : FFI {
  18. return FFI::scope("write");
  19. }
  20. function get_data_addr($data) : FFI\CData {
  21. return FFI::addr($data);
  22. }
  23. function paser_libcurl_ret($data) :string{
  24. return FFI::string($data->buf, $data->size);
  25. }

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

  1. <?php
  2. $libcurl = get_libcurl();
  3. $write = get_write();
  4. $data = get_write_data($write);
  5. $url = "https://www.laruence.com/2020/03/11/5475.html";
  6. $ch = $libcurl->curl_easy_init();
  7. $libcurl->curl_easy_setopt($ch, CURLOPT::URL, $url);
  8. $libcurl->curl_easy_setopt($ch, CURLOPT::SSL_VERIFYPEER, 0);
  9. $libcurl->curl_easy_setopt($ch, CURLOPT::WRITEDATA, get_data_addr($data));
  10. $libcurl->curl_easy_setopt($ch, CURLOPT::WRITEFUNCTION, $write->init());
  11. $libcurl->curl_easy_perform($ch);
  12. $libcurl->curl_easy_cleanup($ch);
  13. $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、

文章转载自鸟哥博客:https://www.laruence.com/2020/03/11/5475.html

[转]PHP FFI详解 - 一种全新的PHP扩展方式相关推荐

  1. php curl https_PHP FFI:一种全新的PHP扩展方式

    (给PHP开发者加星标,提升PHP技能) 转自:laruence/鸟哥 www.laruence.com/2020/03/11/5475.html 随着 PHP7.4 而来的有一个我认为非常有用的一个 ...

  2. java实现线程三种方式_详解三种java实现多线程的方式

    java中实现多线程的方法有两种:继承Thread类和实现runnable接口. 1.继承Thread类,重写父类run()方法 public class thread1 extends Thread ...

  3. 【python教程入门学习】Python函数定义及传参方式详解(4种)

    这篇文章主要介绍了Python函数定义及传参方式详解(4种),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧 一.函数初识 1.定 ...

  4. C语言中三个数比较大小详解——三种方法

    ​ C语言中三个数比较大小详解--三种方法 方法一:if-else法 方法二:函数法 方法三:三目运算符法 C语言中比较三个数的大小有很多方法,以下是我总结的三种方法: 首先我定义 int a = 1 ...

  5. 详解23种设计模式(基于Java)—— 结构型模式(三 / 五)

    目录 3.结构型模式(7种) 3.1.代理模式 3.1.1.概述 3.1.2.结构 3.1.3.静态代理 3.1.4.JDK动态代理 3.1.5.CGLIB动态代理 3.1.6.三种代理的对比 3.1 ...

  6. 详解5种红黑树的场景,从Linux内核谈到Nginx源码,听完醍醐灌顶丨Linux服务器开发丨Linux后端开发

    5种红黑树的场景,从Linux内核谈到Nginx源码,听完醍醐灌顶 1. 进程调度CFS的红黑树场景 2. 虚拟内存管理的红黑树场景 3. 共享内存slab的红黑树场景 视频讲解如下,点击观看: [干 ...

  7. 图文详解两种算法:深度优先遍历(DFS)和广度优先遍历(BFS)

    图文详解两种算法:深度优先遍历(DFS)和广度优先遍历(BFS) 阅读本文前,请确保你已经掌握了递归.栈和队列的基本知识,如想掌握搜索的代码实现,请确保你能够用代码实现栈和队列的基本操作. 深度优先遍 ...

  8. Java 枚举(1): 详解7种常见的用法

    目录 用法一:常量 用法二:switch 用法三:向枚举中添加新方法 用法四:覆盖枚举的方法 用法五:实现接口 用法六:使用接口组织枚举 用法七:关于枚举集合的使用 JDK1.5引入了新的类型--枚举 ...

  9. 由Http Post提交遇到的一个坑,深入详解4种Post发送数据编码方式

    由Http Post提交遇到的一个坑,深入详解4种Post发送数据编码方式 阅读引导: 1.Http post的4中提交方式详解 2.遇到的一个较深的坑,以及解决思路. 最近团队的某个项目遇到一个诡异 ...

  10. java webmethod 参数_java详解Spring接收web请求参数的方式

    本篇文章给大家带来的内容是java详解Spring接收web请求参数的方式 .有一定的参考价值,有需要的朋友可以参考一下,希望对你们有所帮助. 1 查询参数 请求格式:url?参数1=值1&参 ...

最新文章

  1. Python的range()函数
  2. 人工智能应用于建筑领域新前沿
  3. null?对象?异常?到底应该如何返回错误信息
  4. 使用request对象进行数据传递
  5. P3067 [USACO12OPEN]Balanced Cow Subsets G 折半搜索
  6. mysql 组合查询_MySQL组合查询教程
  7. java 代码结构_代码结构说明
  8. java jar 启动项目,SpringBoot项目运行jar包启动的步骤流程解析
  9. Java基础教程【第五章:Java数组】
  10. HTTP协议请求类型响应简介
  11. jsp乱码介绍(续)
  12. 凸优化第四章凸优化问题 4.6 广义不等式约束
  13. oracle中varchar2和nvarchar2的区别
  14. detectron2训练自己的数据集_从零教你训练自己的数据集实现汽车标志识别,汽车品牌识别源码
  15. 网络安全学习第14篇 - 游戏(仙剑奇侠传95)外挂之修改游戏资源文件(修改人物属性)
  16. TikTok和抖音差别大吗?
  17. 数组遍历,forEach(),ever(),some()
  18. 如何获取微信公众号的关注链接?
  19. 线上科技展厅vr全景展厅设计 广交会布展
  20. linux 环境命令行导出dmp文件

热门文章

  1. JSP解决:Attempt to clear a buffer that#39;s already been flushed错误(jsp:forward标签跳转空白)...
  2. IT服务台方案:提供完整的业务流程视图
  3. 快速清除系统中的木马病毒
  4. tf.shape用法
  5. Mysql之auto_increment(自动增长)
  6. Unity发布抖音小游戏:申请注册(包括主体认证和基础信息完善)
  7. Windows 窗口层次关系及窗口层次说明
  8. Python学习教程公开课:好玩的Python
  9. 计算机专业在哪个城市就业好,计算机专业在哪几个城市就业好点?
  10. Win7操作系统: 请求的操作需要提升