UTF-8とエンコーディング
 Tcl/Tk 8.1以降では文字列は内部ではJavaと同じようなUnicode(UTF-8) として扱われます(*1)。 C言語世界での日本語の文字列(シフトJISや日本語EUC) をTclコマンドの戻り値としてそのまま返すと、 Tcl世界ではencodingコマンドでUTF-8に変換する必要が発生します。 C言語世界の中で日本語の文字列をUTF-8に変換するAPIを使っておけば、 Tcl世界で受け取るとそのまま表示可能な日本語として扱えます。 このセクションでは、この2通りの方法で日本語の文字列をC言語世界からTcl言語世界に返すサンプルで比較してみます。
(*1) 「Unicode」と「UTF-8」は本来は別物です。 というかUTF-8はUnicodeの一種で、Tcl/Tkで使われるのはUTF-8です。 また実際に、JavaのUTF-8とTcl/TkのUTF-8は完全に互換性があります。

●エクスターナルでそのまま返すサンプル
 C言語で文字列をリテラル書きしたり、 FILE構造体を使って日本語のテキストファイルを読んだりすると、 シフトJISや日本語EUCなど、 OSや言語環境に依存する日本語コード (Tcl APIマニュアルの呼び名に合わせて、 これらをエクスターナル(External)と呼ぶことにします) で文字列が格納されます。 これをそのままTclコマンドの戻り値として返すTclコマンドを追加するCプログラムが次のサンプルです。 この「greet_raw」コマンドは、 引数で与えられたtime形式の整数値から現在の時刻を計算し、 時刻によって挨拶を日本語で返します。 引数を省略すると現在の時刻に応じた挨拶を返します。


/* サンプル421(共有ライブラリ) */
#include "tcl.h"
#include <time.h>
#include <string.h>
static int greetRawObjCmd(ClientData data, Tcl_Interp* interp,
			  int objc, Tcl_Obj* CONST objv[]){
  time_t clockseconds;
  struct tm* tmtm;
  char* g;
  Tcl_Obj* outobj;

  if(objc > 2){
    Tcl_WrongNumArgs(interp, 1, objv, "clockseconds");
    return TCL_ERROR;
  }
  else if(objc == 1){
    clockseconds = time(NULL);
  }
  else if(Tcl_GetLongFromObj(interp, objv[1], & clockseconds) == TCL_ERROR){
    return TCL_ERROR;
  }
  tmtm = localtime(& clockseconds);
  if(tmtm->tm_hour >= 5 && tmtm->tm_hour < 10){
    g = "おはようさん";
  } else if(tmtm->tm_hour >= 10 && tmtm->tm_hour < 18){
    g = "ちわっす";
  } else if(tmtm->tm_hour >= 18){
    g = "こんばんは";
  } else {
    g = "眠くないですか?";
  }
#if 0
  /* 8.1以降では文字化けしてしまいます。*/
  outobj = Tcl_NewStringObj(g, -1);
#else
  outobj = Tcl_NewByteArrayObj(g, strlen(g));
#endif
  Tcl_SetObjResult(interp, outobj);
  return TCL_OK;
}

DLLEXPORT int Greet_Init(Tcl_Interp* interp){
#ifdef USE_TCL_STUBS
  Tcl_InitStubs(interp, "8.1", 0);
#endif
  Tcl_CreateObjCommand(interp, "greet_raw", greetRawObjCmd, NULL, NULL);
  return Tcl_PkgProvide(interp, "greet", "0.10");
}
/* end. */

 このコマンドの使い方ですが、
load libgreet[info sharedlibextension]
set a [greet_raw]
puts "RAW: $a"
set a [greet_raw [clock scan "1999-02-06 07:30"]]
puts "RAW: $a"
 このように使うと化け化けになってしまいます。 このプログラムは日本語の「おはようさん」 などを単なる12バイトの並びとして返すので、 Tcl世界のUTF-8の文字列に変更するために
