実例で学ぶPHP拡張モジュールの作り方

第2回Hello, PHP Extension!

今回から実際に拡張モジュールを作っていきます。まずは定番の⁠Hello, World!⁠から始めましょう。

はじめてのspecファイル

CodeGen_PECLを使った拡張モジュールの開発には、まずXML形式で仕様を記述したspecファイルを書かねばなりません。リスト1に基本的なspecファイルの内容を示します。

リスト1 helloworld.xml
<?xml version="1.0" encoding="UTF-8"?>
<extension name="helloworld" version="1.0.0">
  <summary>Hello, Extension World!</summary>
  <description><?data
This is my first PHP extension module.
  ?></description>

  <maintainers>
    <maintainer>
      <user>rsk</user>
      <name>Ryusuke Sekiyama</name>
      <email>rsky0711@gmail.com</email>
      <role>lead</role>
    </maintainer>
  </maintainers>

  <license>PHP</license>

  <channel>__uri</channel>

  <release>
    <version>1.0.0</version>
    <date>2008-01-21</date>
    <state>stable</state>
    <notes><?data
- Initial release.
    ?></notes>
  </release>

  <function name="helloworld" role="public">
    <proto>void helloworld()</proto>
    <summary>Hello, World!</summary>
    <description><?data
Outputs "Hello, World!" and linebreak.
    ?></description>
    <code><?data
php_printf("Hello, World!%s", PHP_EOL);
    ?></code>
    <test>
      <code><?data
helloworld();
      ?></code>
      <result mode="plain"><?data
Hello, World!
      ?></result>
    </test>  
  </function>
</extension>

このようにspecファイルではXML宣言の後にルート要素として<extension>タグを配置し、その中にモジュール情報や関数定義を記述します。モジュール名は<extension>タグのname属性として記述します。また、<extension>タグにversion属性がないとpecl-genが警告を出すのですが、実際はversion属性は無視され、代わりに<release>タグの中の<version>タグの内容がモジュールのバージョン番号として扱われます。

モジュール情報

モジュールの説明は<extension>タグ直下の<summary>タグに概要を、<description>タグに詳細な内容を記述します。これら2つのタグは関数、クラス、メソッドなどの説明にも使われます。

加えて<maintainers>タグにモジュールのメンテナ、<license>タグにライセンス、<release>タグにバージョンなどの情報を記述します。バージョンが省略された場合は0.0.1develとみなされます。このほかに<changelog>タグの中に複数の<release>タグを書くことで更新履歴を表すこともできます。これらの情報はPEARパッケージを作るためのpackage.xmlにも反映されます。各タグの詳しい説明は付録Aをご覧ください。

関数定義

関数の定義は<function>タグで記述します。リスト1では1つだけしかありませんが、<extension>タグの中には複数の<function>タグを書くことができます。関数を提供しないモジュールなら<function>タグを省略しても構いません。

関数名は<function>タグのname属性で指定します。これは必須の属性です。role属性は省略可能で、デフォルト値はリスト1にあるのと同じく⁠public⁠です。role属性の値にはほかに⁠internal⁠を指定してモジュール初期化時の処理などを記述できますが、本連載では、その手の処理が必要な場合は生成されたソースコードを直接編集します。

ここで、リスト1からhelloworld()関数を定義している部分を抜粋します。

リスト2 helloworld.xmlより関数定義部を抜粋
  <function name="helloworld" role="public">
    <proto>void helloworld()</proto>
    <summary>Hello, World!</summary>
    <description><?data
Outputs "Hello, World!" and linebreak.
    ?></description>
    <code><?data
php_printf("Hello, World!%s", PHP_EOL);
    ?></code>
    <test>
      <code><?data
helloworld();
      ?></code>
      <result mode="plain"><?data
Hello, World!
      ?></result>
    </test>  
  </function>

リスト2にあるように、関数の引数や戻り値に関する情報は<proto>タグに記述します。この情報を元にマニュアルやリフレクション情報、引数をパースするためのコードが生成されます。今回はただ⁠Hello, World!⁠を表示するだけの関数なので、戻り値はvoid、引数なしにしています。戻り値や引数の指定方法については次回に紹介します[1]⁠。

