unoh.github.com

30分でわかる PHP Extensionの作り方を学べる記事をかいたよー \(^o^)/

Mon Jan 10 23:13:02 -0800 2011

こんにちは。12月に入社した@chobi_eです。

私が所属しているチームではお菓子系男子が30%を超えているという素敵チームで 毎週チーム内の漢の子がお菓子を焼いてくるという状況でハッピハッピハッピーです。

今日は私が学んできたPHP Extension作成についてのノウハウの一部を 公開しようと思います。

PHPExtension作成についての資料はklabさんやyoyaさん rskyさんの記事が参考になりますが私のようにPHPは書けてもCが書けない人には具体的にhello world以降何をすればいいのかがサッパリよく分かりません。

そこで先人達が作ってくれた偉大なライブラリをPHPで扱えるようにする為にC/MigemoのPHPバインディングを作ってみましょう

C/Migemoをインストールしてみる

読者の方の中にはC/Migemoをご存知でない方もいらっしゃるかと思いますので簡単に説明するとC/MigemoはKoRoNさんが作成されているローマ字のまま日本語を(インクリメンタルに)検索する」ための正規表現を生成するライブラリです。

http://www.kaoriya.net/#CMIGEMO

私は普段Ubuntuで開発をしているのでUbuntuでのC/Migemoの導入方法の説明を書きます。 OSXやWindowsの方はC/MigemoのREADMEを読みつつコンパイルしてください。

sudo aptitude install nkf

svn co http://cvs.kaoriya.net/svn/CMigemo/trunk migemo
cd migemo
./configure
make gcc

make gcc-dict
cd dict
make utf8
cd ..
sudo make gcc-install

これでC/Migemoのインストールが完了しました。C/Migemoには簡単な動作を確認する為の プログラムが付属しているのでそれを使って試してみましょう。

cmigemoを起動してchobiという文字列を入力してみます。

chobie@air:~/src/migemo$ cmigemo -d /usr/local/share/migemo/utf-8/migemo-dict
migemo_open("/usr/local/share/migemo/utf-8/migemo-dict")=0x820d008
clock()=0.210000
QUERY: chobi
PATTERN: (チョビ|チョビ|ちょび|chobi|chobi)

chobiに対応した正規表現が生成されましたね!

PHP Extensionの下準備

まずはPHP Extensionの設計を自分がよく理解できる言語でふわっと書いてみましょう。

<?php
/**
* これは実装するExtensionの目標で実際には使いません。
*/
class Migemo{
    public function __construct()
    {
      //migemoのロードを行う
    }

    public function query($query)
    {
      //migemoに問い合せて正規表現を返す
    }
}

せっかくなのでUnitTestも書いておきましょう。

PHPExtensionを作成する場合は別途テスト用のrun-test.phpというのが付属してるんですが、自分で使う分には慣れ親しんだツールを使ったほうが楽なので今回はPHPUnit3を使います。

sudo pear channel-discover pear.phpunit.de
sudo pear channel-discover components.ez.no
sudo pear channel-discover pear.symfony-project.com
sudo pear install phpunit/PHPUnit

