Boost.Python のような手軽さで C++ で PHP の拡張モジュールを書ける「Mozo.PHP (仮称)」

えー、久しぶりにですます調です。本日の第2回PHP拡張勉強会でグダグダながらもちょこっと発表させていただいた、あのライブラリについての紹介です。

ダウンロードは以下からできます。
http://www.voltex.jp/downloads/mozo-php-20080324.tar.bz2
最新版はこちら >> http://voltex.jp/downloads/mozo-php-20080405.tar.bz2
2009-08-10 追記: 現在は Boost.PHP という名前で以下の場所で開発中です。 http://github.com/moriyoshi/boost.php

なぜMozo.PHPを作ったのか

Boost.Python という、Boost Project の一部である変態ライブラリがあるのですが、これは Python の拡張をおおよそ C++ とは思えないような書き方で (でも読みやすく) 書けてしまうという、結構最初見たときは目を疑ってしまうようなモジュールです。

#include <string>
#include <boost/python.hpp>

class foo_class
{
public:
    std::string mtd1() {
        return "mtd1";
    }

    std::string mtd2() {
        return "mtd2";
    }
};

BOOST_PYTHON_MODULE(my_first_python_module)
{
    using namespace boost::python;

    class_<foo_class>("foo_class")
        .def("mtd1", &foo_class::mtd1)
        .def("mtd2", &foo_class::mtd2);
}

たとえばこう書くだけで my_first_python_module というモジュール内に foo_class のラッパを定義できてしまったりします。まー、static initialization でいろいろ頑張ってる都合上 Mach-O の dylib loader と相性が悪い (2つ以上同時に Boost.Python で作られたモジュールを import すると落ちたりする) とか問題はありますが、基本的に SWIG とかいらなくなります。

こんなのが PHP にもあったらおもしろいかも (って誰が喜ぶんだろう) ってノリで作りはじめてできたのが Mozo.PHP というわけです。

現在の Mozo.PHP にできること、できないこと

ざっと言うと、今できることは

  • グローバル関数の定義
  • PHP 関数の呼び出し
  • 値の自動変換
  • 例外の自動変換

で、今できないことは

  • static でない関数の呼び出しサポート
  • クラスのサポート
  • ini 設定の定義
  • より柔軟な関数シグニチャの定義

とかですがそのうちやる気があったら拡充してこうと思ってます。

Mozo.PHP を使う前に

C++ コンパイラと、Boost が必要です。Boost についてはパッケージに含まれるヘッダファイルのみを利用するので、ビルドする必要はありません。

Mozo.PHP の基本

Mozo.PHP では必ず拡張モジュールごとにモジュールクラスというものを定義します。このクラスは拡張モジュールが読み込まれた時にインスタンス化されます。モジュール情報を表す zend_module_entry を動的にいじりたい場合は、このモジュールクラスのコンストラクタの中で行います。(環境によっては期待通り動かなかったりするので推奨はしませんが) もしリクエストを越えてデータを保持しなくてはならないような場合には、このモジュールクラスにメンバ変数を定義して、そこにデータを入れておきます。

モジュールクラスには、必ずモジュールハンドラクラスという内部クラスを定義することになっています。このクラスは、リクエスト毎に生成されます。C で拡張モジュールを書いた方なら、モジュールグローバル変数という名前を聞いたことがあると思いますが、そのグローバル変数がこのハンドラクラスのメンバ変数に相当します。モジュール関数を定義するときはこのクラスに追加していくことがあくまで想定されています。

次に基本的なソースコードのレイアウトを示します。

// (1) mozo::php::moduleクラスを提供するヘッダをインクルード
#include "mozo/php/module.hpp"

using namespace mozo;

// (2) モジュールクラスの定義
class m001_module: public php::module {
public:

    // (3) モジュールハンドラの定義
    class handler
        : public php::module::handler {
    public:
        handler(m001_module* mod)
            :php::module::handler(mod) {}
    };

public:
    m001_module(zend_module_entry* entry)
        : php::module(entry) {
    }
};

