複数ファイルを使った中規模 XS の開発
複数ファイルをビルド対象とした XS の開発について,あまり手間をかけないシンプルで効果的な方法を書いていきます。自分の経験をもとに書きますのでベストプラクティスではありませんが。
余談(SV*()
マクロの sideeffect にやられた話)
state_stack
という配列に state
を整数値でスタックとして格納していて,スタックから過去の state
を取り出そうとしたんです。
んで(わりと素直に)
state = SvIV(av_pop(state_stack));
と書いたら,2つずつスタックから pop されてしまいました*1。
XS hacker なら当たり前な話ですけど,SV*()
というのは一見関数然としてますがマクロなんです。
#define SvIV(sv) (SvIOK(sv) ? SvIVX(sv) : sv_2iv(sv))
こんな形で,いずれのケースにせよ引数 sv
が2回評価されてしまいます。
これを避けるためには SvIVx()
という "sideeffect" のないマクロを使う必要があります。gcc でビルドしている場合には
#define SvIVx(sv) ({SV *_sv = (SV*)(sv); SvIV(_sv); })
のように GCC プリプロセッサ拡張を使って一時変数を経由する形になっています。gcc を使わない場合には,
#define SvIVx(sv) ((PL_Sv = (sv)), SvIV(PL_Sv))
のようにグローバル変数を経由しています*2。いずれにせよ sv
は一度しか評価されません。
いまのご時世だと gcc 版の SvIVx()
で十分最適なコードを吐いてくれると思うのですが,これを SvIV()
の定義としていないのは後方互換性もあるのでしょう。
なお,このへんはきちんと perlapi に書いてあります。
- SvIV
Coerces the given SV to an integer and returns it. See SvIVx for a version which guarantees to evaluate sv only once.
IV SvIV(SV* sv)
- SvIVx
Coerces the given SV to an integer and returns it. Guarantees to evaluate sv only once. Only use this if sv is an expression with side effects, otherwise use the more efficient SvIV .
IV SvIVx(SV* sv)perlapi - perldoc.perl.org
guarantees to evaluate sv only once とか an expression with side effects とかきちんと書いてあって頭ではわかってたつもりですが,スマートに書こうと思ってはまってしまいました。
基本 (Module::Install
を使う)
閑話休題。
たとえば Acme::Example
という XS モジュールを開発している場合。普通に h2xs -n Acme::Example
のようにして枠組みを生成すると Example.xs
というファイルが XS 用ファイルとして生成されます。
で ExtUtils::MakeMaker
を使った Makefile.PL
が生成されます。そのままでもいいんですけど,Module::Install
使ったほうが見目麗しいのでそのような Makefile.PL
を書いてみます。ミニマムには以下のような感じ?
use inc::Module::Install; name 'Acme-Example'; all_from 'lib/Acme/Example.pm'; cc_inc_paths '.'; cc_files 'Example.c'; can_cc or die 'This module requires C compiler.'; auto_include; WriteAll;
このように cc_files
に Example.c
と書いておきます。拡張子が .xs
ではなく .c
となっていることに注意してください。Example.xs
が存在した場合に xsubpp
というトランスレータをかましてくれて Example.c
が自動生成されます(という内容の Makefile
になります)。Example.xs
を修正して make
した場合でも自動的に xsubpp
をやり直してくれるようになります。
他の C ファイルもビルドに含めたい
複数のファイルをビルドに含めたい場合があります。たとえば XS で使いたいライブラリをバンドルしたりとか。そんなときは cc_files
にそれらのファイルを追加すれば OK です。
cc_files 'foo.c', 'bar.c', 'baz.c', 'Example.c';
もっと簡単には glob()
を使ってファイル群を指定すればいいですね。たとえば YAML::Syck
の Makefile.PL
では下記のようにしています。
cc_files (glob("*.c"), (-e 'Syck.c' ? () : 'Syck.c'));
Syck.c
に限ってちょっと複雑なことをしています。YAML-Syck
ディストリビューションを展開した時点では Syck.xs
のみ存在して Syck.c
は存在しません。ですが Makefile.PL
を再生成する状況になった場合,Syck.c
が存在します。なので重複をさけるため上記のようなコードになっているんですね。
今回の場合,単純化するために下記の例を使います。
cc_files glob('*.c'), 'Example.c';
これでも Perl Module を配布する場合にはとくには問題にならないでしょう。
他ディレクトリの C ファイルをビルド対象に含めたい
いままでは,ディストリビューションのトップディレクトリにフラットに foo.c
,bar.c
というファイルが存在することを想定していました。
ではたとえば src/mylib/foo.c
,src/mylib/bar.c
のように別ディレクトリに C ソース群を用意していた場合はどのようにすればよいのでしょうか。単純に考えると
cc_files glob('src/mylib/*.c'), 'Example.c';
でいい気がします。しかしこれではうまくいきません。
この記述でビルドをおこなうと,src/mylib/foo.c
のオブジェクトファイルが,ディストリビューションのトップディレクトリに foo.o
として生成されます。それだけならいいのですが,いざ *.so
にリンクする際に src/mylib/foo.o
というオブジェクトをリンクしようとして「存在しない」と怒られてしまいます。
このようにトップディレクトリにオブジェクトファイルが生成される理由は(生成された)Makefile
の下記の記述によるものです。
.c$(OBJ_EXT): $(CCCMD) $(CCCDLFLAGS) "-I$(PERL_INC)" $(PASTHRU_DEFINE) $(DEFINE) $*.c
$(OBJ_EXT)
は Unix では .o
になります。また $(CCCMD)
の部分に -c
オプションが含まれます。しかし,-o
オプションが含まれないので,オブジェクトコードの出力先がカレントディレクトリ……すなわちトップディレクトリになってしまうんですね。
理想としては,
.c$(OBJ_EXT): $(CCCMD) $(CCCDLFLAGS) "-I$(PERL_INC)" $(PASTHRU_DEFINE) $(DEFINE) -o $@ $*.c
のようになっていることが望ましいです。このように Makefile
を書き換えることにしましょう。Makefile.PL
の生成する Makefile
をいじるためには,ExtUtils::MakeMaker
の機能を使います。
具体的には,Makefile.PL
の末尾あたりに下記のコードをつけくわえます。
package MY; sub c_o { my $src = shift->SUPER::c_o(@_); my ($from, $to) = ('$(DEFINE) $*.', '$(DEFINE) -o $@ $*.'); $src =~ s{\Q$from\E}{$to}gxms; return $src; }
MY
というパッケージ名に関数を定義すると((まぁ sub MY::c_o
とすればいいんですけど。)) ExtUtils::MakeMaker
の挙動を修正することができます((Module::Install
は下位で ExtUtils::MakeMaker
を使っているのでこれでもうまく働きます。))。c_o
という関数は,先ほどの .c$(OBJ_EXT):
のような *.c
→ *.o
ターゲット出力部分をフックします。
ここでは SUPER::c_o
を呼び出してフィルタリングしてますが,もちろん自力で全文書き出しても構いません。
要所のみ抜き出して全体像がわからなくなってしまったかと思いますので,Makefile.PL
全文を以下に記します。
use inc::Module::Install; name 'Acme-Example'; all_from 'lib/Acme/Example.pm'; cc_inc_paths '.'; cc_files glob('*.c'), 'Example.c'; can_cc or die 'This module requires C compiler.'; auto_include; WriteAll; package MY; sub c_o { my $src = shift->SUPER::c_o(@_); my ($from, $to) = ('$(DEFINE) $*.', '$(DEFINE) -o $@ $*.'); $src =~ s{\Q$from\E}{$to}gxms; return $src; }
ほかの手は?
さきほどは MY
パッケージ名前空間に c_o
という関数を定義しましたが,postamble
という関数を定義すると,Makefile
の最後に出力結果が付与されます。これで好きなように依存関係やビルド対象を記述できるというわけですね。
もっというと,別ディレクトリで静的ライブラリをビルドし(そしてその静的ライブラリをターゲットとして $(MAKE)
するよう postamble
に記述する)XS ライブラリにリンクするようにするとよりスマートになります。一番シンプルな例は Cache::Memcached::Fast の Makefile.PL ですかね。より複雑な例は Memcached::libmemcached の Makefile.PL があげられると思います。
このアプローチも綺麗は綺麗なんですが,ややプラットフォーム依存性が強くなってしまうのが難点かもしれません。
複数の XS ファイルをビルドしたい
sender.xs
, receiver.xs
という 2 つの XS ファイルがあったとします。今までと同様ふつうに
cc_files glob('*.c'), 'sender.c', 'receiver.c';
のようにすれば両者とも XS code としてビルドしてくれます。
しかし,たとえば sender.xs
が
/* sender.xs */ MODULE=Acme::Example PACKAGE=Acme::Example::Sender void hoge()
receiver.xs
が
/* receiver.xs */ MODULE=Acme::Example PACKAGE=Acme::Example::Receiver void fuga()
のようになっている場合……つまり,両者の MODULE
が一致している場合には問題となります。具体的には,リンク時に boot_Acme__Example
というシンボルが重複しているという warning が出力されるはずです。
なぜかというと,各 XS code ごとに BOOTSTRAP コードが生成されるのですが,それが
#ifdef __cplusplus extern "C" #endif XS(boot_Acme__Example); /* prototype to pass -Wmissing-prototypes */ XS(boot_Acme__Example) { dXSARGS; char* file = __FILE__; XS_VERSION_BOOTCHECK ; newXS("Acme::Example::Sender::hoge", XS_Acme__Example__Sender__hoge, file); XSRETURN_YES; }
のようになっているから。この boot_*
という部分は MODULE=*
で指定したモジュール名をもとに算出されるので,同じ MODULE=
を指定している XS code 間でバッティングが発生するのですね。
解決方法の一つめ。分離した実装コードを #include
で読み込む。つまり,
MODULE=Acme::Example PACKAGE=Acme::Example::Sender #include "sender.h" MODULE=Acme::Example PACKAGE=Acme::Example::Receiver #include "receiver.h"
のような形態にします。
悪くはないんですが,開発中だと sender.h
や receiver.h
を更新したとしても make
がそれを検出できず((いままでの過程で生成された Makefile
だとヘッダ等の依存関係まで記述されていないからです。さきほどの postamble
を利用して依存関係を記述すれば解決可能だと思います。)),テストビルド中に手数が増えてしまいます。
解決方法の二つめ。MODULE=
に指定してやるモジュール名を変えてやる。先ほどの例だと MODULE=*
が共通で PACKAGE=*
が違っていましたが,そもそも MODULE=*
の部分を変えてやればよい。
sender.xs
は
/* sender.xs */ MODULE=Acme::Example::Sender void hoge()
のようにして receiver.xs
は
/* receiver.xs */ MODULE=Acme::Example::Receiver void fuga()
のようにしてやると BOOTSTRAP コード部分のシンボルが衝突しません。
しかしこのままだと,XS ライブラリコードを読み込んでも XS で定義した関数を Perl から呼び出すことができなくなってしまいます。なぜなら,上記の boot_Acme__Example
関数をみていただければわかりますが,この BOOTSTRAP コードで Perl のシンボルテーブルに XS 関数を登録しているからなんですね。BOOTSTRAP コードをわけたものの,それらが *.so
の読み込み時に呼び出されないため XS 関数がシンボルテーブルに登録されないのです。
そこで,マスターとなる XS code module をでっちあげて,そこから各モジュールの BOOTSTRAP コードを呼び出すようにしてあげましょう。
/* XS.xs */ MODULE=Acme::Example BOOT: boot_Acme__Example__Sender(aTHX_ cv); boot_Acme__Example__Receiver(aTHX_ cv);
まぁー Acme::Example::Sender
の BOOTSTRAP コードにまぜてもいいんですが,今回の例だとマスター XS モジュールを用意したほうが名前空間的には綺麗でしょう。cv
の部分がちと暗黒魔法的ですが,個人的にはこのようにしています。
解決方法の三つめ。これは厳密には解決方法ではないのですが,typemap
をきちんと使ってやると XS code 自体は非常にシンプルになる(引数と戻り値と関数名を記述するだけ)ので,XS code は1ファイルにまとめる,という方法です。
/* XS.xs */ MODULE=Acme::Example PACKAGE=Acme::Example::Sender void hoge() MODULE=Acme::Example PACKAGE=Acme::Example::Receiver void fuga()
のようにして,sender.c
と receiver.c
にはおのおの
/* sender.c */ void hoge() { /* blah, blah, blah, ... */ }
と
/* receiver.c */ void fuga() { /* blah, blah, blah, ... */ }
のように,通常の C スタイルで書く。
これが一番綺麗ですが,引数(や戻り値)を自分で解釈する際には結局 sender.c
や receiver.c
がどんどん汚くなりますね。また本来 static
記憶クラスに封じ込めておけた関数がグローバルになってしまうというトレードオフもあります。