Posted at

PHP7.4 の FFIを試した


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_OPENdlopenのエイリアス)が呼ばれるようだし。


まとめ

このサンプルプログラムは https://github.com/nishimura/php_ffi_samples ここに置いた。

これでPHPはフォーム操作とDBのやり取りに専念して、細かなロジックはHaskellでもGoでもRustでも好きな言語で書ける!