// (4) モジュール情報の定義
#define MOZO_PHP_MODULE_NAME m001
#define MOZO_PHP_MODULE_CAPITALIZED_NAME M001
#define MOZO_PHP_MODULE_VERSION "0.1"
#define MOZO_PHP_MODULE_CLASS_NAME m001_module

// (5) 初期化コード (ブートストラップ) のインクルード
#include "mozo/php/module_def.hpp"

まず (1) で module クラスを定義するヘッダをインクルードし、(2) (3) でモジュールクラスおよびモジュールハンドラクラスを定義、(4) で必要なモジュール情報を #define マクロで定義し、最後に (5) で Mozo.PHP が提供する、拡張モジュールの動作に必要な初期化コードをインクルードするという流れになっています。

(4) のモジュール情報の定義で使うプリプロセッサマクロの意味を以下に示します。

マクロ名 意味
MOZO_PHP_MODULE_NAME モジュール名
MOZO_PHP_MODULE_CAPITALIZED_NAME モジュール名を大文字にしたもの。将来廃止の可能性あり。
MOZO_PHP_MODULE_VERSION モジュールのバージョン。数値ではなく文字列で指定する。
MOZO_PHP_MODULE_CLASS_NAME モジュールクラスの名前。

拡張モジュールのビルド

さて、以上のコードをビルドするために、次のような Makefile を用意します。GNU Make の拡張を利用していますが、大抵の環境には入っていることと思いますのであまり問題にはならないでしょう (笑) 試していませんが Cygwin ではだめかもしれません。

CXX=g++
LD=g++

PHP_HOME=$(HOME)/opt/php-5.2
PHP_CONFIG=$(PHP_HOME)/bin/php-config
PHP_INCLUDES=$(shell $(PHP_CONFIG) --includes)
#BOOST_INCLUDES=-I$(HOME)/opt/boost-1.34.1/include

CPPFLAGS=-I. $(PHP_INCLUDES) $(BOOST_INCLUDES)
CXXFLAGS=-g
LDFLAGS:=$(if $(shell uname | grep "Darwin"),  -bundle -undefined dynamic_lookup, -shared)

all: m001.so

m001.o: m001.cpp
	$(CXX) -DCOMPILE_DL_M001 $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $<

m001.so: m001.o
	$(LD) $(LDFLAGS) -o $@ $^

clean:
	rm -f *.o
	rm -f *.so

.PHONY: clean all
.SUFFIXES: .o .cpp .so

この Makefile でビルドするには、make のオプションに PHP_CONFIG=... で php-config スクリプトの場所を指定します。php-config スクリプトは PHP のインストール時に bin ディレクトリにインストールされ、拡張モジュールをビルドするのに必要な情報を与えてくれるシェルスクリプトです。もし php-config スクリプトにパスが通っている場合 (ディストロのパッケージから入れたような場合はこれに該当します) は単純にスクリプト名だけを指定すればよいでしょう。

make PHP_CONFIG=php-config

なお、もしBoost がシステムにインストールされておらず、ソースパッケージを展開したディレクトリをそのまま使いたい場合は次のようにします。

make PHP_CONFIG=php-config BOOST_INCLUDES=-I/home/moriyoshi/src/boost_1_34_1

さて、ビルドが正常終了すると現在の作業ディレクトリ配下に m001.so が生成されていることが確認できると思います。これを PHP で利用可能にするには php.ini を書き換えて extension_dir に現在の作業ディレクトリを指定した上で extension に m001.so を指定すればいいのですが、CLI 版の PHP を使っている場合は、コマンドラインパラメータを指定するだけで PHP 起動時に読み込ませることができます。

php -dextension_dir=`pwd` -dextension=m001.so [引数...]

モジュールが正しくロードできているかを確認するために次のコードで試してみます。

<?php var_dump(extension_loaded('m001')); ?>

