PHPのエクステンションを作ってみる

Shared Object(.so)の勉強はこれの布石だったわけだが、インフルエンザのようなただの風邪のようなものにやられてしまい、間が空いてしまった。
休み明けに出社してみたらなんか出来上がってるっぽくて、俺の苦労はいったい……って感じだったが、折角なのでまとめておく。
ちなみに環境はCentOS 4.5と5のPHP 5.1.6で試した。PHP 5.2以前の場合はちょっとした罠があるので注意。

時代はPEAR::CodeGen_PECL

とりあえず作り方をぐぐってみたら、ext_skelというジェネレータを使うことがわかった。
が、さらに調べていると今はPEAR::CodeGen_PECLを使うのが作法らしい。


つーことで、とりあえずインストール。

pear install -a CodeGen_PECL

これでpecl-genというジェネレータに相当するコマンドが使えるようになる。
CodeGen_PECLでのエクステンション開発の流れは次のようになる。

  1. XMLによる定義ファイルを作成
  2. pecl-genに定義ファイルを読み込ませる
  3. (必要があればコード修正)
  4. phpize
  5. configure
  6. make && make install

以上のような感じでエクステンションが出来上がる。定義ファイルがほぼ全てという感じ。
エクステンション自体のコードはC/C++で書くことになるわけだが、このコードすらも基本的にはXMLの中に書き込む。

定義ファイルを書く

定義ファイルはこんな感じ。
詳しいことはマニュアルを参照。
とりあえず試してみるレベルではmaintainers、release、changelog、licenseあたりはなくてもよさそう。

<?xml version="1.0" ?>
<extension name="strsavefile" version="0.0.1">
    <summary>strsavefile PHP extension</summary>
    <description>
        strsavefile PHP extension
    </description>

    <maintainers>
        <maintainer>
            <user>yuki</user>
            <name>Yuki</name>
            <email>paselan at Gmail.com</email>
            <role>developer</role>
        </maintainer>
    </maintainers>

    <license>PHP</license>

    <release>
        <version>0.0.1</version>
        <state>alpha</state>
        <date>2007-11-12</date>
        <notes>
            Test version.
        </notes>
    </release>

    <changelog>
    </changelog>

    <deps language="c" platform="all">
        <with name="strsavefile" testfile="strsavefile.h">
            <header name="strsavefile.h" path="." />
            <lib name="strsavefile" platform="all" />
        </with>
    </deps>

    <function name="strsavefile">
        <proto>int strsavefile(string text, string filename)</proto>
        <code>
        <![CDATA[
        int result;

        result = strsavefile(text, filename);
        if (result < 0)
            RETURN_FALSE;

        RETURN_LONG(result);
        ]]>
        </code>
    </function>
</extension>

depsは外部依存ライブラリの設定で、ヘッダとかライブラリとか他のエクステンションに依存する場合はここで指定する。
ここではstrsavefileという自作の外部ライブラリを指定している。先日のhello.soを発展させて「文字列を指定のファイルに保存する」という関数を作ってみた。仕事で想定していた仕様が文字列とファイル名を引数に取るインターフェイスだったもんで。
で、ここの内容はconfigure時にチェックされて、指定のファイルが存在しないとエラーになる。またconfigureとも連動していて、--with-strsavefile=DIRのような形でライブラリの位置を指定するとそこを参照してくれる。デフォルトは/usr:/usr/localとかそんな感じだと思われ。


functionが実際の関数を定義している部分。protoがプロトタイプ宣言でcodeが関数の内容になる。
protoはPHPの型を使うがcodeはC/C++のコードを書く。
RETURN_XXXはPHPの変数(zval構造体?)としてリターンするマクロ。他にもzend_xxx関数とかphp_printf関数だとかPHP用の機能がたくさん用意されている。


ここで気になるのはプロトタイプで指定しているstring型の引数をいきなりネイティブな関数に渡している(ように見える)ところ。
実はここに書いたコードはそのまま関数の中身になるわけではなく、pecl-genがちょっと小細工をしてくれる。
吐き出されたコードを見ると次のようになっている。

PHP_FUNCTION(strsavefile)
{

    const char * text = NULL;
    int text_len = 0;
    const char * filename = NULL;
    int filename_len = 0;



    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &text, &text_len, &filename, &filename_len) == FAILURE) {
        return;
    }

    do {
        int result;

        result = strsavefile(text, filename);
        if (result < 0)
            RETURN_FALSE;

        RETURN_LONG(result);
    } while (0);
}