load libgreet[info sharedlibextension]
set a [encoding convertfrom cp932 [greet_raw]]
puts "RAW=>encoding: $a"
set a [encoding convertfrom cp932 \
       [greet_raw [clock scan "1999-02-06 07:30"]] \
      ]
puts "RAW=>encoding: $a"
このようにする必要があります。
 さてプログラムに戻って重大な注意ですが、 エクスターナルで書かれた日本語の文字列を絶対にTcl_NewStringObj でTclオブジェクトに変換したり、Tcl_SetResultでそのまま返したりしてはいけません。 これらのAPIを使うと、エクスターナルで書かれているにも関わらず、 Tcl処理系はUTF-8の文字列と解釈するので、 Tcl世界では…ありゃりゃ?
RAW=>encoding: こんb?は
RAW=>encoding: お?B、さん
ぎゃははは。このように化け化けになってしまいます。
 さらにもうひとつ重大な注意ですが、 Tcl_NewStringObjの第2引数は、 「-1」を指定すれば自動的に文字列の長さを計算してくれますが、 Tcl_NewByteArrayObjの第2引数に-1を渡してはいけません。 エラーにもならずにSIGSEGVで落ちてしまうので、注意が必要です。

●C言語中でUTF-8に変換するサンプル
 上の例のようにTclスクリプトで毎回encodingを使う手間を省くには、 C言語世界の中で予め文字列をUTF-8に変換するAPIを使うことができます。 Tcl APIにはエクスターナルからUTF-8に変換するAPIと UTF-8からエクスターナルに変換するAPIが用意されているため、 現在文字列がどの文字コードで書かれているか判別できさえすれば、 フリーソフトのnkfみたいなツールもこれですぐ作れます。 ていうか、 英語のアルファベットしか使わない国のプログラマはエンコーディングなんか使わないはずなので、 Tcl/Tkのエンコーディングを扱っているページというのは世界中探してもそうないでしょう。 というわけで、世界でも珍しいTcl/TkのエンコーディングのAPIのページですよ。 テヘヘ。嬉しいなっ。 ちなみに、自分で新しいエンコーディングを追加することもできるはずなんですが、 それはまだ試していません。というより、リアルな世界での外国語を知らないため、 そんなサンプルがおいそれと作れないのです。


/* サンプル422(共有ライブラリ) */
#include "tcl.h"
#include <time.h>
#include <string.h>
#define BUFSIZE 1024
static int greetUtf8aObjCmd(ClientData data, Tcl_Interp* interp,
			    int objc, Tcl_Obj* CONST objv[]){
  time_t clockseconds;
  struct tm* tmtm;
  char* g;
  char* thisSourceEncoding = "cp932"; /* このCプログラムで使う日本語 */
  Tcl_Encoding e;
  Tcl_Obj* outobj;
  int flags = (TCL_ENCODING_START | TCL_ENCODING_END |
	       TCL_ENCODING_STOPONERROR);
  Tcl_EncodingState statebuf;
  int srcReadCount = 0, destWroteCount = 0, destCharsCount = 0;
  int r;
  char destbuf[BUFSIZE];
  int destlen = BUFSIZE;

  if(objc > 2){
    Tcl_WrongNumArgs(interp, 1, objv, "clockseconds");
    return TCL_ERROR;
  }
  else if(objc == 1){
    clockseconds = time(NULL);
  }
  else if(Tcl_GetLongFromObj(interp, objv[1], & clockseconds) == TCL_ERROR){
    return TCL_ERROR;
  }
  tmtm = localtime(& clockseconds);

  if(tmtm->tm_hour >= 5 && tmtm->tm_hour < 10){
    g = "おはようさん";
  } else if(tmtm->tm_hour >= 10 && tmtm->tm_hour < 18){
    g = "ちわっす";
  } else if(tmtm->tm_hour >= 18){
    g = "こんばんは";
  } else {
    g = "眠くないですか?";
  }
  if((e = Tcl_GetEncoding(interp, thisSourceEncoding)) == NULL){
    Tcl_AppendResult(interp, "unrecognizable encoding name:",
                     thisSourceEncoding, NULL);
    return TCL_ERROR;
  }
  r = Tcl_ExternalToUtf(interp, e, g, strlen(g), flags,
			& statebuf, destbuf, destlen,
			& srcReadCount, & destWroteCount, & destCharsCount);
  if(r != TCL_OK){
    Tcl_AppendResult(interp, "error occurred during encoding conversion to ",
                     thisSourceEncoding, NULL);
    return TCL_ERROR;
  }
  outobj = Tcl_NewStringObj(destbuf, destWroteCount);
  Tcl_SetObjResult(interp, outobj);
  return TCL_OK;
}

