複数ファイルを使った中規模 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_filesExample.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::SyckMakefile.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.cbar.c というファイルが存在することを想定していました。

ではたとえば src/mylib/foo.csrc/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.hreceiver.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.creceiver.c にはおのおの

/* sender.c */
void
hoge()
{
	/* blah, blah, blah, ... */
}

/* receiver.c */
void
fuga()
{
	/* blah, blah, blah, ... */
}

のように,通常の C スタイルで書く。

これが一番綺麗ですが,引数(や戻り値)を自分で解釈する際には結局 sender.creceiver.c がどんどん汚くなりますね。また本来 static 記憶クラスに封じ込めておけた関数がグローバルになってしまうというトレードオフもあります。

*1:問題はそのこと自体になかなか気づかなかったということなのですが。

*2:旧スレッドモデルを使わない場合は sv_iv() という関数を呼び出すようになっています。