関数の実装は<code>タグに記述します。今回はprintf(3)のZend API版であるphp_printf()関数で⁠Hello, World!⁠を出力しています。PHP_EOLマクロはPHPのPHP_EOL定数と同じく改行コードを表す定数で、UNIX系ならLF("\n"⁠⁠、WindowsではCR+LF("\r\n")です[2]⁠。

今回はspecファイルに関数の実装を書きましたが、関数が増えたり、その内容が複雑になってくるとspecファイルの見通しが悪くなりますし、C言語としてのキーワード補完などエディタのサポートも受けられません。そこで、次回からはspecファイルに書くのはプロトタイプまでに留め、実装はpecl-genで生成されたソースコード上に記述していきます。

<test>タグではmake testで実行されるテストケースを記述します。<code>タグの内容が実行されるPHPコード、<result>タグの内容が期待される出力です。出力結果の最初と最後のホワイトスペースは無視され、改行コードはLFに統一して評価されます。<test>タグを省略してもテストケースの雛形は生成されるので、後からテストケースを書いても(あるいは書かなくても)構いませんが、筆者は関数のプロトタイプと期待される結果はセットでspecファイルに書くほうが良いと考えます。

XML特殊文字について

ところでリスト1には内容が <?data ?> という処理命令で囲まれているタグがあります。これは内容をCDATA(構文解析されない文字データ)として扱うCodeGen_PECL独自の命令で、CDATAセクション(<![CDATA[ ]]>)と同じはたらきをします。構文解析されないということは <, >, &, " などの特殊文字をそのまま書けるということなので、特殊文字を含む内容は <?data ?> または <![CDATA[ ]]> で囲んでおくと見やすくなります。本連載では<description>や<code>などの内容が複数行にわたる可能性があるタグの内容は <?data ?>で囲み、インデントしないものとします。

ソースコードを生成する

specファイルの用意ができたら、次はpecl-genコマンドでソースコードやその他のファイルを生成します。pecl-genコマンドの基本的な使い方は、引数としてspecファイルのパスを与えるだけです。

操作1 pecl-genを実行
$ pecl-gen helloworld.xml
Creating 'helloworld' extension in './helloworld'

Your extension has been created in directory ./helloworld.
See ./helloworld/README and/or ./helloworld/INSTALL for further instructions.

さらに続けてlsコマンドで生成されたファイルを確認してみると、操作2のようになりました。これを分類すると図1のようになります。specファイルの内容によっては生成されるファイルの構成が操作2と異なる場合もあります。

操作2 pecl-genで生成されたファイル一覧
$ ls helloworld
CREDITS          config.m4        helloworld.dsp   package.xml      tests
LICENSE          config.w32       helloworld.xml   package2.xml
README           helloworld.c     manual           php_helloworld.h
図1 pecl-genで生成されたファイルの分類
図1 pecl-genで生成されたファイルの分類

マニュアルやpackage.xmlまで生成されているのが興味深いですね。ただしマニュアルといっても、そのままWebブラウザで見られるHTML形式のものではなく、マニュアルの元となるファイル群です。これをコンパイルしてHTML、CHM、PDFなどの形式のマニュアルを生成します。今回はマニュアル生成までは行いませんが、この連載中にはマニュアルやPEARパッケージを制作する回を設ける予定です。

ビルド、テスト、インストール

さてpecl-genの次はモジュールをビルドします。ここから先の作業は最初にphpizeコマンドでconfig.m4からconfigureスクリプトやMakefile.inなどを生成し、configure、make、make installというように、一般に配布されているPHP拡張モジュールのインストール方法とまったく同じです。

標準以外のパスにインストールされているPHPに対して拡張モジュールをビルドしたい場合は、phpizeをフルパスで指定し、configureの引数に --with-php-config=/path/to/php-config を追加します。php-configコマンドはphpizeコマンドと同じディレクトリにインストールされているPHPのビルド情報を取得するためのスクリプトです。

操作3 helloworldモジュールをビルド
$ cd helloworld
$ phpize
Configuring for:
PHP Api Version:         20041225
Zend Module Api No:      20060613
Zend Extension Api No:   220060519
$ ./configure --enable-helloworld
(省略)
$ make
(省略)
Build complete.
Don't forget to run 'make test'.

無事ビルドできたら、さっそくインストール、と行きたいところですが、その前にちゃんとhelloworld()関数が動作するかテストします。

このときphp.iniのextension_dirディレクティブの値が⁠modules/⁠(カレントディレクトリにあるmodulesディレクトリ)で上書きされるので、php.iniのextensionディレクティブで拡張モジュールを読み込むようにしていると、モジュールが読み込めなかった旨のエラーが出力されてテストが失敗します。make testの際にはphp.iniのextensionディレクティブをすべてコメントアウトしておいてください。

操作4 テストを実行
$ make test
(省略)
=====================================================================
Running selected tests.
PASS helloworld() function [tests/helloworld.phpt] 
=====================================================================
Number of tests :    1                 1
Tests skipped   :    0 (  0.0%) --------
Tests warned    :    0 (  0.0%) (  0.0%)
Tests failed    :    0 (  0.0%) (  0.0%)
Tests passed    :    1 (100.0%) (100.0%)
---------------------------------------------------------------------
Time taken      :    0 seconds
=====================================================================

テストが成功したら、いよいよインストールです。

操作5 インストール
$ sudo make install
Installing shared extensions:     /usr/local/lib/php/extensions/no-debug-non-zts-20060613/

Hello, World!

インストールできたら、あとはphp.iniにhelloworldモジュールを読み込むディレクティブ⁠extension=helloworld.so⁠を追加し、WebサーバやFastCGIプロセスを再起動すれば、helloworld()関数が利用できるようになります。

今回はphp.iniの編集やWebサーバ再起動を行わずにコマンドラインでhelloworld()関数を使ってみます。

操作6 Hello, World!
$ php -d extension=helloworld.so -r 'helloworld();'
Hello, World!

make testで確認済みとはいえ、実際に見てみると感慨深いですね[3]⁠。

おわりに

これで基本的なspecファイルの書き方と、拡張モジュールのインストール方法が理解していただけたと思います。

実は<extension>タグの子要素はすべて省略しても構わないのですが、今回は解説のため詳しく書きました。specファイルの内容はソースコードだけでなく、自動生成されるマニュアルなどにも反映されるので、モジュールを公開する予定があるなら、なるべく詳しく書くことをお勧めします。逆に自分専用のモジュールを作る場合は<description>タグやリリース情報は一切書かず、本体の開発に専念するのもありでしょう。

本連載で紹介するspecファイルもできる限り詳しい記述をするように務めますが、ページの都合で記事中では省略することがあります。specファイルおよびソースコード一式は本サイトからダウンロードできるようにしますので、そちらと合わせて読んでいただければと思います。

次回はOpenCVライブラリを使ってWEBカメラの画像をキャプチャする拡張モジュールを制作し、外部ライブラリとリンクする方法、引数を取り値を返す関数の作り方などを紹介します。

サンプルファイルのダウンロード

helloworld.xml.txt(specファイル)

次のページは今回使ったCodeGen_PECLタグやZend APIの一覧表です。

付録A 今回使用したCodeGen_PECLタグ

以下に今回使ったCodeGen_PECLタグの一覧を示します。ほかのタグの説明は今後の連載で使うたびに書きますが、あまり使いそうにないタグは紹介しないかもしれません。すべてを知りたい方はspecファイルのDTDをご参照ください。

表A-1 ルート要素<extension>の属性
属性名省略可説明
name × モジュール名。
version バージョン番号。省略すると警告が出るが、書いても無視される。
表A-2 ルート要素<extension>直下のタグ
タグ名省略可内容または子要素となるタグの説明
summary モジュールの概要。
description モジュールの説明。
maintainers 1つ以上の<maintainer>タグで各メンテナの情報を記述する(<maintainer>の子要素については表A-3を参照⁠⁠。
license ライセンスの種類。BSD、LGPL、PHPのいずれかで、GPLを選ぶとPHP本体のライセンスと矛盾するのでエラー。省略すると⁠unknown⁠になる。
channel PEARパッケージのチャンネル。デフォルト値は⁠pecl.php.net⁠⁠。
release リリース情報(子要素は表A-4を参照⁠⁠。
changelog 更新履歴。過去のバージョンの<release>が子要素になる。
function 関数定義(属性は表A-5、子要素は表A-6を参照⁠⁠。
表A-3 <maintainer>の子要素
タグ名省略可説明
user CVSアカウントなどの短いユーザ名。デフォルト値は⁠unknown⁠⁠。
name フルネーム。デフォルト値は⁠Anonymous Coward⁠⁠。
email メールアドレス。デフォルト値は⁠unknown@example.org⁠⁠。
role 役割。lead、developer、contributor、helperのいずれかで、省略するとunknownになる。PEARパッケージ作成には最低1人の⁠lead⁠が必要。
表A-4 <release>の子要素
タグ名省略可説明
version バージョン番号。ソースコードにも反映される。デフォルト値は⁠0.0.1⁠⁠。
date リリースの日付。省略するとpecl-genを実行した日時になる。
state リリースの状態。stable、beta、alpha、devel、snapshotのいずれか。デフォルト値は⁠devel⁠で、⁠stable⁠以外の場合は安定版でないことを示すEXPERIMENTALファイルが生成される。
notes リリースノート。
表A-5 <function>の属性
属性名省略可説明
name × 関数名。
role public(PHPスクリプトから使える関数)かinternal(モジュール内部の特殊関数)のどちらか。デフォルト値は⁠public⁠⁠。
表A-6 <function>の子要素
タグ名省略可内容または子要素となるタグの説明
summary 関数の概要。
description 関数の説明。
proto 関数のプロトタイプ。戻り値と引数を特定の書式にしたがって記述する(詳しくは次回⁠⁠。
code 関数の実装をC言語(またはC++)で記述する。引数をパースするコードはプロトタイプにしたがって自動生成される。
test テストケース(子要素は表A-7を参照⁠⁠。
表A-7 <test>の子要素
タグ名省略可内容または子要素となるタグの説明
code × テスト内容のPHPコード。
result 期待される出力。デフォルト値は⁠OK⁠⁠。mode属性で結果の評価方法を指定できる(mode属性に指定する値は表A-8を参照⁠⁠。
表A-8 <result>タグのmode属性
結果の評価方法
plain 内容が出力と等しいかを調べる。
format 内容はフォーマット文字列として扱われ、出力がフォーマットにマッチするか調べる。フォーマットはsscanf()関数のような書式だが、正規表現に変換して評価される。/ はエスケープしなくてよい。
regex 内容は正規表現として扱われ、出力が正規表現にマッチするか調べる。正規表現中の / はエスケープしないといけない。

付録B 今回使用したZend API

Zend APIには、今回使ったphp_printf()のほかに、文字列やバイナリデータをそのまま出力するための関数php_body_write()とphp_header_write()があります。両者の違いはPHPの出力バッファリングの影響を受けるか否かですが、通常はphp_body_write()を使います。いずれも戻り値は出力されたデータの長さ(バイト数)です。

表B-1 出力用関数
定義説明
int php_printf(const char *format, ...) フォーマットに従って文字列を出力する(バッファリングあり⁠⁠。
フォーマットの書式と対応する引数の型はprintf(3)とほぼ同じ[4]⁠。
int php_body_write(const char *str, uint str_length TSRMLS_DC) 長さを指定してデータを出力する(バッファリングあり⁠⁠。
int php_header_write(const char *str, uint str_length TSRMLS_DC) 長さを指定してデータを出力する(バッファリングなし⁠⁠。

また、php_body_write()およびphp_header_write()のラッパーとして表B-2、表B-3のようなマクロも用意されています。

表B-2 出力用マクロ(バッファリングあり)
定義説明
int PHPWRITE(const char *str, uint str_length) 長さを指定してデータを出力する。
PUTS(const char *str) ヌル終端文字列を出力する。
do-whileブロックとして定義されており、戻り値を得ることはできない。
char PUTC(char c) 1文字出力する。cは変数でないといけない。戻り値はc。
表B-3 出力用マクロ(バッファリングなし)
定義説明
int PHPWRITE_H(const char *str, uint str_length) 長さを指定してデータを出力する。
PUTS_H(const char *str) ヌル終端文字列を出力する。
do-whileブロックとして定義されており、戻り値を得ることはできない。
char PUTC_H(char c) 1文字出力する。cは変数でないといけない。戻り値はc。

付録C TSRM(Thread Safe Resource Manager)について

付録BでTSRMLS_DCというマクロが出てきました。Zend APIにはTSRMというスレッドセーフにメモリなどのリソースを管理するためのリソースマネージャがあり、TSRMLS_DCはTSRMのスレッドローカルストレージを受け渡しするためのマクロです。TSRMLS_DCとは⁠Thread Safe Resource Manager Local Storage, Declaration Comma⁠の頭文字をとったもので、TRSMLS_CC(Thread Safe Resource Manager Local Storage, Call Comma)と対になってそれぞれ定義側、呼び出し側でほかの引数に続けて、区切りのコンマ無しで記述されます。コンマを必要としないのは名前が示すように、コンマもマクロに含まれるからです。ほかに引数をとらない関数の定義および呼び出しにはそれぞれTSRMLS_DとTSRMLS_Cが使われます。

Windows版以外のPHPはマルチスレッド非対応でコンパイルされることが多く、このときTSRM系マクロの定義は空(TSRMLS_Dはvoid)となっており、書き忘れてもコンパイルでき、正常に動作しますが、マルチスレッド対応(ZTS、Zend Thread Safe)モードではコンパイルできません。先に紹介したPHPWRITE()のように、よく使うAPIがTSRMを意識せずに使えるようになっているのは、単に記述を短くするためだけでなく、このような不具合を予防する目的があってのことだと考えられます。

おすすめ記事

記事・ニュース一覧