DLLEXPORT int Greet_Init(Tcl_Interp* interp){
#ifdef USE_TCL_STUBS
  Tcl_InitStubs(interp, "8.1", 0);
#endif
  Tcl_CreateObjCommand(interp, "greet_utf8a", greetUtf8aObjCmd, NULL, NULL);
  return Tcl_PkgProvide(interp, "greet", "0.10");
}
/* end. */

 今度のコマンド「greet_utf8a」は先ほどの「greet_raw」 と同じ機能ですが、encodingコマンドを使う必要がなくお手軽です。
load libgreet[info sharedlibextension]
set a [greet_utf8a]
puts "UTF8: $a"
set a [greet_utf8a [clock scan "1999-02-06 07:30"]]
puts "UTF8: $a"
 Tcl_GetEncodingは、"shiftjis"などの エンコーディング名(エクスターナル)から対応する内部表現 (Tcl_Encoding型の構造体)を取得するAPIです。 エクスターナルからUTF-8に変換するにはTcl_ExternalToUtf を、UTF-8からエクスターナルに変換するには Tcl_UtfToExternalを使います。 これらのAPIは豪勢にも11個の引数をとりますが、 今回は対象文字列全体を一発で変換する一番簡単な例ということで、 第5引数はプログラムの通り記号定数の論理和で、 第6引数は受け取るだけで何にも使っていません。 第7引数には変換結果を格納するデータ領域をあらかじめ確保しておき、 いくら確保したかを第8引数で渡します。 これは、1文字が2バイトで表される日本語世界では、 変換元の2倍も用意しておけば十分でしょう。 9番目の引数には変換元から読んだバイト数が、 10番目の引数には変換先に書きこんだバイト数が、 最後の引数には変換先のデータが変換先の文字コードで何「文字」 に相当するかがそれぞれ書きこまれます。このうち10番目の引数は大事ですが、 他の2つは無視しても構いません。

 


日本語のファイル名
 ファイル名を渡されて何か処理をするTclコマンドを自分で作る場合には、 日本語など非英語圏の言葉の文字の配慮が必要です。 これらの文字は、Tcl/Tkの内部ではUTF-8表現になっているので、 渡されたファイル名をそのまま使ってはいけません。 例えば以前のDStringの紹介のところで出てきたサンプル51の 「stringsfile」というTclコマンドにTclスクリプトから
  set r [stringsfile "コピー.jpg"]
のようなファイル名を指定すると、 あっても存在しないというエラーになってしまいます。 そこで、次のように直します。
/* サンプル423(main関数をもたない共有ライブラリ) */
#include <stdio.h>
#include <ctype.h>
#include <sys/stat.h>
#include "tcl.h"
#define  MAXLEN 1024

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

static int getsize(char* filename){
  struct stat sbuf;
  if(stat(filename, & sbuf) < 0) return -1;
  return sbuf.st_size;
}