次のように実行し、bool(true)と表示されれば成功です。

php -dextension_dir=`pwd` -dextension=m001.so m001.php

拡張モジュールに関数を追加する

モジュールのビルド環境が整ったところで、モジュールに関数を追加してみましょう。関数を定義するにも、次のステップを踏むだけです。

  1. mozo/php/function.hpp をインクルード
  2. モジュールクラスに mozo::php::function_container を継承させる (実態は mix-in)
  3. モジュールクラスのコンストラクタで関数エントリを定義し、zend_module_entry.functions に代入する (defun)。

次の例では、与えられた2つの引数の和を戻り値として返すadd(int, int) と 差を返すsub(int, int) という関数をハンドラクラスに定義し、それを defun("add", ...) で登録しています。defun の第1引数は PHP 側から利用するときの関数名、第2引数はPHPから呼び出されるC++関数へのポインタです。

#include "mozo/php/module.hpp"
#include "mozo/php/function.hpp"

using namespace mozo;

class m002_module
    : public php::module,
      public php::function_container<m002_module> {
public:
    class handler
        : public php::module::handler {
    public:
        handler(m002_module* mod)
            :php::module::handler(mod) {}

        static int add(int a, int b) {
            return a + b;
        }

        static int sub(int a, int b) {
            return a - b;
        }
    };
public:
    m002_module(zend_module_entry* entry)
        : php::module(entry) {
        entry->functions =
             defun("add", &handler::add).
             defun("sub", &handler::sub);
    }
};

#define MOZO_PHP_MODULE_NAME m002
#define MOZO_PHP_MODULE_CAPITALIZED_NAME M002
#define MOZO_PHP_MODULE_VERSION "0.1"
#define MOZO_PHP_MODULE_CLASS_NAME m002_module

#include "mozo/php/module_def.hpp"

さて、これを前の Makefile を流用して (s/m001/m002/) ビルドし、試してみます。

<?php
for ($i = 0; $i < 10; ++$i) {
    var_dump(add($i, 2), sub($i, 2));
}
?>

これを実行すると、次のような結果が表示されるはずですね。

int(2)
int(-2)
int(3)
int(-1)
int(4)
int(0)
int(5)
int(1)
int(6)
int(2)
int(7)
int(3)
int(8)
int(4)
int(9)
int(5)
int(10)
int(6)
int(11)
int(7)

逆に PHP の関数を呼び出してみる

Mozo.PHP では、自分で作った関数の中から PHP の関数を逆に呼び出したりもできます。次の例は、2つの文字列引数を受け取ってそれをつなげたものを strtoupper() をそのまま使って大文字に変換する concat_and_uppercase() という関数を定義しています。

#include "mozo/php/module.hpp"
#include "mozo/php/function.hpp"

using namespace mozo;

class m003_module
    : public php::module,
      public php::function_container<m003_module> {
public:
    class handler
        : public php::module::handler {
    public:
        handler(m003_module* mod)
            :php::module::handler(mod) {}

        static php::const_value_ptr concat_and_uppercase(
                std::string a, std::string b) {
            php::function strtoupper("strtoupper");
            return strtoupper(a + b);
        }
    };
public:
    m003_module(zend_module_entry* entry)
        : php::module(entry) {
        entry->functions =
             defun("concat_and_uppercase", &handler::concat_and_uppercase);
    }
};

#define MOZO_PHP_MODULE_NAME m003
#define MOZO_PHP_MODULE_CAPITALIZED_NAME M003
#define MOZO_PHP_MODULE_VERSION "0.1"
#define MOZO_PHP_MODULE_CLASS_NAME m003_module

#include "mozo/php/module_def.hpp"

まあ見ればすぐに分かると思いますが、ここでのポイントは mozo::php::function というクラスです。これはコンストラクタに PHP の関数名を渡すと、以降そのオブジェクトはその PHP の関数を呼び出すファンクタとして機能するようになります。

そろそろ眠いので今日はこの辺で。