(给PHP开发者加星标,提升PHP技能)

转自:laruence/鸟哥

www.laruence.com/2020/03/11/5475.html

随着 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扩展,我们大概会这么写:

<?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呢?

首先我们下载PHP-FFI, 编译安装,PHP-FFI需要PHP-7.4以及libffi-3以上。

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

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

具体到这个例子,我们写一个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" );

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

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

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

<?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函数作为回调函数通过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_LIB的宏,来告诉FFI这些函数来自libcurl.so, 当我们用FFI::load加载这个h文件的时候,PHP FFI就会自动载入libcurl.so, 好,现在整个代码会是:

<?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;}

注意此处的init函数,因为在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);

好了,跑一下吧?

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

ffi.enable=1opcache.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,引用对应的函数。

好了,经过这个例子,大家应该对FFI有了一个比较深入的理解了,有兴趣,就去找一个C库,试试吧?

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

https://github.com/laruence/stashes/tree/master/ffi

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

推荐阅读  点击标题可跳转

PHP 虚拟机 HHVM 3.26 发布,引入全新 HackC 编译器

php 命令行下的常用命令

看完本文有收获?请分享给更多人

关注「PHP开发者」加星标,提升PHP技能

好文章,我在看❤️

php curl https_PHP FFI:一种全新的PHP扩展方式相关推荐

  1. java基础(1)-几种获取类的扩展方式

    摘要 在日常开发过程中经常需要获取类的扩展集.即获取类的子类集(抽象类),或者接口实现类.比如说状态模式中,状态构建类,策略模式中的,策略构造方式.本文介绍几种获取方式. 实现 以策略模式为例 定义了 ...

  2. 9种常见的INTERNET接入方式

    9种常见的INTERNET接入方式 提到接入网,首先要涉及一个带宽问题,随着互联网技术的不断发展和完善,接入网的带宽被人们分为窄带和宽带,业内专家普遍认为宽带接入是未来发展方向. 宽带运营商网络结构如 ...

  3. NeurIPS 2021 | 寻MixTraining: 一种全新的物体检测训练范式

    来源:专知 本文附论文,建议阅读5分钟物体检测是计算机视觉中的基础课题. MixTraining: 一种全新的物体检测训练范式 论文链接: https://www.zhuanzhi.ai/paper/ ...

  4. Nature封面:每天工作21.5小时的AI化学家,8天内完成688个实验,已自主发现一种全新催化剂...

    来源:学术头条 本文约2000字,建议阅读5分钟. 本文为你介绍一款人工智能机器人化学家. 日本现代机器人之父大阪大学教授石黑浩曾经表示,"人类的进化有两种方式,一种是基因进化,还有一种是技 ...

  5. Nature封面:AI 机器人研发出了一种全新的化学催化剂

    来源:学术头条 本文约2262字,建议阅读5分钟. 本文介绍来自利物浦大学的研究人员,成功的开发了一款人工智能机器人化学家.这款机器人化学家可以同时考虑数十个维度的变量,每天工作 21.5 个小时,像 ...

  6. KDD 18 论文解读 | GraphWave:一种全新的无监督网络嵌入方法

    在碎片化阅读充斥眼球的时代,越来越少的人会去关注每篇论文背后的探索和思考. 在这个栏目里,你会快速 get 每篇精选论文的亮点和痛点,时刻紧跟 AI 前沿成果. 点击本文底部的「阅读原文」即刻加入社区 ...

  7. AutoLayout代码布局使用大全—一种全新的布局思想

    相信ios8出来之后,不少的ios程序员为了屏幕的适配而烦恼.相信不少的人都知道有AutoLayout 这么个玩意可以做屏幕适配,事实上,AutoLayout不仅仅只是一个为了多屏幕适配的工具, 它真 ...

  8. 热像仪 二次开发 c++_一种全新的红外热像仪——“可编程红外热像仪”

    如 今,红外热像仪对于很多人而言已经不是一个新鲜事物了,它利用红外探测器对被测目标的红外辐射进行探测,并加以光电转换和信号处理等手段,将被测目标的温度分布转换为我们人眼可以直观识别的图像.受益于这种温 ...

  9. 一种全新的软件界面设计方法

    一种全新的软件界面设计方法 撰文:Aweay 你可转载,拷贝,但必须加入作者署名Aweay,如果用于商业目的,必须经过作者同意. 下载实例代码 关键字:COM MySpy IE SetUIHanlde ...

最新文章

  1. mysql之修改表引擎
  2. 刚上线就报名2000人!8位大牛免费讲座,再不报名就满额了!
  3. java线程主要状态及转换_Java线程状态转换及控制
  4. ds查找—二叉树平衡因子_面试官让我手写一个平衡二叉树,我当时就笑了
  5. ibatis提示Unable to load embedded resource from assembly Entity.Ce_SQL.xml,Entity.
  6. java开发怎么打补丁_[Java教程]【NC】出补丁与打补丁
  7. ubuntu 12.10 php55安装过程
  8. PHP第十次实验总结,The Clean Architecture in PHP 读书笔记(十)
  9. acl在内核里的位置_Windows 注入篇 之 内核 APC 注入
  10. 2009-07-03 19:48 在linux中如何获得微秒精度的时间?-转
  11. 中国古代道家思想与网页重构的思考
  12. SpringBoot使用模板动态导出PDF使用itextpdf
  13. android接入微信登录授权提示{errcode:40125,errmsg:invalid appsecret, view more at 。。。解决办法
  14. 华为S5700交换机设置密码包括telnet密码
  15. 中国越野车和皮卡市场趋势报告、技术动态创新及市场预测
  16. 在VS中怎么用vb画矩形_怎样画颜色绚丽的插画?
  17. Java实训报告----计算图形的周长和面积(保姆级,完整版)
  18. 用spark分析北京积分落户数据,按用户身份证所在省份城市分析
  19. 浮点数的二进制表示方法
  20. 杭电1007 Quoit Design

热门文章

  1. SQL Server 2008处理隐式数据类型转换在执行计划中的增强
  2. 智能对联模型太难完成?华为云ModelArts助你实现!手把手教学
  3. 云图说 | 华为云GPU共享型AI容器,让你用得起,用得好,用的放心
  4. 华为云提供多场景本地数据上云方案,数据上云不再愁
  5. c语言sqlserver进行odbc编程,在VS下用C语言连接SQLServer2008
  6. Spring和SpringMVC整合
  7. 设计模式笔记四:建造者模式
  8. mysql5.7开启二进制日志_MySQL5.7二进制日志
  9. latex常用的公式
  10. 计算图是个什么东西?一大堆的函数用法笔记