前回は、PHP-FFIを使ってモンテカルロ法による円周率の計算が高速化されることを試しました。今回は、FFIが一体どのようにしてネイティブコードを実行しているのかを見ていきます。
PHP自体の実行内容を追うために、gdbを使います。
調査時に使ったソースコード
ffi.php
1 |
|
Cの共有ライブラリ
1 |
|
デバック実行
php-buildでインストールしたPHP7.4のphp-cliをgdbを使って動かします。下記はgdbを起動すると共に、breakpointを4つ設定しています。最後にffi.phpをコールしてデバッグ実行開始。
1 | $ sudo gdb --command ~/php-srcs/source/7.4snapshot_debug/.gdbinit ~/.phpenv/versions/7.4snapshot_debug/bin/php |
実行されるPHP-FFIのソースコード解説
PHPソースコードのext/ffi/ffi.c
のソースについてのみ解説します。その他のコードまでは追ってられなかったので…
- 共有ライブラリの読込
ffi.php
の2行目、共有ライブラリを読み込んで、実行したい関数のシグネーチャーを指定しています。
1 | ZEND_METHOD(FFI, cdef) |
この関数内で、DL_LOAD
というマクロに共有ライブラリのパスを渡しています。DL_LOAD
の中身は実行しているOSにも依るのですが、Unix系であればdlopen
、WindowsならばLoadLibrary
です。
共有ライブラリの読み込みが終わると、実行したい関数のアドレスを取得しています。
1 | addr = DL_FETCH_SYMBOL(handle, ZSTR_VAL(name)); |
DL_FETCH_SYMBOL
マクロに、共有ライブラリのハンドルと関数名を渡しています。マクロの中身は、Unix系の場合はdlsym
、Windowsの場合はGetProcAddress
です。
- 共有ライブラリの関数実行
ffi_prep_cif
とffi_call
の2つのシステムコールを呼ぶことで、ネイティブ関数の実行は完了です。
少し気になるのは、dlopen
、dlsym
はWindows版の記述もあったけど、ffi_call
にはWindows用の分岐が無いです。Windowsでもffi_call
のシステムコールは存在するのでしょうか?それともUnix系オンリーの機能なのか…。
以上で、PHP-FFIのソースコード上の流れも把握できました。実際に複雑な部分はffi_prep_cif
する時の引数やら戻り値を動的に作っていく箇所だけで、それ以外は素直なソースコードと思いました。
C言語での実装
さらに腹落ちさせるために、PHP-FFIがやっていることをC言語で書いてみました。
1 |
|
コンパイルは下記コマンド
1 | $ gcc ffi.c -ldl -lffi |
順番としては
dlopen
共有ライブラリを開いてハンドルを取得dlsym
共有ライブラリ内のsymbolのアドレスを取得ffi_prep_cif
を読んでffi_call
実行用のffi_cif
構造体を作成ffi_call
で共有ライブラリの関数を実行
まとめ
今回の調べものに際して、rubyのFFI周りの記事とかを参照したところ、大体同じような作りになっているようです。常套手段というか、これが普通みたいです。
中身も把握できたので、これで安心して使えますね!