独自のTcl・TkコマンドをC言語で書き、Tcl/Tk処理系を拡張する

●Tclコマンドを追加するCのプログラムを書く
前のページの例は、「自分のCプログラムでTcl/Tkを呼び出す」というものでしたが、今度は逆に、 独自のTcl・Tkコマンドを追加するプログラムをC言語で書き、Tcl/Tk処理系を拡張してみます。 EXEを作るかDLLを作るか、と先述しましたが、どちらの場合も、コマンドを追加するソースプログラムは共通です。
 サンプルとして、まず下のプログラムを作ります。

/* サンプル241 */
#include <stdio.h>
#include "tcl.h"

#define BUFSIZE 1024
static char ereason[BUFSIZE];
#define RERROR \
{ Tcl_SetResult(interp,ereason,TCL_STATIC); return TCL_ERROR; }

static int monthnameCmd(ClientData data, Tcl_Interp* interp,
                               int argc, char* argv[]){
  static char* months[] = {
    "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
  };
  int m;
  if(argc != 2){
    sprintf(ereason,
      "Illegal number of arguments, usage: %s month(1..12)",
      argv[0]);
    RERROR;
  }
  if(sscanf(argv[1], "%d", & m) == 0 || m < 1 || m > 12){
    sprintf(ereason, "Argument must be 1..12.");
    RERROR;
  }
  Tcl_SetResult(interp, months[m], TCL_STATIC);
  return TCL_OK;
}

DLLEXPORT int Monthname_Init(Tcl_Interp* interp){
  Tcl_CreateCommand(interp, "monthname", monthnameCmd, NULL, NULL);
  return TCL_OK;
}

これを monthname.c とでもしておきます。 このプログラムは、「monthname」というTclコマンドを定義しています。

  set a [monthname 10]
とすると "Oct" が変数 a に入ると、そんな感じのコマンドです。 もちろんエラーにもちゃんと対応していて、
  set a [monthname aho]
とか
  set a [monthname 1000000]
などといたずらしようとするときちんとエラーが飛んできます。 実はTcl 8.1以降では上の例のCプログラムは少し古いスタイルなのですが、 まずはこんな感じということで…

●スーパーtclsh・スーパーwishをビルドする
で、このmonthnameコマンドを追加した強化版のtclshやwishをビルドしないといけませんが、これはとても簡単です。 Tcl/Tk処理系のソース配布にもれなくついてくる、またはUNIXの場合はインストールが完了したら /usr/local/lib/tcl8.4 などのディレクトリに「なぜか」転がっている tclAppInit.c(拡張tclshを作る場合)または tkAppInit.c(拡張wishを作る場合)をコピーしてきます。 そしてエディタのそのプログラムの 'Tcl_AppInit' という関数の中に 「例えばModという拡張を組み込みたい場合はここにMod_Initを書け」 というような意味の英語のコメントが書かれている部分に、 おもむろに次のコードをこぴぺします。

    { extern int Monthname_Init(Tcl_Interp* );
      if(Monthname_Init(interp) == TCL_ERROR)
        return TCL_ERROR;
    }
以上、monthname.cとtclAppInit.cまたはtkAppInit.cがそろったところで、 コンパイル。まず拡張tclshの場合はこんな感じです。 ていうか、そろそろMakefileを書いたほうがいいころですね。 テキストエディタでMakefileを書きます。

CC	= gcc
CFLAGS	= -I/usr/local/include -O2 -fPIC
LDFLAGS	= -L/usr/local/lib
LIBS	= -ltcl8.4 -lm
.c.o:
	$(CC) -o $@ $(CFLAGS) -c $*.c
monthtclsh: tclAppInit.o monthname.o
	$(CC) -o $@ $(LDFLAGS) tclAppInit.o monthname.o $(LIBS)

ではアイメイク。ヘアメイク。

  make
うまくmonthtclshができればおなぐさみですが、どうでしょう? 実際に実行してみれば、このmonthtclshが monthname という新しいコマンドが追加された以外はまったくそのままtclshであることがわかるはずです。

同じく、今度は拡張wishを作ってみます。

CC	= gcc
CFLAGS	= -I/usr/local/include -I/usr/X11R6/include -O2
LDFLAGS	= -L/usr/local/include -L/usr/X11R6/lib
LIBS	= -ltk8.4 -ltcl8.4 -lX11 -ldl -lm
.c.o:
	$(CC) -o $@ $(CFLAGS) -c $*.c
monthwish: tkAppInit.o monthname.o
	$(CC) -o $@ $(LDFLAGS) tkAppInit.o monthname.o $(LIBS)

こちらもやり方はまったく同じです。

●共有ライブラリにする
今度はmain関数をもたず、tclshやwishからTcl言語のloadコマンドでロードできる共有ライブラリをビルドしてみます。 サンプルは上のサンプル241(monthname.c)を使います。Makefileは次のようになります。 共有ライブラリってナニ?という方もとりあえずこうしてみて、動いてしまえば共有なんとかのお勉強はまったくいりません。 動かなかったら、お手持ちのシステムで共有ライブラリを作る方法を少し勉強しないといけないかもしれません。

CC	= gcc
SHLD	= gcc -shared
CFLAGS	= -I/usr/local/include -O2 -fPIC
LDFLAGS	= -L/usr/local/include
LIBS	= -ltcl8.4 -lm
.c.o:
	$(CC) -o $@ $(CFLAGS) -c $*.c
