FFIを動かす準備と確認
PHP7.4に入るFFIのマニュアルが出来ていたので試してみる。
https://php.net/ffi
とは言ってもCの操作のマッピングに近いので、もしかしたらマニュアルよりCのソースを見ないと意味が分からないかもしれない。
まずはソースを取ってきてコンパイルする。
git clone https://github.com/php/php-src.git
cd php-src
git checkout PHP-7.4
./configure --with-ffi
make
--with-ffi
が必要。他のオプション無しだとコンパイル時間はそれほどかからない。
FFIを有効にしつつサーバーを立ち上げるのは
php-src/sapi/cli/php -d ffi.enable=1 -S localhost:8000
というように ffi.enable=1
を設定する。
サーバーが立ち上がったら、まずは https://php.net/ffi.examples-basic この辺の例をコピペしてエラーが出ないことを確認する。
FFIのプログラムを書く
動くことを確認したら、Cで共有ライブラリを作ってみる。
#include <string.h>
#define BUF_SIZE 1024
const char * sample(const char *data, int mod)
{
char buf[BUF_SIZE];
const char * ret = buf;
int i;
for (i = 0; i <= strlen(data) && i < BUF_SIZE; i++){
if (data[i] >= 97 && data[i] <= 122 && i % mod == 0)
buf[i] = data[i] - 32;
else
buf[i] = data[i];
}
return ret;
}
特定の位置の小文字を大文字にするプログラム。
これを
gcc -shared -o libsample.so sample.c
で共有ライブラリにしてPHPから読み込む。
<?php
header('Content-Type: text/plain');
$ffi = FFI::cdef("
const char * sample(const char *data, int mod);
", __DIR__ . '/libsample.so');
var_dump($ffi);
var_dump($ffi->sample("sample test test", 3));
var_dump($ffi->sample("sample test test", 4));
var_dump($ffi->sample("sample test test", 1));
/* output:
object(FFI)#1 (0) {
}
string(16) "SamPle teSt TesT"
string(16) "SampLe tEst Test"
string(16) "SAMPLE TEST TEST"
*/
上手く動いた。
PHPのstring
はC言語でconst char *
で受け取れる。
const
は必要っぽい。整数はint
をそのまま使える。
コールバックを利用する
マニュアルではzend_write
を利用しているが、サンプルとしてはあまり良くないと思う。
Cの関数ポインタがPHPのクロージャにマッピングされるという動きを試してみる。
#include <string.h>
#define BUF_SIZE 1024
typedef int (*callback_t)(int);
const char * sample(const char *data, callback_t callback)
{
char buf[BUF_SIZE];
const char * ret = buf;
int i;
for (i = 0; i <= strlen(data) && i < BUF_SIZE; i++){
if (data[i] >= 97 && data[i] <= 122 && callback(i))
buf[i] = data[i] - 32;
else
buf[i] = data[i];
}
return ret;
}
とても単純な int
を受け取って int
を返す関数。
これをPHP側ではクロージャにマッピングする。
<?php
header('Content-Type: text/plain');
$ffi = FFI::cdef("
typedef int (*callback_t)(int);
const char * sample(const char *data, callback_t callback);
", __DIR__ . '/libsample.so');
var_dump($ffi);
var_dump($ffi->sample("sample test test", fn($i) => $i % 3));
var_dump($ffi->sample("sample test test", fn($i) => $i % 4));
var_dump($ffi->sample("sample test test", fn($_) => true));
/* output:
object(FFI)#1 (0) {
}
string(16) "sAMpLE TEsT tESt"
string(16) "sAMPlE TeST tEST"
string(16) "SAMPLE TEST TEST"
*/
アロー関数もPHP7.4のソースにマージされているので使える。
本当はコールバック中で文字列を受け取って文字列を返したいところだけど、文字列を返そうとするとUncaught FFI\Exception: FFI internal error. Unsupported return type
のエラーになるようだ。受け取るのは出来るんだけど。
構造体を利用する
バリューオブジェクト的な引数を使いたい場合は構造体を利用する。
struct point {
int x;
int y;
};
これをPHPで使うには
<?php
$ffi = FFI::cdef('
struct point {
int x;
int y;
};
');
$point = $ffi->new('struct point');
$point->x = 1;
$point->y = 2;
とする。
折角なので先程のコールバックと組み合わせてみる。
#include <string.h>
#define BUF_SIZE 1024
typedef int (*callback_t)(int);
struct cbdata {
callback_t f;
};
const char * sample(const char *data, struct cbdata *cbdata)
{
char buf[BUF_SIZE];
const char * ret = buf;
int i;
for (i = 0; i <= strlen(data) && i < BUF_SIZE; i++){
if (data[i] >= 97 && data[i] <= 122 && cbdata->f(i))
buf[i] = data[i] - 32;
else
buf[i] = data[i];
}
return ret;
}
<?php
header('Content-Type: text/plain');
$ffi = FFI::cdef("
typedef int (*callback_t)(int);
struct cbdata {
callback_t f;
};
const char * sample(const char *data, struct cbdata *cbdata);
", __DIR__ . '/libsample.so');
$cbdata = $ffi->new('struct cbdata');
$n = 3;
$cbdata->f = function($i) use(&$n){
return $i % $n === 0;
};
$pcbdata = FFI::addr($cbdata);
var_dump($ffi->sample("sample test test", $pcbdata));
$n = 4;
var_dump($ffi->sample("sample test test", $pcbdata));
$n = 1;
var_dump($ffi->sample("sample test test", $pcbdata));
/* output:
string(16) "SamPle teSt TesT"
string(16) "SampLe tEst Test"
string(16) "SAMPLE TEST TEST"
*/
C関数からのコールバックでもPHPの参照を使えるので、値を順次変更しながらCの関数を実行することも出来そうである。
Qiitaでよく参照渡しとかの記事が話題になっているけれども、この辺の参照やポインタが何かということが分かっていないとハマるかもしれない。
C言語以外を利用する
数値計算を高速に実行したい場合はC言語で良いかもしれないが、文字列の加工はCでやりたくない。
他の言語にもFFIがあれば、C言語を通してやり取りできる。
#include <stdio.h>
#include <HsFFI.h>
#ifdef __GLASGOW_HASKELL__
#include "Callback_stub.h"
#endif
#include "template_operations.h"
int prepare() {
hs_init(0,0);
}
int finish() {
hs_exit();
}
static const char * assign_string = "";
void assign_value_set(const char *value)
{
assign_string = value;
}
const char *assign_value_get()
{
return assign_string;
}
const char* parse(char *data, struct template_operations *tops)
{
return hsParse(data, tops);
}
typedef struct template_operations
{
size_t (*assign)(const char * key, const char * value);
} CTOPS;
{-# LANGUAGE ForeignFunctionInterface #-}
#include "template_operations.h"
module HaskellPhp where
import Foreign
import Foreign.C.String
import Foreign.C.Types
data Tops = Tops { assign::FunPtr (CString -> CString -> IO CInt) }
foreign export ccall hsParse :: CString -> Ptr Tops -> IO CString
foreign import ccall "dynamic" mkFun :: FunPtr (CString -> CString -> IO CInt)
-> (CString -> CString -> IO CInt)
foreign import ccall "assign_value_get" value_get :: IO CString
instance Storable Tops where
sizeOf _ = #size CTOPS
alignment _ = #alignment CTOPS
peek ptr = do
assign' <- (#peek CTOPS, assign) ptr
return Tops { assign=assign' }
poke ptr (Tops assign') = do
(#poke CTOPS, assign) ptr assign'
--
-- Function to parse some string
--
hsParse :: CString -> Ptr Tops -> IO CString
hsParse cs cops = do
ops <- peek cops
let f = mkFun $ assign ops
a <- newCString "foo"
b <- newCString "bar"
r <- f a b
cstr <- value_get
s <- peekCString cstr
newCString $ "hsParse finish: [" ++ show r ++ "]" ++ "[" ++ s ++ "]"
<?php
header('Content-Type: text/plain');
$ffi = FFI::cdef('
int prepare();
int finish();
typedef struct template_operations
{
size_t (*assign)(const char * key, const char * value);
} CTOPS;
const char* parse(char *data, struct template_operations *tops);
void assign_value_set(const char *value);
const char * assign_value_get();
', __DIR__ . '/libcallback.so');
$ffi->prepare();
$tops = $ffi->new('struct template_operations');
$tops->assign = function($key, $value) use ($ffi){
$value = "$key = $value";
$ffi->assign_value_set($value);
// Notice:
// return string value is not supported
return strlen($value);
};
$data = '<div>$foo</div>';
$ret = $ffi->parse($data, FFI::addr($tops));
var_dump($ffi->assign_value_get());
var_dump($ret);
$ffi->finish();
/* Output
string(9) "foo = bar"
string(30) "hsParse finish: [9][foo = bar]"
*/
Haskellを使うサンプル。
Haskellでテンプレートエンジンを作ってその中からPHPと相互にやり取りするというような想定。
Cとのやり取りはhsc2hs
を使う。
返却値は文字列に出来ないのでC言語のグローバル変数にした。
PHPは元々スレッドセーフではないので、特に問題ないハズ?
ここまで来れば、もうC言語は要らないよねということで直接Haskellを呼び出す。
<?php
header('Content-Type: text/plain');
$ffi = FFI::cdef('
int hs_init(int *, char **[]);
int hs_exit();
typedef struct template_operations
{
size_t (*assign)(const char * key, const char * value);
} CTOPS;
const char* parse(char *data, struct template_operations *tops);
void return_value_set(const char *value);
const char * return_value_get();
', __DIR__ . '/libcallback.so');
$argc = FFI::new('int');
$argv = FFI::new('char[0]');
$pargv = FFI::addr($argv);
$ffi->hs_init(FFI::addr($argc), FFI::addr($pargv));
$tops = $ffi->new('struct template_operations');
$tops->assign = function($key, $value) use ($ffi){
$value = "$key = $value";
$ffi->return_value_set($value);
// Notice:
// return string value is not supported
return strlen($value);
};
$data = '<div>$foo</div>';
$ret = $ffi->parse($data, FFI::addr($tops));
var_dump($ffi->return_value_get());
var_dump($ret);
$ffi->hs_exit();
/* Output
string(9) "foo = bar"
string(30) "hsParse finish: [9][foo = bar]"
*/
{-# NOINLINE return_value #-}
return_value :: IORef CString
return_value = unsafePerformIO $ newCString "" >>= newIORef
return_value_set :: CString -> IO ()
return_value_set a = writeIORef return_value a
return_value_get :: IO CString
return_value_get = readIORef return_value
データのやり取りでAPIとしてC言語の宣言が出てくるけれど、C言語の実装は無くなった。
Haskellを普通に書くとグローバル変数が使えないので、そこはunsafePerformIO
で。
Cだとstatic
と書けばメモリ上の同じ位置だという安心感があるけれども、他の言語ではコンテキストから抜けてもグローバル変数が同じ領域を指しているのかが若干不安が残るが、問題なく動いているようだ。
同時アクセスしたときは…うーん。PHPは毎回コンテキストを生成するので影響ないよね。FFI::cdef
が実行されたときにDL_OPEN
(dlopen
のエイリアス)が呼ばれるようだし。
まとめ
このサンプルプログラムは https://github.com/nishimura/php_ffi_samples ここに置いた。
これでPHPはフォーム操作とDBのやり取りに専念して、細かなロジックはHaskellでもGoでもRustでも好きな言語で書ける!