簡単なTclコマンドを作るには

 ここでは、前のページに出てきた、Tclコマンドを追加するCプログラムの書き方について、 もう少し順を追って紹介したいと思います。 mainを作らずに処理系を拡張するには、 Tclコマンドを自分で作って標準処理系に追加する、という方法をとります。 例えば、複雑な経理計算をする?Tclコマンドkeiriを普通のTclスクリプトで

  keiri $name $old $days $hours
こんな感じで使えるようにするものです。もちろん
  set result [keiri $name $old $days $hours]
このように他のコマンドに渡せるように値を返せないといけませんし (別に返さなくてもいいですが)、
  keiri $name $old $days
のように指定が足りない場合は、いきなり実行時エラー(不正な処理)で落ちたりせずに
  Error: too few arguments: should be 'keiri name old days hours' .
のようなエラーメッセージを出すように仕立てるべきでしょう。 このようないくつかの作法をC言語で書く方法を少し知る必要がありますが、 これらはおおむね一度書いてしまえばコピーペーストで使いまわしが効くものです。 あとは自分が使いたい本筋の処理をその間にC言語で書くだけで、 いろいろなコマンドが追加できるようになるはずです。

/* サンプル281(main関数をもたない共有ライブラリ) */
#include <stdio.h>
#include <string.h>
#include "tcl.h"

static int ennightHandleProc(ClientData clientData, Tcl_Interp* interp,
			     int argc, char* argv[]){
  int i;
  char* p, * q, * r;
  static char* night = "night";
  char ereason[1024];
  if(argc != 2){
    sprintf(ereason,
      "wrong number of arguments:shoule be %s string", argv[0]);
    Tcl_SetResult(interp, ereason, TCL_VOLATILE);
    return TCL_ERROR;
  }
  for(i=0,q=ereason,p=argv[1]; *p && i<1024; ){
    if(! strncmp(p, "morning", 7)){
      for(r=night; *r; *q++=*r++); p += 7; i += 7;
    }
    else{ *q++ = *p++; i++; }
  }
  *q = '\0';
  if(! *p){
    Tcl_SetResult(interp, ereason, TCL_VOLATILE);
    return TCL_OK;
  }
  else{
    strcpy(ereason, "sorry, given string too long.");
    Tcl_SetResult(interp, ereason, TCL_VOLATILE);
  }
}

DLLEXPORT int Night_Init(Tcl_Interp* interp){
  Tcl_CreateCommand(interp, "ennight", ennightHandleProc, NULL, NULL);
  return TCL_OK;
}

 順を追っていきましょう。 インクルードするヘッダファイルですが、通常tcl.hと、あと必要があればtk.hを使えばOKです。

static int ennightHandleProc(ClientData clientData, Tcl_Interp* interp,
			     int argc, char* argv[])
 次にこの関数の形ですが、これは基本のうちはこの形で使いまわしてOKです。 実は後でもう一種類別の形が出てきまして、現在の正統はそちらなのですけど、 いきなりだとややこしくなりすぎるのでここでは省略します。関数名は自由です。
 argcとargvは、 通常のCのmain関数と同じようなものです。 argv[0]はそのTclコマンドの名前が必ず入っています。ですから、 複数のよく似たTclコマンドを同じコマンドプロシージャで登録しても、 コマンドプロシージャでargv[0] を調べることでどのコマンドが呼び出されたのか知ることができます。 コマンドが全く引数なしで呼ばれた場合はargcは1になります。 コマンドに引数がつくと、先頭から1つずつargv[]に代入されます。 例えば、上の「Night拡張」で新しく作った ennightというTclコマンドは文字列に含まれる「morning」という部分を全部 「night」に変えて返すだけというふざけたコマンドですが、
  ennight "Good morning, but it rains." "Nothing like the storm."
とすると
  argv[0] ... ennight
  argv[1] ... Good morning, but it rains.
  argv[2] ... Nothing like the storm.
となります。また、
  ennight Good morning, but it rains. "Nothing like the storm."
とすると、当然
  argv[0] ... ennight
  argv[1] ... Good
  argv[2] ... morning,
  argv[3] ... but
  argv[4] ... it
  argv[5] ... rains.
  argv[6] ... Nothing like the storm.