libmonthname.so: monthname.o
	$(SHLD) -o $@ $(LDFLAGS) $? $(LIBS)

ここでは標準的なUNIXということで共有ライブラリの拡張子を .so にしましたが、HP-UX の .sl のような例外の場合適宜置き換えてください。

libmonthname.so ができたら、tclshを起動して、実際にロードさせてみます。

  shell% tclsh8.4
  % load libmonthname.so Monthname
  % monthname 10
  Oct

このように、共有ライブラリを作って標準tclsh・wishにロードさせても、 オリジナルのコマンドを追加するなどの拡張ができ、こちらのほうが多くの場合ベンリです。

Windows(VC++)の場合は共有ライブラリとは呼ばずダイナミックリンクライブラリ(DLL)ですが、基本的な考え方は同じです。

SHLD	= cl /LD
TCLROOT	= C:\Wintools\Tcl
CFLAGS	= /nologo /O2 /I"$(TCLROOT)\include"
LDFLAGS	= /nologo
LIBS	= "$(TCLROOT)\lib\tcl84.lib" "$(TCLROOT)\lib\tk84.lib"
.c.obj:
	$(CC) $(CFLAGS) -c $*.c
libmonthname.dll: monthname.obj
	$(SHLD) /Fe"$@" $(LDFLAGS) monthname.obj $(LIBS)

なお、サンプル241のCプログラムは少し古いスタイルだと書きましたが、それを現在最新鋭(かしら?)のスタイルで書き直すとこうなります。

/* サンプル242 */
#include <stdio.h>
#include "tcl.h"

static int monthnameObjCmd(ClientData data, Tcl_Interp* interp,
                           int objc, Tcl_Obj* CONST objv[]){
  static char* months[] = {
    "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
  };
  long m;
  if(objc != 2){
    Tcl_WrongNumArgs(interp, 1, objv, "month(1..12)");
    return TCL_ERROR;
  }
  if(Tcl_GetLongFromObj(interp, objv[1], & m) != TCL_OK ||
     m < 1 || m > 12){
    Tcl_AppendToObj(Tcl_GetObjResult(interp), "Argument must be 1..12.", -1);
    return TCL_ERROR;
  }
  Tcl_SetObjResult(interp, Tcl_NewStringObj(months[m], -1));
  return TCL_OK;
}

DLLEXPORT int Monthname_Init(Tcl_Interp* interp){
  Tcl_InitStubs(interp, "8.1", 0);
  Tcl_CreateObjCommand(interp, "monthname", monthnameObjCmd, NULL, NULL);
  return Tcl_PkgProvide(interp, "monthname", "1.00");
}

このサンプル242は Tcl_... という名前の関数で埋め尽くされていますが、これらがTcl APIと呼ばれているものです。 こいつらどもを1つずつ全部紹介しようというのがこの「なもなも」の趣旨、とこういうわけなのです。

さらに、2024年後半にリリースされたTcl/Tk 9.0では、このAPIの形式が一部変わっています。 Tcl9対応に書き直すと次のようになります。

/* サンプル242 */
/*
 * monthname.c
 * 2025/02/09
 */
#include <stdio.h>
#include "tcl.h"

/*
static int monthnameObjCmd(ClientData data, Tcl_Interp* interp, int objc, Tcl_Obj* CONST objv[]){
*/
static int monthnameObjCmd(void* data, Tcl_Interp* interp, int objc, Tcl_Obj* const objv[]){
  static char* months[] = {
    "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
  };
  long m;
  if(objc != 2){
    Tcl_WrongNumArgs(interp, 1, objv, "month(1..12)");
    return TCL_ERROR;
  }
  if(Tcl_GetLongFromObj(interp, objv[1], & m) != TCL_OK ||
     m < 1 || m > 12){
    Tcl_AppendToObj(Tcl_GetObjResult(interp), "Argument must be 1..12.", -1);
    return TCL_ERROR;
  }
  Tcl_SetObjResult(interp, Tcl_NewStringObj(months[m], -1));
  return TCL_OK;
}

DLLEXPORT int Monthname_Init(Tcl_Interp* interp){
  if(Tcl_InitStubs(interp, "8.5-10", 0) == 0){
     return TCL_ERROR;
  }
  Tcl_CreateObjCommand(interp, "monthname", monthnameObjCmd, NULL, NULL);
  return Tcl_PkgProvide(interp, "monthname", "1.00");
}

Makefileの例も書いておきます。Tkのlibのファイル名が少し変わっています。

SHLD	= cl /LD
TCLROOT	= C:\Wintools\Tcl9.0.1
CFLAGS	= /nologo /O2 /I"$(TCLROOT)\include"
LDFLAGS	= /nologo
LIBS	= "$(TCLROOT)\lib\tcl90.lib" "$(TCLROOT)\lib\tcl9tk90.lib"
.c.obj:
	$(CC) $(CFLAGS) -c $*.c
libmonthname.dll: monthname.obj
	$(SHLD) /Fe"$@" $(LDFLAGS) monthname.obj $(LIBS)

ざっと駆け足でTcl/TkとCのいろいろな組合せ技について紹介しましたが、以降ではだんだんそれぞれの深みにはまっていきます。

お楽しみに!(騙)

なもなも top
(first uploaded 1999/07/22 last updated 2025/02/09, MISUMI URANO)