Interop サービスを使って CLR 環境から PHP スクリプティングエンジンを呼び出す。

最近、いまさらですが C# にまともに取り組みはじめました。Interop サービスの痒いところに手が届き具合に「おー、すげえ」と感嘆の声をあげてばかりです。もはや、JNI が不幸でなりません。

で、どうせならということで PHP の embed SAPI を使って C# から PHP のスクリプティングエンジンを呼び出してみることにしました。

using System;
using System.Collections;
using System.Runtime.InteropServices;
using System.Text;

class Traits
{
    public const String DllName = "libphp5.so";
}

[StructLayout(LayoutKind.Sequential)]
struct ZValueStr {
    public IntPtr val;
    public int len;
};

[StructLayout(LayoutKind.Explicit)]
struct ZValueStorage {
    [FieldOffset(0)]
    public Int32 lval;
    [FieldOffset(0)]
    public double dval;
    [FieldOffset(0)]
    public ZValueStr str;
    [FieldOffset(0)]
    public IntPtr ht;
    [FieldOffset(0)]
    public IntPtr obj;
};

enum ZValueType: byte
{
    NULL           = 0,
    LONG           = 1,
    DOUBLE         = 2,
    BOOL           = 3,
    ARRAY          = 4,
    OBJECT         = 5,
    STRING         = 6,
    RESOURCE       = 7
};

[StructLayout(LayoutKind.Sequential)]
struct ZValue
{
    public ZValueStorage value;
    public uint refcount;
    public ZValueType type;
    public byte is_ref;

    [DllImport(Traits.DllName, EntryPoint="_zval_dtor_func")]
    private static extern void zval_dtor(ref ZValue v);

    public void Dispose()
    {
        zval_dtor(ref this);
    }

    public Object Marshal()
    {
        switch (type) {
        case ZValueType.NULL:
            return null;
        case ZValueType.LONG:
            return value.lval;
        case ZValueType.DOUBLE:
            return value.dval;
        case ZValueType.BOOL:
            return value.lval;
        case ZValueType.ARRAY:
            return value.ht; // XXX: マーシャリング必要
        case ZValueType.OBJECT:
            return value.obj; // XXX: マーシャリング必要
        case ZValueType.STRING:
            byte[] buf = new byte[value.str.len];
            System.Runtime.InteropServices.Marshal.Copy(
                    value.str.val, buf, 0, value.str.len);
            return new String(Encoding.Default.GetChars(buf));
        case ZValueType.RESOURCE:
            return value.lval;
        }

        return null;
    }
}

public class PHPRunner
{
    [DllImport(Traits.DllName)]
    private static extern int php_embed_init(int argc, String[] argv);

    [DllImport(Traits.DllName)]
    private static extern void php_embed_shutdown();

    [DllImport(Traits.DllName)]
    private static extern int zend_eval_string(String str, out ZValue retval,
            String name);

    public static int Main(String[] args)
    {
        // PHP 初期化
        if (php_embed_init(0, null) != 0) {
            Console.Error.WriteLine(
                    "Unable to initialize PHP scripting engine.");
            return 1;
        }

        // PHP スクリプトを実行
        ZValue retval = new ZValue();

        zend_eval_string("htmlspecialchars('&&&日本語')", out retval, "eval");
        Console.WriteLine(retval.Marshal());
        retval.Dispose();

        zend_eval_string("1 + 2 + 3 * 4", out retval, "eval");
        Console.WriteLine(retval.Marshal());
        retval.Dispose();

        // クリーンアップ
        php_embed_shutdown();
        return 0;
    }
}

遊びかた

1. まず、PHP を --enable-embed=shared つきでビルドします。
./configure --enable-embed=shared
make && make install DESTDIR=/tmp/php
2. 次に適当なディレクトリでこのソースを
mcs phprunner.cs

てな感じでコンパイルします。(これは mono の場合)

3. できたアセンブリと同じディレクトリに 1 の手順で作った共有ライブラリ $(PREFIX)/lib/libphp5.so をコピーします。
cp /tmp/php/usr/local/lib/libphp5.so .
4. 実行します。
mono phprunner.exe

こんな表示が出れば成功です。

&&&日本語
15

課題

  • HashTable やオブジェクトのマーシャルがありません。
    • HashTable はまあなんとかすぐ実装できるんじゃないでしょうか。
    • オブジェクトはけっこう面倒くさいと思います。
  • TSRM 非対応です。
    • なので、スレッドセーフじゃありません。
    • Win32 の .NET Framework 上で試す場合、Win32 バイナリパッケージの php_embed.dll を使うことになりますが、こいつは TSRM ビルドなことに留意する必要があります。