Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
6
Help us understand the problem. What is going on with this article?
@nishimura

PHP7.4 の FFIを試した

More than 1 year has passed since last update.

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でも好きな言語で書ける!

6
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
6
Help us understand the problem. What is going on with this article?