关闭

PHP FFI - 一种全新的PHP扩展方式

时间: 2020-02-10阅读: 522标签: php

随着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(<<<CTYPE
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(<<<CTYPE
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 <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);
        } 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=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,引用对应的函数。

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

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

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

站长推荐

1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

2.广告联盟: 整理了目前主流的广告联盟平台,如果你有流量,可以作为参考选择适合你的平台点击进入

链接: http://www.fly63.com/article/detial/8177

关闭

php 定时任务

google百度了下,PHP任务大体上可以分为三类,最近需要去定时请求数据,然后分析之后 指定相应的文本 通过socket广播给用户。具体的分析 制定文本的业务 不复杂。 使用curl 请求数据 。但是对于定时任务这一块怎么使用都不行。

高级PHP工程师所应该具备一些技能

很多面试,很多人员能力要求都有“PHP高级工程师的字眼”,如果您真心喜欢PHP,并且您刚起步,那么我简单说说一个PHP高级工程师所应该具备的,希望给初级或已经达到中级的PHP工程师一些帮助。

php中0,空,null和false的区别

null为不存在之意:php底层的zval空间里(结构见下方)没有存其value值,只存储了一个type标志其 IS_NULL(所以解释了 empty(null)=true,isset(null)=false ,isset(‘‘)=true)

PHP事务是什么?

不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。

php中Url几个常用的函数

parse_url()本函数解析一个 URL 并返回一个关联数组,包含在 URL 中出现的各种组成部分。本函数不是用来验证给定 URL 的合法性的,只是将其分解为下面列出的部分。

浅谈PHP中pack、unpack的详细用法

PHP中有两个函数pack和unpack,很多PHPer在实际项目中从来没有使用过,甚至也不知道这两个方法是用来干嘛的。这篇文章来为大家介绍一下它俩到底是用来干啥的。

PHP的高效率写法

尽量静态化;如果一个方法能被静态,那就声明它为静态的,速度可提高1/4,甚至我测试的时候,这个提高了近三倍。echo的效率高于print,因为echo没有返回值,print返回一个整型;在循环之前设置循环的最大次数,而非在在循环中;

php后台运行以及定时任务的4种实现原理以及代码

后台任务在我们php编程中虽然用的不是很多甚至很多php程序员都没听过甚至觉得后台运行是不可能实现的,本人因为项目需求多次演变在这里分享给大家:写成网页浏览的形式打开即执行然后用http监控

5分钟理解依赖注入和控制反转

很多人不理解依赖注入和控制反转主要是不熟悉适用场景,其实在很多框架中我们说用到了 IoC 和 DI,实际上都是一回事,他实际上就是我们设计模式的一种:门面模式,也称外观模式。实际上,在去理解 依赖注入 和 控制反转 时,我们需要有两个概念。

php中isset() 和 empty() 的区别

很多人只想着高深的技术,却连基础知识储备都不过关!一个简单的问题都能被问的发怵,简直可笑!对!说的就是我自己!接下来会一直坚持做一些简单的知识总结。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!