というようになります。 ようするにTcl言語のプロシージャを呼び出したときに引数が扱われる作法と全く同じです。 …が、ちょっと考えて頂けると分かると思いますが、 このargcとargvを使う限り、有意な文字列の途中にNULL文字(0x00) が含まれているバイナリ文字列を扱うことは絶対にできません。 そのようなバイナリ文字列も扱うには、関数の形を先述のもう一種類のものにする必要があります。 これはずっと後で出てくるので、しばしお待ちくださいね。

 で、argcとargvを使って何か処理をした上で、 Tclコマンドの実行結果として何かの値を返すわけですが、

  • Tclコマンドが正常終了の場合、インタープリタに「コマンドの値」 をセットし、記号定数TCL_OKを返す。
  • Tclコマンドがエラー終了の場合、インタープリタに「エラーメッセージ」 をセットし、記号定数TCL_ERRORを返す。
となります。 上の例を見るとなんとなーくお分かりと思います。 そこで使っているのは最も簡単で(しかし時代遅れの)API、 Tcl_SetResultを使います。 この第2引数に「コマンドの値」または「エラーメッセージ」を格納します。 第3引数の記号定数は、 第2引数のデータ領域がどこにあるかによって次のように使い分けます。
  • TCL_STATIC ... データが静的な領域(static char)にあって、関数を抜けてもその領域が上書きされない場合
  • TCL_VOLATILE ... データが揮発する(auto char)スタック領域にあり、関数を抜けると上書きされる場合
  • TCL_DYNAMIC ... データがAPI Tcl_Allocで確保された領域にある場合
 TCL_STATICの代わりにNULLを使っているコードもありますが、これは TCL_STATICが零にマクロ定義されているため、「偶然」NULL と互換性があるだけです。正しくはTCL_STATICを使うべきです。

DLLEXPORT int Night_Init(Tcl_Interp* interp){
  Tcl_CreateCommand(interp, "ennight", ennightHandleProc, NULL, NULL);
  return TCL_OK;
}

 はてさて、 この関数がこの共有ライブラリ(DLL)がロードされたとき最初に呼ばれる関数で、 この中のTcl_CreateCommandによって新しく「ennight」 というTclコマンドがTclインタープリタに追加されています。 DLLEXPORTというマクロは、WindowsでDLL内の関数を外部に公開する指定で、 UNIXでは省略できます。 この関数の名前ですが、後で出てくる理由で、「(なんとか)_Init」と名づけ、 (なんとか)の部分は最初の1文字が英大文字、 残りは全て英小文字の単語にしておくことをお勧めします。

 インタープリタに結果を返すときに使うAPIはいくつかありますが、「旧式扱い」とされている Tcl_SetResultの他に、 Tcl_AppendResultがベンリなAPIです。 これは複数の文字列を連結してエラーメッセージを作って返すときなどに特にベンリで、 このような目的にはこちらを使うとよいでしょう。 上のサンプル281を書き換えてみるとこのようになります。

/* サンプル282(main関数をもたない共有ライブラリ) */
#include <stdio.h>
#include <string.h>
#include "tcl.h"

static int ennightHandleProc(ClientData clientData, Tcl_Interp* interp,
			       int argc, char* argv[]){
  int i;
  char* p, * q, * r;
  static char* night = "night";
  if(argc != 2){
    Tcl_AppendResult(interp,
      "wrong number of arguments:shoule be ", argv[0], " string", NULL);
    return TCL_ERROR;
  }
  for(i=0,q=ereason,p=argv[1]; *p && i<1024; ){
    if(! strncmp(p, "morning", 7)){
      for(r=night; *r; *q++=*r++); p += 7; i += 7;
    }
    else{ *q++ = *p++; i++; }
  }
  *q = '\0';
  if(! *p){
    Tcl_AppendResult(interp, ereason, NULL); return TCL_OK;
  }
  else{
    Tcl_AppendResult(interp, ereason, NULL); return TCL_ERROR;
  }
}

DLLEXPORT int Night_Init(Tcl_Interp* interp){
  Tcl_CreateCommand(interp, "ennight", ennightHandleProc, NULL, NULL);
  return TCL_OK;
}
/* end. */

Tcl_AppendResultは、

 Tcl_AppendResult(interp,
   "wrong number of arguments:shoule be ", argv[0], " string",  NULL);
このように文字列を何個でもつなげて返すことができます。 最後の引数は必ずNULLでないといけません。

なもなも top
(first uploaded 1999/03/12 last updated 2000/12/06, Urano398)