PHP7.4 には、非常に便利だと思う拡張機能が付属しています: PHP FFI (Foreign Function Interface)
、PHP FFI RFC の説明を引用:
For PHP、FFI は、純粋な PHP で PHP 拡張機能と C ライブラリへのバインディングを作成する方法を開きます。
はい、FFI は相互に直接呼び出すための高水準言語を提供しますが、PHP の場合は言い換えれば、 , FFIを使うとC言語で書かれた様々なライブラリを簡単に呼び出すことができます。
実際には、既存の C ライブラリのパッケージである PHP 拡張機能が多数あり、一般的に使用される mysqli
、curl、gettext
などは次のとおりです。 PECL にも含まれており、同様の拡張機能が多数あります。
従来の方法では、既存の C 言語ライブラリの機能を使用する必要がある場合、C 言語でラッパーを作成し、拡張機能にパッケージ化する必要があります。このプロセスでは、全員が PHP 拡張機能の書き方を学ぶ必要があります。 ? もちろん、今では Zephir
のような便利な方法がいくつかあります。ただし、学習コストはまだかかります。FFI を使用すると、PHP スクリプトで C 言語で書かれたライブラリの関数を直接呼び出すことができます。
C 言語の数十年の歴史の中で、優れたライブラリが蓄積されてきましたが、FFI を使用すると、この膨大なリソースを直接便利に楽しむことができます。
本題に戻りますが、今日は例を使用して、PHP を使用して libcurl を呼び出し、Web ページのコンテンツをクロールする方法を紹介します。 PHP にはすでにカール拡張機能がありませんか?そうですね、まず libcurl の API に精通していることと、それがあるからこそ比較しやすいということ、従来の展開方式である AS 方式や FFI 方式のほうが直接使いやすいのではないか?
まず第一に、あなたが読んでいる現在の記事を例として考えてみましょう。次に、そのコンテンツをキャプチャするコードを書く必要があります。従来の PHP のカール拡張機能を使用する場合は、おそらく次のように書くでしょう。 this :
<?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);
(私の Web サイトは https であるため、SSL_VERIFYPEER
を設定する操作がもう 1 つあります) FFI を使用するとどうなりますか?
まず、PHP7.4 の ext/ffi を有効にします。PHP-FFI には libffi-3 以降が必要であることに注意してください。
次に、呼び出したい関数のプロトタイプがどのようなものかを PHP FFI に伝える必要があります。FFI::cdef
を使用でき、そのプロトタイプは次のとおりです:
FFI::cdef([string $cdef = "" [, string $lib = null]]): FFI
文字列 $cdef
に C 言語の関数宣言を書くことができます。FFI はそれを 解析して、これを文字列
$ で使用したいことを理解します。 lib ライブラリで呼び出される関数のシグネチャは何ですか? この例では、3 つの 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 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_setopCURLOPT_RETURNTRANSFER
を 1 に設定しますが、libcurl には文字列を直接返す機能がないか、WRITEFUNCTION
の代替関数が提供されています。はこの関数を呼び出しますが、実際には、PHP Curl 拡張機能も同じことを行います。 現在、追加関数として PHP 関数を FFI 経由で libcurl に直接渡すことはできませんが、これを行うには 2 つの方法があります:
1.
WRITEDATA を使用します。デフォルトの libcurl は、変数関数として fwrite
を呼び出します。そして、WRITEDATA
を通じて libcurl に fd を与えることができます。これにより、stdout
を書き込むのではなく、Enter this を書き込むことができます。 fd2. 単純な C 関数を自分たちで作成し、FFI 日付を渡して libcurl に渡します。
まず最初のメソッドを使用しましょう。まず
fopen を使用する必要があります。今回は、C ヘッダー ファイル (file.h
) を定義してプロトタイプを宣言します。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">void *fopen(char *filename, char *mode);
void fclose(void * fp);</pre><div class="contentsignin">ログイン後にコピー</div></div>
と同様に、すべての 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);
static function load(string $filename): FFI;
しかし、FFI に対応するライブラリをロードするように指示するにはどうすればよいでしょうか?上記のように、FFI_LIB
マクロを定義することで、これらの関数が
から来ていることを FFI に伝えます。 , 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 <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 中国語 Web サイトの他の関連記事を参照してください。