PHP7.4에는 매우 유용하다고 생각되는 확장 기능이 제공됩니다: PHP FFI(외부 함수 인터페이스)
, PHP FFI RFC의 설명 인용: 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(<<<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" );
这里有个地方是,文档中写的是返回值是CURL *
,但事实上因为我们的示例中不会解引用它,只是传递,那就避免麻烦就用void *
代替。
然而还有个麻烦的事情是,PHP预定义好了:
로그인 후 복사
好了,定义部分就算完成了,现在我们完成实际逻辑部分,整个下来的代码会是:
<?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의 경우 FFI는 순수 PHP에서 PHP 확장 및 C 라이브러리에 대한 바인딩을 작성하는 방법입니다.예, FFI는 서로 직접 호출할 수 있는 고급 언어를 제공하며, PHP의 경우 FFI를 사용하면 다음으로 작성된 다양한 라이브러리를 편리하게 호출할 수 있습니다. C 언어. 🎜🎜실제로 일부 기존 C 라이브러리, 일반적으로 사용되는
mysqli
, 컬, gettext
등의 패키지인 PHP 확장이 많이 있습니다. PECL 유사한 확장에 그 중 다수가 있습니다. 🎜🎜기존 C 언어 라이브러리의 기능을 사용해야 하는 전통적인 방식에서는 C 언어로 래퍼를 작성하고 이를 확장으로 패키징해야 합니다. 이 과정에서 모든 사람은 물론 PHP 쓰기를 확장하는 방법을 배워야 합니다. , 이제 Zephir
와 같은 몇 가지 편리한 방법이 있습니다. 그러나 여전히 약간의 학습 비용이 있으며 FFI를 사용하면 PHP 스크립트에서 C 언어로 작성된 라이브러리의 함수를 직접 호출할 수 있습니다. 🎜🎜C 언어의 수십 년 역사 동안 우수한 라이브러리가 축적되었으며 FFI를 통해 이 거대한 리소스를 편리하게 즐길 수 있습니다. 🎜🎜다시 본론으로 돌아가서, 오늘은 PHP를 사용하여 libcurl을 호출하여 웹 페이지 콘텐츠를 크롤링하는 방법을 소개하는 예를 사용하겠습니다. 왜 libcurl을 사용합니까? PHP에는 이미 컬 확장 기능이 없나요? 글쎄요, 우선 저는 libcurl의 API에 익숙합니다. 둘째, 바로 그것 때문에 전통적인 확장 방식인 AS와 FFI 방식이 직접적으로 사용하기 더 쉽지 않습니까? 🎜🎜먼저 지금 읽고 있는 기사를 예로 들어보겠습니다. 이제 기사의 내용을 캡처하기 위한 코드를 작성해야 합니다. 기존 PHP 컬 확장을 사용하면 다음과 같이 작성할 것입니다. 🎜 (내 웹사이트가 https이기 때문에 SSL_VERIFYPEER
를 설정하는 작업이 한 번 더 필요합니다.) FFI를 사용하면 어떻게 되나요? 🎜🎜먼저 PHP7.4의 ext/ffi를 활성화하세요. PHP-FFI에는 libffi-3 이상이 필요하다는 점에 유의하세요. 🎜🎜그런 다음 호출하려는 함수의 프로토타입이 무엇인지 PHP FFI에 알려야 합니다. 이를 위해 FFI::cdef
를 사용할 수 있으며 해당 프로토타입은 다음과 같습니다. 🎜<?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);
$cdef
에서 C 언어 함수 선언을 작성할 수 있으며, FFI는 이를 파싱
하고 $lib</ 문자열에서 호출하려고 한다는 것을 이해합니다. code> 라이브러리 함수의 서명은 무엇입니까? 이 예에서는 세 가지 libcurl 함수를 사용합니다. 해당 선언은 libcurl 문서에서 찾을 수 있으며 일부는 <code>curl_easy_init
에 관한 것입니다. 🎜🎜특히 이 예에서는 선언할 모든 내용이 포함된 curl.php
를 작성합니다. 코드는 다음과 같습니다. 🎜#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; }
CURL *
이지만 실제로 이 예에서는 역참조되지 않으므로 그냥 전달된 다음 문제를 방지하려면 대신 void *
를 사용하세요. 🎜🎜그러나 또 다른 귀찮은 점은 PHP가 다음을 미리 정의했다는 것입니다. 🎜#define FFI_LIB "write.so" typedef struct _writedata { void *buf; size_t size; } own_write_data; void *init();
gcc -O2 -fPIC -shared -g write.c -o write.so
curl_setop</ 코드만 호출하면 됩니다. ><code>CURLOPT_RETURNTRANSFER
를 1로 설정하지만 libcurl은 실제로 문자열을 직접 반환하는 기능이 없거나 데이터가 반환될 때 WRITEFUNCTION
에 대한 대체 함수를 제공합니다. libcurl이 호출합니다. 이 함수는 실제로 PHP 컬 확장도 동일한 기능을 수행합니다. 🎜🎜현재 FFI를 통해 libcurl에 추가 함수로 PHP 함수를 직접 전달할 수 없으므로 두 가지 방법이 있습니다. 🎜🎜1 WRITEDATA
를 사용하면 기본 libcurl이 < 코드를 호출합니다. >fwrite는 변수 함수이며 WRITEDATA
를 통해 libcurl에 fd를 제공하여 stdout
에 쓰지 않고 이 fd에 쓰도록 할 수 있습니다 🎜🎜 2. 간단한 함수에 C를 직접 작성하고 FFI 날짜를 전달한 후 libcurl에 전달합니다. 🎜🎜먼저 첫 번째 방법을 사용해 보겠습니다. 먼저 fopen
을 사용해야 합니다. 이번에는 C 헤더 파일(file.h
)을 정의하여 프로토타입을 선언합니다. 🎜 <?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);
file.h
처럼 모든 libcurl 함수 선언을 curl.h
에 넣습니다.function FFI::new(mixed $type [, bool $own = true [, bool $persistent = false]]): FFI\CData
FFI::load</를 사용할 수 있습니다. code>를 사용하여 .h 파일을 로드합니다. 🎜<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">static function addr(FFI\CData $cdata): FFI\CData;</pre><div class="contentsignin">로그인 후 복사</div></div><div class="contentsignin">로그인 후 복사</div></div>🎜하지만 FFI에게 해당 라이브러리를 로드하도록 지시하는 방법은 무엇입니까? 위와 같이 <code>FFI_LIB
매크로를 정의하여 이러한 함수가 libcurl.so
에서 온 것임을 FFI에 알립니다. 이 h 파일을 사용하면 PHP FFI가 자동으로 libcurl.so를 로드합니다.so🎜🎜그렇다면 fopen
이 로딩 라이브러리를 지정할 필요가 없는 이유는 FFI가 변수 기호 테이블에서도 기호를 찾기 때문입니다. code>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 <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; }
注意此处的初始函数,因为在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 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!