SWIGのタイプマップについて調べているのですが、Tclのコマンドを作るときに、引数の省略やリストの受け渡しといった、気のきいたインターフェースを作るにはなかなかややこしくて、結局タイプマップファイルと生成されたラッパーコードを比較しながらうまいやり方を見つけていくしかないのかなあと思います。で、色々と考察したいと思います。まずは、引数にリストを渡す場合を考えたいと思います。
%sum { 1 2 3 4 5} 1 2 3 4 5 = 15
と、このようにリストの値を合計するようなコマンドを作りたいと思います。Cのコード test.c はこんな風に普通に書きます。
void sum(int list[], int len) { int i; int sum=0; for (i=0; i<len; i++){ sum += list[i]; printf("%d ", list[i]); } printf("= %d\n", sum); }
で、インターフェースファイル test.i はこんな感じになります
%module test %typemap(in) (int list[], int len) { int i; Tcl_Obj *tclobj=NULL; /*リストオブジェクトの長さの取得*/ if(Tcl_ListObjLength(interp, $input, &$2)==TCL_ERROR){ return TCL_ERROR; } /*リストオブジェクトを新しいint配列に変換*/ $1 = (int*) malloc($2 * sizeof(int)); for(i=0; i<$2; i++) { Tcl_ListObjIndex(interp, $input, i, &tclobj); Tcl_GetIntFromObj(interp, tclobj, $1+i); } } %typemap(freearg) (int list[], int len) { if ($1) free($1); } extern void sum(int list[], int len);
%typemap(in) (int list[], int len)でCのsum関数に対応した引数を作ってます。$1がint list[] 、$2がint lenに対応しています。実際に生成されるラッパーコードでは、$1はint *arg1、$2は int arg2に置き換えられてtypemap(in)のコードが埋め込まれ、次に本体のコードがsum(arg1, arg2)という風に呼び出されて、最後にtypemap(freearg)のコードで確保したメモリを解放するというわけです。 で、こいつからswig -tcl test.iとかやると、test_wrap.cとか出来るので、適当にコンパイルしてやればOKです。
%load test %sum { 1 2 3 4 5} 1 2 3 4 5 = 15 % sum 1 1 = 1
まあ、こんなもんでしょうかね・・・。それじゃあ・・・
% sum 1 2 Wrong # args.:sum list ?len? argument 2 % sum {1 2 a} 1 2 1735289204 = 1735289207 expected integer but got "a" % sum a 0 = 0 expected integer but got "a"
引数は1つしか取らないのでエラーになるのはいいんですが、?len?というのはlenという引数が省略できますよというなので、?list?の部分は無い方がいいんですが・・・。どうやって引数のエラーメッセージを書き換えるんだろう・・・。あと、文字列も数値になっちゃうのも、まあ現状の変換部分で書いてないので構わないんですが、expected integer but got "a"のエラーメッセージはどこから出てきてるのか追っかけないといけない(のがめんどくさい)ような。 expected integer but got "a"のようなエラーはたぶんResultがセットされてないからだと思うんですが、sumがvoidだからって何もSetResultしてくれないというのはちょっと・・・。catch {sum {a}} msg のようなエラー処理ができないもんですかね・・・。つーかこれもタイプマップで書かないとダメなのか。ということは、リストオブジェクトをまず文字列の配列にして数値変換かましていけばいいのかな。
タイプマップで複数の引数を取る時のwrong argsのエラーメッセージがなんとなくバグなような気がしないでもないが、SWIGにあまり詳しくないので何か間違えているのかもしれないけど、やっぱり納得がいかんので、普通に省略可能な引数を使ったサンプルを書いてみる。 add a ?b? のようなa+bをするけどbは省略可能というのを考える。
test.c
int add(int a, int b) { return a+b; }
test.i
%module test %typemap(default) int b { $1 = 0; } extern int add(int a, int b);
typemap(default)というのは、デフォルト値を設定するというやつ。そのままなんですが、で、これをコンパイルすると
% load test % add Wrong # args. :add a ?b? argument 1 % add 1 2 3 % add 1 1 % add a expected integer but got "a":add a ?b? argument 1 %
うーん、問題がないようだ。ちゃんと省略できてるし、エラーメッセージも正しい・・・なにか特別なオプションがあるのだろうか・・・
省略可能引数についてまだやります。引数が多い時はどうなるのかな・・・たとえば、真ん中の引数だけ省略みたいな謎なインターフェースを書いちゃったら・・・
test.c
int add(int a, int b, int c) { return a+b+c; }
test.i
%module test %typemap(default) int b { $1 = 0; } extern int add(int a, int b, int c);
コード生成もコンパイルも通るようです・・・。では実行をは・・・
% load test % add Wrong # args. :add a ?b? ?c? argument 1 % add 1 8994641 % add 1 2 8994643 % add 1 2 3 6
うーん・・・なるほど・・・。まあ当然なんですが・・・。ライブラリを書くときにC関数の引数の名前付けとか順序とかにコツがいるかも。
複数の引数をタイプマップで変換したときの引数のエラーメッセージがなんか変なの結局あきらめました。は1.3.21についているmultimapのサンプルをそのままコンパイルしてみても同様の状態なので・・・。この部分のエラーメッセージはただの文字列として埋め込まれてるだけなので、手で書き換えればいいかな・・・。
引数の省略の問題はまあさておき。。。C++のクラスをTkのようなインターフェースでラップすることもできるらしいです。 test.h
#ifndef DEF_TEST #define DEF_TEST class Test { private: int z; public: Test(); ~Test(); int x; int y; int Add(); int getZ(); void setZ(int num); }; #endif
test.cpp
#include "test.h" #include <stdio.h> Test::Test() { printf("born\n"); } Test::~Test() { printf("killed\n"); } int Test::Add(){ return x + y; } int Test::getZ(){ return z; } void Test::setZ (int num){ z=num; }
test.i
%module test %{ #include "test.h" %} %include "test.h"
これをコンパイルして実行してみます。c++の時は-c++オプションをつけて
swing -c++ -tcl test.i
みたいにします。
% load test % Test t born _a84bca00_p_Test % t configure -x 3 % t configure -y 4 % t configure -z 5 Invalid attribute name. % t configure -x 6 -y 7 % t cget -x 6 % t cget -y 7 % t cget -z Invalid attribute name.
どうやらちゃんとプライベートなメンバにはアクセスできないようだし、コンストラクタも呼ばれているようです。
% t m Invalid method. Must be one of: configure cget -acquire -disown -delete Add getZ setZ
メソッド名を間違えた時も、ちゃんとアクセス可能なメソッド名が一覧表示されるようです。configure とcgetの他の、Add、getZ、setZは自分で定義したもので、-acquire -disown -deleteはクラスの破棄とか所有権を変えるときに使うらしいですが、とりあえず、自分で定義したメソッドを使ってみます。
% t setZ 8 % t getZ 8 % t configure -x 9 -y 10 % t Add 19
普通だ・・・。普通すぎて何も書くことが無い・・・。
SWIGのマニュアルの23.4.2章のメモリ管理のところに具体例つきで詳しく書いてありますが、クラスのメモリ管理がややこしいので、前の続きで書きます。configure、cget以外にあった自分で定義してないメソッド-acquire、-disown、-deleteはメモリ管理に関わるオプションのようです。クラスには所有権というのがあるようで、thisownという(隠し?)オプションで所有権があるときはフラグが立ってるようです。まあ単純に、
% t cget -thisown 1
で調べられます。Tclからだけ参照されていれば所有権があるとされて、他から参照されたりすると所有権が無いとされるようです。所有権の変更はSWIGが勝手にやってくれるようですが、参照関係を把握できないような場合もあるので(オブジェクトが別のオブジェクト(のポインタ)を引数で受けるときとか)、所有権を手で変えられるオプションがついています。
-acquire 所有権を得る。 -disown 所有権を失う。 -delete クラスの破棄。
で、-deleteをするとデストラクタが呼ばれて破棄されます。所有権が無い時に-deleteすると(しちゃダメなんでしょうが・・・)、デストラクタを呼ばずにポインタを失ってしまうようです。 % t -delete でクラスを破棄します。
tclの拡張はパッケージ化して名前空間でコマンドがまとめられてるのがスマートなので、そういう風なのを作ってみます。
test.c
int add(int a, int b) { return a+b; } int sub(int a, int b) { return a-b; }
test.i
%module test extern int add(int a, int b); extern int sub(int a, int b);
パッケージのバージョンはswigでラッパーコードを作成する時に引数でオプションを渡します。
swig -tcl -pkgversion 1.0 -namespace test.i
とまあ、こんな感じです。namespaceはデフォルトではインターフェースに定義したモジュール名、この場合はtestになりますが、-prefixオプションで変更することも出来ます。 コンパイルしてパッケージ化してみます。tclshでパッケージのインデックスファイルを作る必要があります。
% pkg_mkIndex . test.dll
これでpkgIndex.tclが作られます。パッケージのインストールはlibディレクトリ(winだとc:\tcl\libとか、unixだと/usr/shareとか)にパッケージ名と同じ名前のディレクトリを作って、そこにpkgIndex.tclと作成したtest.dll(test.so)を配置するというのが正しい?インストールの仕方なんですが、ここではめんどくさいのでauto_pathに.を追加して適当にこなします(汗)。
% lappend auto_path . C:/Tcl/lib/tcl8.4 C:/Tcl/lib . % package require test 1.0
無事にパッケージが読み込めたみたいです。では使ってみます。
% test::add 1 2 3 % test::sub 3 4 -1 % test::add Wrong # args. :test::adda b argument 1 % test::sub Wrong # args. :test::suba b argument 1
エラーメッセージがおかしいです。test::add a bなんじゃ・・・。うーん、エラーメッセージの生成は鬼門なのかも・・・。見なかったことにしよう・・・。今度はnamespaceのインポートをしてみます。
% namespace import test::* % add 1 2 invalid command name "add"
ありゃ・・・。test_wrap.cを確認してみたらTcl_Exprtしてない。ここも手でかきかえなくちゃダメみたいです。import必要無いというのなら別に書き換えなくてもいいですが・・・。これも何かオプションがあるのかな?
namespace のインポートが出来ないのが腹立たしいのでパッチを作りました。SWIGの生成するラッパーコードは*.swgに定義されてるので、それを書き換えさえすればSWIG本体をコンパイルしなくてもいいみたいです。SWIG\Lib\tcl\tcl8.swgのパッチです。ひょっとしたらタイプマップの再定義とかがインターフェースファイルで出来て、こういうことをしなくても良いのかもしれないけど、まだよくわからない・・・。
--- original_tcl8.swg Fri Dec 12 18:29:00 2003 +++ tcl8.swg Thu Feb 26 19:40:51 2004 @@ -607,6 +607,9 @@ SWIGEXPORT(int) SWIG_init(Tcl_Interp *interp) { int i; static int _init = 0; +#ifdef SWIG_namespace + Tcl_Namespace *nsPtr; +#endif if (interp == 0) return TCL_ERROR; #ifdef USE_TCL_STUBS if (Tcl_InitStubs(interp, (char*)"8.1", 0) == NULL) { @@ -625,13 +628,26 @@ } _init = 1; } + +#ifdef SWIG_namespace + nsPtr = (Tcl_Namespace *)Tcl_FindNamespace(interp, SWIG_namespace, + (Tcl_Namespace *)NULL, TCL_LEAVE_ERR_MSG); + if (nsPtr == NULL) return TCL_ERROR; +#endif + for (i = 0; swig_commands[i].name; i++) { Tcl_CreateObjCommand(interp, (char *) swig_commands[i].name, (swig_wrapper_func) s wig_commands[i].wrapper, swig_commands[i].clientdata, NULL); +#ifdef SWIG_namespace + Tcl_Export(interp, nsPtr, (char *)( swig_commands[i].name + strlen(SWIG_namespace) + 2) , 0); +#endif } for (i = 0; swig_variables[i].name; i++) { Tcl_SetVar(interp, (char *) swig_variables[i].name, (char *) "", TCL_GLOBAL_ONLY); Tcl_TraceVar(interp, (char *) swig_variables[i].name, TCL_TRACE_READS | TCL_GLOBAL _ONLY, (Tcl_VarTraceProc *) swig_variables[i].get, (ClientData) swig_variables[i].addr); Tcl_TraceVar(interp, (char *) swig_variables[i].name, TCL_TRACE_WRITES | TCL_GLOBA L_ONLY, (Tcl_VarTraceProc *) swig_variables[i].set, (ClientData) swig_variables[i].addr); +#ifdef SWIG_namespace + Tcl_Export(interp, nsPtr, (char *)( swig_variables[i].name + strlen(SWIG_namespace ) + 2) , 0); +#endif } SWIG_InstallConstants(interp, swig_constants); %}
それでは前にnamespace のところで作ったやつで試してみます。
% load test % test::add Wrong # args. :test::adda b argument 1 % test::add 1 2 3 % test::sub 1 2 -1
まあ、ここまでは動きます。namespaceをインポートしてみます。
% namespace import test::a* % add Wrong # args. :test::adda b argument 1 % sub wrong # args: should be "subst ?-nobackslashes? ?-nocommands? ?-novariables? string" % add 2 3 5
test::a*だけインポートしたので、addコマンドだけインポートされているのがわかります。
まだやります。タイプマップファイルを眺めていると、typemap.iに色々な便利なタイプマップが定義されているのがわかります。引数にポインタを渡して値を格納したりする時には、int *OUTPUTなどのタイプマップを使うようです。このOUTPUTは引数を省略してくれるんですが、ちょっと試してみたらエラーメッセージもちゃんとしてたので、多引数のマップ(multimap)をマニュアルに書いているような(前にやったような)やりかたを使わないで、やってみたいと思います。
test.c
int sum(int list[], int len) { int i; int sum=0; for (i=0; i<len; i++){ sum += list[i]; printf("%d ", list[i]); } printf("= %d\n", sum); return 0; }
これは前に使ったやつと同じなんですが、前は
%typemap (int list[], int len)
みたいなタイプマップを書きましたが、今回はタイプマップの引数は1つづつ分けて書きます。
test.i
%module test %wrapper %{ int *LEN; %} %typemap(in,numinputs=0) int len { LEN = &$1; } %typemap(in,numinputs=1) int list[] { int i; Tcl_Obj *tclobj=NULL; /*リストオブジェクトの長さの取得*/ if(Tcl_ListObjLength(interp, $input, LEN)==TCL_ERROR){ return TCL_ERROR; } /*リストオブジェクトを新しいint配列に変換*/ $1 = (int*) malloc(*LEN * sizeof(int)); for(i=0; i<*LEN; i++) { Tcl_ListObjIndex(interp, $input, i, &tclobj); Tcl_GetIntFromObj(interp, tclobj, $1+i); } } %typemap(freearg) int list[] { if ($1) free($1); } extern int sum(int list[], int len);
とまあこんな感じ・・・。引数ごとに処理するスコープで共有できるローカル変数の作り方がよくわからなかったので、結局グローバル変数のLENを定義して使っています。うーんたぶん何か方法があるのだろうけどよくわからん。。。
% load test % sum Wrong # args. :sum list argument 1 % sum {1 2 3 4 5} 1 2 3 4 5 = 15 0
エラーメッセージの引数がちゃんと省略されています(目的達成)。うーむ、でもコードがややこしくなりそうな。エラーメッセージはハードコーディングされているので、正規表現で置き換えていってもいいような気がします(弱気)。
例外処理とかのやり方を考えます。%exceptionを使うと、関数の返り値を評価したりすることもできます。SWIGのマニュアルではtypemaps.iを使ったりしてるようですが、これは返り値がリストになるので、Tclっぽくないと思います。リストでエラーコードを返すのではなく、エラーの場合はTcl_Errorを返して、Resultにエラーメッセージを格納すれば、組み込みTclのコマンドのようにcatchでエラーを補足できるはずです。
test.c
int mydiv(int a, int b, int *out) { if (b==0) return 1; *out = a/b; return 0; }
0で除算するとエラーコード1を返す関数です。
test.i
%module test %typemap(in, numinputs=0) int *OUTPUT(int temp) { $1 = &temp; /*実体の確保*/ } %typemap(argout) int *OUTPUT { Tcl_SetObjResult(interp,Tcl_NewIntObj((int) *$1)); } %exception mydiv %{ $action switch (result) { case 0: /* OK */ break; case 1: /* ErrorCode 1 */ Tcl_SetResult(interp, "Divide by Zero!!", TCL_STATIC ); SWIG_fail; default: /* Unknown Error */ Tcl_SetResult(interp, "Unknown Error!!", TCL_STATIC ); SWIG_fail; } %} extern int mydiv(int a, int b, int *OUTPUT);
このインターフェースでは*OUTPUTが2つと、例外処理をしてるのを1つ書いています。展開されて、最初に実行されるのは、まず%typemap(in,numinputs) int *OUTPUTで、Tclのコマンドの引数を消し、*OUTPUTはポインタとして宣言されるので、tempで値を入れる実体を確保します。次に、%exception mydivで$actionでmydivを実行して、その返り値(result)の値でエラー処理をしています。0の時は値が正しくセットされてるので、SetResultせずにそのまま抜けます。SWIG_failというのはただのgotoで、エラー処理をして終了するラベルに飛んでくれます。で、%exceptionの次のブロックでresultがSetResultされるんですが、この部分のタイプマップの展開を変える方法がよくわからなかったので、%typemap(argout) int *OUTPUTを使ってもう一度OUTPUTをSetResultして終了です。それでは使ってみます。
% load test % mydiv Wrong # args. :mydiv a b argument 1 % mydiv 1 0 Divide by Zero!! % mydiv 1 1 1
と、まあ、エラーの時は値のかわりにエラーメッセージが表示されるというわけです。エラーメッセージの引数の数も合っています。次にエラーをキャッチしてみます。
% if {[catch {mydiv 1 0} msg]} {puts "ERROR $msg"} else {puts $msg} ERROR Divide by Zero!! % if {[catch {mydiv 1 1} msg]} {puts "ERROR $msg"} else {puts $msg} 1
と、まあちゃんとエラーもキャッチできるようになりました。インターフェースはもう少しすっきりと書けないかとは思いますが・・・。