static int stringsfileHandleProc(ClientData clientData, Tcl_Interp* interp,
				 int argc, char* argv[]){
  FILE* fp;
  int   i, sz, len;
  char* p, * q, * filebuf;
  char  buf[MAXLEN];
  Tcl_DString ds, * dsp;
  Tcl_DString dsfilename;
  char* filename;

  dsp = & ds;

  if(argc == 1){
    sprintf(ereason, "too few arguments, usage: %s filename", argv[0]);
    RERROR;
  }
#if 0
  filename = argv[1];
#else /* (あ) */
  filename = Tcl_UtfToExternalDString(NULL, argv[1], -1, & dsfilename);
#endif
  if((sz = getsize(filename)) == -1){
    sprintf(ereason, "cannot stat file %s", filename);
    Tcl_DStringFree(& dsfilename);  /* (い) */
    RERROR;
  }
  if((fp = fopen(filename, "rb")) == NULL){
    sprintf(ereason, "cannot open %s", filename);
    Tcl_DStringFree(& dsfilename);  /* (い) */
    RERROR;
  }
  if((filebuf = (char* )Tcl_Alloc(sz)) == NULL){
    strcpy(ereason, "memory exhausted"); fclose(fp);
    Tcl_DStringFree(& dsfilename);  /* (い) */
    RERROR;
  }
  if((len = fread(filebuf, 1, sz, fp)) != sz){
    sprintf(ereason, "problems occurred in reading from %s", filename);
    Tcl_Free(filebuf); fclose(fp);
    Tcl_DStringFree(& dsfilename);  /* (い) */
    RERROR;
  }
  fclose(fp);
  Tcl_DStringInit(dsp);
  for(p=filebuf, i=0; i<len; ){
    if(isgraph(*p)){
      int clen;
      for(clen=0,q=buf; clen<MAXLEN&&isgraph(*p); clen++) *q++ = *p++;
      *q = '\0'; p++; i+=clen+1;
      if(clen > 5) Tcl_DStringAppendElement(dsp, buf);
    }
    else{
      p++; i++;
    }
  }
  Tcl_DStringFree(& dsfilename);  /* (い) */

  Tcl_Free(filebuf);
  Tcl_DStringResult(interp, dsp);
  Tcl_DStringFree(dsp);
  return TCL_OK;
}

DLLEXPORT int Stringsfile_Init(Tcl_Interp* interp){
  Tcl_CreateCommand(interp, "stringsfile",
		    stringsfileHandleProc, NULL, NULL);
  return TCL_OK;
}
/* end. */
 (あ)と(い)の部分が変わっています。 Tcl_UtfToExternalDStringは、 先の引数を11個ももつTcl_UtfToExternal よりも簡単にUTF-8からエクスターナルに変換する処理をするAPIです。 第1引数はTcl_UtfToExternal のときと同じく、Tcl_Encoding型の変換先のエンコーディングを表す構造体を渡しますが、 変換先がロケ−ル(言語環境情報)から決められるシステム標準のエンコーディング (MS-Windows日本語版ならcp932です)の場合はNULLでOKです。 言うまでもないことですが、 MS-Windows日本語版の日本語のファイル名はもちろんシフトJISなので、 ここではNULLを使っています。 第2引数がTclコマンドの引数から渡されるUTF-8の文字列(ここではファイル名)です。 第3引数はその長さをバイト数で渡しますが、-1を渡せばstrlen(文字列) の値が自動的に入るので、普通は-1でよいでしょう。 最後の引数にはTcl_DString型の変数へのポインタを渡しますが、これは Tcl_DStringInit初期化してある必要はありませんTcl_UtfToExternalDStringは、変換した結果をこのDString変数に格納するほか、 それを戻り値としても返すので、普通は戻り値の方を使えばよく、 DStringは特に使う必要はありません。ただし、処理が終わったら忘れずに Tcl_DStringFreeを使って領域を開放する必要があります。

 −UNIXの場合は注意が必要です。 UNIXではアルファベットのファイル名しか使わないのが一般的ですが、 逆にもし作ろうとした場合にはどんな文字コードのファイル名でも作ってしまえるため、対象のファイル名がどの文字コードで書かれているのかを正確に知っておく必要があります。

 最後に、この逆に、エクスターナルからUTF-8に変換する API Tcl_ExternalToUtfDStringも使い方はほとんど同じです。

なもなも top
(first uploaded 1999/06/13 last updated 2000/12/07 , EK - Urano398 )