プロトタイプの引数に対応する変数が宣言されて、何やら専用の関数で引数を取得していることが見て取れる。
実際にcodeに書いた部分はdo-whileの中に収まっているようだ。


とまあこんな感じで引数と戻り値については簡単に処理できるようになっているので、真の意味で関数のコードに注力すればよい。
PHPとの橋渡しなどの詳しいことについてはマニュアルのPHP: PHP のコア: ハッカーの手引き - Manualを参照。ってもいまいち情報に欠けるんだが。あとはPECLのモジュールを参考にするしかない。
ただ、既存ライブラリのPHPラッパー関数を作る程度なら上記のようにちょろっと書けば済んでしまう。

そしてコンパイル

肝ともいえる定義ファイルが出来上がったので早速pecl-genに読み込ませてみる。

pecl-gen strsavefile.xml

するとエクステンション名(strsavefile)のディレクトリが作成されて、コンパイルに必要なファイルがごっそり出来上がる。
ちなみに、XMLファイルを何度も手直しして試すような場合には-fオプションをつけると、生成されるファイル群を強制上書きで処理してくれる。


エクステンションの作成で(というかphpizeするのに)本質的に必要なファイルはソースコードの他にconfig.m4(UNIX用)とconfig.w32(Windows用)だけだが、pecl-genはテストケースのスケルトンだとかPECLパッケージに必要なpackage.xmlだとか色々な付属品も生成してくれる。
とりあえずここではあまり気にせずコンパイルまで進むことにする。この状態で何も手を加えなくてもmakeまで辿りつけてしまうのがすばらしい。
まずはphpizeコマンドを実行。何も指定せず実行すればいいらしい。

phpize

config.m4やconfig.w32を基にconfigureやmanualなどを生成してくれるので続けざまにconfigureしてみる。

./configure --enable-strsavefile
または
./configure --with-strsavefile=DIR
                            :
                            :
                            :
checking PHP version... configure: error: need at least PHP 4.0.0

なんかエラーが出た。PHP 4以上じゃないとダメらしい。いやいや、PHP 5.1.6が入ってるっての。
エラーメッセージを頼りに調べてみるとconfig.m4にチェックルーチンが書かれていて、どうやらphp_version.hを読み込んでPHP_VERSION_IDを見ているようだ。
ところが自分の環境のphp_version.hにはPHP_VERSION_IDが定義されていなかった。なんだこりゃ。


調べてみたらどうやらPHP 5.2から密かに追加された定数らしい。
というわけで、回避策としてはphp_version.hにPHP_VERSION_IDを追加してやるか、config.m4のチェックルーチンをPHP_MAJOR_VERSIONとか昔からある定数でチェックするように書き換えるという方法が考えられる。
ヘッダをいじってしまうのはあまりよろしくない気がするので、面倒だが毎回config.m4を修正することにした。
何もしなくてもmakeまで行けるとか言ったのが嘘になってしまったじゃないか。


config.m4の次のような箇所を

#if PHP_VERSION_ID < 40000
#error  this extension requires at least PHP version 4.0.0
#endif

次のような感じにしてみた。

#if PHP_MAJOR_VERSION < 4
#error  this extension requires at least PHP version 4.0.0
#endif

これでconfigureが通るようになったはずなので、無事にmakeまでたどり着ける。

./configure --enable-strsavefile
または
./configure --with-strsavefile=DIR
make

組み込んでみる

makeが通ればmodulesディレクトリの中に.soファイル(ここではstrsavefile.so)が出来上がっているはず。
これをextensionを格納するディレクトリに放り込んでもいいし、make installすればその辺も勝手にやってくれる。


あとはphp.iniにstrsavefile.soを追加してphpinfo()を実行すれば、strsavefileの情報が追加されていることが確認できる。
定義ファイルをきちんと書いていればそれなりの情報が表示される。


こんな感じでPHPからも呼び出せるようになっている。ちょっと感動。

<?php
    $result = strsavefile('PHPのエクステンションを作ろう!', 'strsavefile_test.txt');
?>


以上、実際の経験になぞって書いたら長くなってしまったが、基本的にはこんな感じでエクステンションの作成が行えた。
自分みたいなエンド寄りのプログラマがわざわざエクステンションを作ることはあまりないかもしれないが、Cライブラリでしか提供されていない機能をPHPで使う必要が出てきた際には役に立ちそうだ。