テストを書きます。

  • MigemoTest.php
  • <?php
    class MigemoTest extends \PHPUnit_Framework_TestCase{
    
    
        /**
         * Migemoロードされているかしらべるよ
         */
        public function testMigemoIsLoaded()
        {
            $this->assertEquals(true,extension_loaded("migemo"));
        }
    
        /**
         * Migemoクラスがあるかしらべるよ
         * @depends testMigemoIsLoaded
         */
        public function testMigemoClassExists()
        {
            $this->assertEquals(true,class_exists("Migemo"));
        }
    
        /**
         * @depends testMigemoClassExists
         */
        public function testQueryMethodExists()
        {
            $reflection = new \ReflectionClass("Migemo");
            $this->assertEquals(true,$reflection->hasMethod("query"));
        }
    
        /**
         * @depends testQueryMethodExists
         */
        public function testQuery()
        {
            $migemo = new Migemo();
            $this->assertEquals("(チョビ|チョビ|ちょび|chobi|chobi)",$migemo->query("chobi"));
            unset($migemo);
        }
    
        /**
         * @depends testQuery
         */
        public function testQueryCycle()
        {
            $migemo = new Migemo();
            for($i = 0; $i < 30; $i++){
                $this->assertEquals("(チョビ|チョビ|ちょび|chobi|chobi)",$migemo->query("chobi"));
            }
            unset($migemo);
    
        }
    }
    

    さて、これでExtension作成までの準備が出来ましたのでサクサク書いていきます。

    PHP Extensionを書く

    私は容量悪い系の人間なのでコツコツ書いてきますよ!みなさんも写経についてきてくださいね

  • config.m4
  • PHP_ARG_ENABLE(migemo,
      [  --enable-migemo      Enable "migemo" extension support])
    
    if test $PHP_MIGEMO != "no"; then
      PHP_NEW_EXTENSION(migemo, migemo.c, $ext_shared)
      PHP_ADD_LIBRARY(migemo,, MIGEMO_SHARED_LIBADD)
      PHP_SUBST(MIGEMO_SHARED_LIBADD)
    fi

    phpizeというビルド環境をチェックしつつコンパイルに必要な設定を書いてくれるconfig.m4というマクロファイルを書きます。とりあえずこう書けばOK

    本来はもうちょっとPHP Extensionらしい書き方が有るんですが長くなるので今回はこんな形でごまかしちゃいましょう。

    続いてPHPにExtensionを登録するためのコードを書きます。

  • php_migemo.h
  • #ifndef PHP_MIGEMO_H
    #define PHP_MIGEMO_H
    
    #define PHP_MIGEMO_EXTNAME "migemo"
    #define PHP_MIGEMO_EXTVER "0.1"
    
    
    #ifdef HAVE_CONFIG_H
    #include "config.h"
    #endif
    
    #include "php.h"
    #include 
    
    extern zend_module_entry migemo_module_entry;
    #define phpext_migemo_ptr &migemo_module_entry;
    
    PHPAPI zend_class_entry *migemo_class_entry;
    
    #endif /* PHP_MIGEMO_H */
    
  • migemo.c
  • #include "php_migemo.h"
    
    static zend_function_entry php_migemo_methods[] = {
        {NULL, NULL, NULL}
    };
    
    PHP_MINIT_FUNCTION(migemo) {
        return SUCCESS;
    }
    
    PHP_MINFO_FUNCTION(migemo)
    {
    }
    
    zend_module_entry migemo_module_entry = {
    #if ZEND_MODULE_API_NO >= 20010901
        STANDARD_MODULE_HEADER,
    #endif
        PHP_MIGEMO_EXTNAME,
        NULL, 					/* Functions */
        PHP_MINIT(migemo),	    /* MINIT */
        NULL, 	                /* MSHUTDOWN */
        NULL, 	                /* RINIT */
        NULL,	                /* RSHUTDOWN */
        PHP_MINFO(migemo),	    /* MINFO */
    #if ZEND_MODULE_API_NO >= 20010901
        PHP_MIGEMO_EXTVER,
    #endif
        STANDARD_MODULE_PROPERTIES
    };
    
    #ifdef COMPILE_DL_MIGEMO
    ZEND_GET_MODULE(migemo)
    #endif
    

    そいでは準備ができましたのでコンパイルしてみましょう。

    phpize
    ./confiugre
    make
    sudo make install
    
    # migemo本体の共有ライブラリいれてからldconfigしてなかったので初回だけ。
    sudo ldconfig
    

    php5のconfファイルにmigemo.soを追加します。

  • /etc/php5/conf.d/migemo
  • extension=migemo.so

    これでPHPにMigemo Extensionを登録するだけのExtensionが作れました。

    chobie@air:~/src/php-migemo$ php -m | grep migemo
    migemo
    

    確かにPHPExtensionがロードされているようですね。UnitTestも実行しておきましょう。

    chobie@air:~/src/php-migemo$ phpunit MigemoTest.php 
    PHPUnit 3.5.6 by Sebastian Bergmann.
    
    .FSSS
    
    Time: 0 seconds, Memory: 3.00Mb
    
    There was 1 failure:
    
    1) MigemoTest::testMigemoClassExists
    Failed asserting that  matches expected .
    
    /home/chobie/src/php-migemo/MigemoTest.php:19
    
    FAILURES!
    Tests: 2, Assertions: 2, Failures: 1, Skipped: 3.
    

    きちんとモジュールが読み込まれるテストは成功していますね。

    クラスだけを実装する

    それではMigemoクラスをExtensionで実装してみましょう。まずは単純にクラスの定義だけを行ないます。PHPで言えばこんなコードになります。

    <?php
    class Migemo{}
    

    新しいクラスを定義するにははPHP_MINIT_FUNCTION(migemo)に3行追加するだけで出来ます。

  • migemo.c
  • PHP_MINIT_FUNCTION(migemo) {
        zend_class_entry ce;
        INIT_CLASS_ENTRY(ce, "Migemo", php_migemo_methods);
        migemo_class_entry = zend_register_internal_class(&ce TSRMLS_CC);
        return SUCCESS;
    }
    

    コレだけです。zend_class_entry `migemo_class_entry`は前もってphp_migemo.h で宣言しておいたのでPHP_MINIT_FUNCTIONで使うことができます。

    INIT_CLASS_ENTRYは一番目にzend_class_entry、2番目にクラス名、3番目にクラスに 対応したzend_function_entryを渡せばOKです。

    それでは再度コンパイルしてチェックしてみましょう。

    sudo make install
    
    chobie@air:~/src/php-migemo$ phpunit MigemoTest.php 
    PHPUnit 3.5.6 by Sebastian Bergmann.
    
    ..FII
    
    Time: 0 seconds, Memory: 3.00Mb
    
    There was 1 failure:
    
    1) MigemoTest::testQueryMethodExists
    Failed asserting that  matches expected .
    
    /home/chobie/src/php-migemo/MigemoTest.php:28
    
    FAILURES!
    Tests: 5, Assertions: 3, Failures: 1, Incomplete: 2.
    

    2つテストが成功しました。クラスも定義できたのでどんどん進みましょう。

    queryメソッドを実装してみる

    ひとまずこれでMigemoクラスが扱えるようになったので地味な部分はとりあえずおいておいてqueryメソッドを実装してしまいましょう。

    メソッドの追加は先程`INIT_CLASS_ENTRY`で指定した`php_migemo_methods`に どんなメソッドがあるかを追加しておけばOKです。

    static zend_function_entry php_migemo_methods[] = {
        PHP_ME(migemo, query, arginfo_migemo_query, ZEND_ACC_PUBLIC)
        {NULL, NULL, NULL}
    };
    

    zend_function_entry配列のお約束は最後は必ず{NULL,NULL,NULL}で締める、という事を守っておけばOK。PHP_MEなどのzend_function_entry用マクロのあとは ,は必要ありません。大体一緒なので雰囲気で書いちゃいましょう。

    これでMigemoクラスにはqueryというメソッドがあるよ!というのを定義出来ました。 肝心の実装はどう書くかというと。

    ZEND_BEGIN_ARG_INFO_EX(arginfo_migemo_query, 0, 0, 1)
        ZEND_ARG_INFO(0, query)
    ZEND_END_ARG_INFO()
    PHP_METHOD(migemo, query)
    {
        migemo *m;
        char *query;
        int ret = 0;
        int query_len = 0;
        unsigned char *result;
        if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
            "s", &query, &query_len) == FAILURE){
            return;
        }
    
        m =  migemo_open("/usr/local/share/migemo/utf-8/migemo-dict");
        result = migemo_query(m, query);
    
        RETVAL_STRING(result, 1);
        migemo_release(m, result);
        migemo_close(m);
    }
    

    こんな感じです。Zend Engine2周りの関数やマクロが入ってきて若干分かりづらいかもしれませんがこれはこういうもんだと思っておけば大丈夫です。

    RETVAL_STRINGはZEND_APIで定義されているマクロの一つでメソッドや関数の返り値を 簡単に設定できるマクロです。RETVAL系のマクロを使うとCの型からPHPの型に簡単に 変換しつつ返り値の設定をしてくれるので便利ですね。

    それではまたコンパイルして試してみましょう

    sudo make install
    
    chobie@air:~/src/php-migemo$ phpunit MigemoTest.php 
    PHPUnit 3.5.6 by Sebastian Bergmann.
    
    .....
    
    Time: 7 seconds, Memory: 3.00Mb
    
    OK (5 tests, 34 assertions)
    

    おー、ちゃんと動いていますね!

    しかし今のままだとqueryメソッドを呼ぶ際に毎回migemoの初期化と解放をしているのが気に入りません。

    それではコンストラクタを実装してみましょう。

    コンストラクタを実装

    コンストラクタの実装と行きたいのですがよく考えてみると一つ困ったことがでてきました。 migemoポインタはどこに格納しておけばよいのでしょう?

    クラスのインスタンス関連付けて何かしらの構造を持ち回したい場合は自前の 構造体を定義してクラス生成時のオブジェクトの生成をハンドリングする方法を使います。

    オブジェクトの生成をハンドリングする為にはオブジェクトを作るための関数と、オブジェクトが作ったメモリを開放するための関数を定義してzend_class_entry->create_object にオブジェクトを作成する為の関数を設定してあげます。

  • migemo.c
  • typedef struct{
        zend_object zo;
        migemo *migemo;
    } php_migemo;
    

    構造体のキャストを行なっているので先頭はzend_objectでなければいけません。

    続いてオブジェクトの生成と解放を行う関数を追加します。殆ど同じなのでこういうもんだと思ってコピペすればOKです。大事なところは緑でマーキングしておいたので自分で違うのを作る場合は参考にしてください。

  • migemo.c
  • static void php_migemo_free_storage(php_migemo *object TSRMLS_DC)
    {
        zend_object_std_dtor(&object->zo TSRMLS_CC);
        
        if(!object->migemo){
          migemo_free(object->migemo);
        }
        object->migemo = NULL;
        efree(object);
    }
    
    zend_object_value php_migemo_new(zend_class_entry *ce TSRMLS_DC)
    {
    	zend_object_value retval;
    	php_migemo *obj;
    	zval *tmp;
    
    	obj = ecalloc(1, sizeof(*obj));
    	zend_object_std_init( &obj->zo, ce TSRMLS_CC );
    	zend_hash_copy(obj->zo.properties, &ce->default_properties, (copy_ctor_func_t) zval_add_ref, (void *) &tmp, sizeof(zval *));
    
    	retval.handle = zend_objects_store_put(obj, 
            (zend_objects_store_dtor_t)zend_objects_destroy_object,
            (zend_objects_free_object_storage_t)php_migemo_free_storage,
            NULL TSRMLS_CC);
    	retval.handlers = zend_get_std_object_handlers();
    	return retval;
    }
    

    コンストラクタを追加します。

    PHP_METHOD(migemo, __construct){
        php_migemo *this = (php_migemo *) zend_object_store_get_object(getThis() TSRMLS_CC);
    
        this->migemo = migemo_open("/usr/local/share/migemo/utf-8/migemo-dict");
    }
    

    コンストラクタを追加したのでメソッド定義とクラスの初期化部分に少し手を入れます。

    static zend_function_entry php_migemo_methods[] = {
        PHP_ME(migemo, __construct, NULL, ZEND_ACC_PUBLIC)
        PHP_ME(migemo, query, arginfo_migemo_query, ZEND_ACC_PUBLIC)
        {NULL, NULL, NULL}
    };
    
    PHP_MINIT_FUNCTION(migemo) {
        zend_class_entry ce;
        INIT_CLASS_ENTRY(ce, "Migemo", php_migemo_methods);
        migemo_class_entry = zend_register_internal_class(&ce TSRMLS_CC);
    migemo_class_entry->create_object = php_migemo_new;
    
        return SUCCESS;
    }
    

    コンストラクタ側でmigemoの生成行うようにしたので、それに合わせて queryの実装を変更します。

    PHP_METHOD(migemo, query)
    {
        php_migemo *this = (php_migemo *) zend_object_store_get_object(getThis() TSRMLS_CC);
        char *query;
        int ret = 0;
        int query_len = 0;
        unsigned char *result;
        if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
            "s", &query, &query_len) == FAILURE){
            return;
        }
    
        result = migemo_query(this->migemo, query);
    
        RETVAL_STRING(result, 1);
        migemo_release(this->migemo, result);
    }
    

    これで実装が完了しましたね。もう一回コンパイルして試してみましょう。

    chobie@air:~/src/php-migemo$ phpunit MigemoTest.php 
    PHPUnit 3.5.6 by Sebastian Bergmann.
    
    .....
    
    Time: 0 seconds, Memory: 3.00Mb
    
    OK (5 tests, 34 assertions)
    

    ユニットテストの時間が短縮されていて効果が出ていますね。これで実装が終了しました!

    あとがき

    どうでしたか?思っていたより簡単に開発できたのではないでしょうか?

    PHPバインディングから始めるExtension作成は割と身近で親しみやすく、 次に自分が何を調べるべきかという目標が明確に分かるのでとっかかりには オススメだと思います。

    最後に参考になるリンクと宣伝を貼っておきます。

  • php-migemo http://github.com/chobie/php-migemo
  • 今回作成したExtensionのGitリポジトリです。

  • php-git http://github.com/chobie/php-git
  • php-gitはlibgit2をPHPで扱えるようにする為のPHPバインディングです。最近作り始めたばかりなので まだまだalpha developmentで課題はたくさん有りますがPHPでgit repositoryを使った操作が行えます。

  • ZendEngine 勉強会グループ http://groups.google.com/group/php-zendengine/
  • 2011年2月か3月に東京でZendEngineについての勉強会を行ないます。私もLT予定ですので ご興味のある方はぜひぜひどうぞ!

  • [klabさん] PHP Extension を作ろう第1回 - まずは Hello World
  • [klabさん] PHP Extension を作ろう第2回 - 引数と返値
  • [klabさん] PHP Extension を作ろう第3回 - クラスを作ろう
  • [yoyaさん] PHP extension の作り方
  • [rskyさん] 実例で学ぶPHP拡張モジュールの作り方 第6回 PHP 5.3の変更点(その2)
  • それでは、みんなも作ってア・ラ・モード☆