Tcl/Tk 8.1 이후 문자열은 내부에서 Java와 같은 Unicode(UTF-8)로 처리되어집니다. C언어에서 2바이트 언어인 한글 문자열을 Tcl 커맨드에 대입시 Tcl의 encoding 커맨드로 UTF-8로 변환할 필요가 생깁니다. C언어에서 한글 문자열을 UTF-8로 변환하는 API를 사용하여, Tcl에서 그 값을 리턴 받는다면 그대로 표시 가능한 값을 얻을 수 있습니다. 이 강좌에서는 한글 문자열을 처리하는 방법에 대해서 알아봅니다.
Unicode와 UTF-8은 본래 다른것입니다. UTF-8은 Unicode의 일종으로, Tcl/Tk에서는 UTF-8이 사용되어집니다. 또 실제로 Java의 UTF-8과 Tcl/Tk의 UTF-8은 완전한 상호 교환이 가능합니다.
External 그대로 리턴해주는 예제
C언어로 FILE구조체를 사용하여 한글 텍스트파일을 읽거나 하면, O/S나 언어 환경에 의존한 한글코드(Tcl API 매뉴얼에서는 External이라 부르고 있습니다.)의 문자열로 저장이 됩니다. 이 값을 그대로 Tcl 커맨드로 돌려주는 Tcl 커맨드를 작성해 보겠습니다. 이 greet_raw 커맨드는, 인자로 부여받은 time 형식의 정수 값으로부터 현재의 시간을 계산하여, 그 시간에 맞는 인사말을 한글 문자열로 리턴해줍니다. 인자를 생략하면 현재의 시간을 계산하여 인사말을 리턴해줍니다.
#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 >= 22 && tmtm->tm_hour < 7){
g = "좋은 꿈 꾸세요.";
} else if(tmtm->tm_hour >= 7 && tmtm->tm_hour < 18){
g = "즐거운 직장 생활 되세요.";
} else if(tmtm->tm_hour >= 18 && tmtm->tm_hour < 22){
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");
}
VC++용 Makefile
SHLD = cl /LD
TCLROOT = C:\Program Files\Tcl
CFLAGS = /nologo /O2 /I"$(TCLROOT)\include"
LDFLAGS = /nologo
LIBS = "$(TCLROOT)\lib\tcl84.lib" "$(TCLROOT)\lib\tk84.lib"
.c.obj:
$(CC) $(CFLAGS) -c $*.c
libgreet.dll: greet.obj
$(SHLD) /o $@ $(LDFLAGS) greet.obj $(LIBS)
아래는 커맨드의 사용 예 입니다.
load libgreet[info sharedlibextension]
set a [greet_raw]
puts "RAW: $a"
set a [greet_raw [clock scan "2005-02-03 15:20"]]
puts "RAW: $a"C:\Temp>tclsh test.tcl
RAW: ??°?¿? ?÷?? ???° ??¼¼¿?.
RAW: ??°?¿? ?÷?? ???° ??¼¼¿?.
위와 같이 사용하면 한글 문자열의 결과 값인 "즐거운 직장 생활 되세요."를 단순히 24바이트의 O/S에 의존하는 코드(MS의 한글 윈도즈는 cp949, 일본어 윈도즈는 cp932)로 문자열로 리턴해주기 때문에, Tcl의 UTF-8로 변환키위해 아래와 같이 해야 합니다.
load libgreet[info sharedlibextension]
set a [encoding convertfrom cp949 [greet_raw]]
puts "RAW=>encoding: $a"
set a [encoding convertfrom cp949 \
[greet_raw [clock scan "2005-02-03 15:20"]]]
puts "RAW=>encoding: $a"
C:\Temp>tclsh test.tcl
RAW=>encoding: 즐거운 직장 생활 되세요.
RAW=>encoding: 즐거운 직장 생활 되세요.
이 코드에서 중요한 점은, External로 쓰여진 한글 문자열은 절대로 Tcl_NewStringObj를 사용하여 Tcl 오브젝트로 변환하거나, Tcl_SetResult를 사용하여 그대로 리턴해주면 안 됩니다. External로 쓰여 있다 해도, Tcl에서는 UTF-8의 문자로 해석하기 때문입니다.
또 하나 중요한 점은, Tcl_NewStringObj의 2번째 인자에 -1을 지정하면 자동으로 문자열의 길이를 계산해줍니다. 하지만 Tcl_NewByteArrayObj의 2번째 인자에는 -1을 대입하면 안 됩니다. 에러가 발생하지 않으면서 SIGSEGV 시그널이 발생하기 때문에 주의가 필요합니다.
C언어에서 UTF-8로 변환하는 예제
위의 예제와 같이 Tcl 스크립트를 매번 encoding 하는 번거로움을, UTF-8로 변환시켜주는 API로 수고를 줄일 수 있습니다. Tcl API에는 External로부터 UTF-8로 변환하는 API와 UTF-8로부터 External로 변환하는 API가 제공되고 있기 때문에, 현재 문자가 어느 문자 코드로 작성되어 있는지 알 수만 있다면, 공개 소프트웨어인 nkf(네트워크용 코드 컨버전 프로그램)와 같은 툴도 만들 수 있습니다. 영어만 사용하는 국가의 프로그래머(Tcl/Tk의 탄생지도 영어권입니다.)는 인코딩 같은 것은 신경 쓰지 않기 때문에, Tcl/Tk의 인코딩 API을 알려주는 웹 페이지를 어디서나 찾아보기 힘들 것입니다.
#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 = "cp949";
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 >= 22 && tmtm->tm_hour < 7){
g = "좋은 꿈 꾸세요.";
} else if(tmtm->tm_hour >= 7 && tmtm->tm_hour < 18){
g = "즐거운 직장 생활 되세요.";
} else if(tmtm->tm_hour >= 18 && tmtm->tm_hour < 22){
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");
}
이번 커맨드 greet_utf8a는 조금전에 greet_raw와 동일한 기능을 수행하는 커맨드이지만, encoding 커맨드를 사용할 필요가 없이 간단해졌습니다.
load libgreet[info sharedlibextension]
set a [greet_utf8a]
puts "UTF8: $a"
set a [greet_utf8a [clock scan "2005-02-03 15:30"]]
puts "UTF8: $a"
C:\Temp>tclsh test.tcl
UTF8: 즐거운 직장 생활 되세요.
UTF8: 즐거운 직장 생활 되세요.
Tcl_GetEncoding은 cp949와 같은 인코딩의 이름(External)으로부터 내부 구조체(Tcl_Encoding 타입의 구조체)를 생성하는 API입니다. External로부터 UTF-8로 변환하는 API는 Tcl_ExternalToUtf이며, UTF-8로부터 External로 변환하는 API는 Tcl_UtfToExternal API를 사용합니다.
한글 파일명
파일명으로 어떤 처리를 하는 Tcl 커맨드를 만들 경우에는, 한글등 비 영문어권 문자의 배려가 필요합니다. 이러한 문자는, Tcl/Tk의 내부에서 UTF-8로 표현되어 있기 때문에, 입력 받은 파일명을 그대로 사용하면 안 됩니다. 예를 들면 이전의 DString 강좌에서 나왔던 stringfile 커맨드로부터 Tcl 스크립트에서
set r [stringsfile "민인학.tcl"]
와 같은 파일명을 지정하면, 파일이 존재하여도 존재하지 않는다는 에러가 발생하므로, 다음과 같이 수정해야 합니다.
#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;
}
# '민인학.tcl' 파일이 존재한다면 파일의 데이타를 보여줍니다.
load libstringsfile[info sharedlibextension]
set a [stringsfile "민인학.tcl"]
puts $a
// 수정 부분이 변경되었습니다. Tcl_UtfToExternalDString은 11개의 인자를 갖는 Tcl_UtfToExternal보다 간단하게 UTF-8로부터 External로 변환 처리를 하는 API입니다.
첫번째 인자는 Tcl_UtfToExternal과 같이 Tcl_Encoding 형의 문자 인코딩을 나타내는 구조체를 넣어주지만, 로케이션이 이미 O/S나 언어 환경정보로부터 결정된 시스템이라면(MS 윈도즈의 한글판은 cp949) NULL을 넣어주면 됩니다.
두 번째 인자는 Tcl 커맨드의 인자로부터 입력받은 UTF-8의 문자열(여기서는 파일명)이며,
세 번째 인자는 그 길이를 바이트수로 입력해 줍니다. -1을 입력하면 strlen의 값이 자동으로 대입되기 때문에, 보통은 -1로 지정합니다.
마지막 인자에는 Tcl_DString형의 변수 포인터를 입력해 주며, Tcl_DStringInit로 초기화할 필요는 없습니다. Tcl_UtfToExternalDString은, 변환한 결과를 DString 변수에 저장하는 것 외에 그 값을 리턴해주기 때문에, 보통은 리턴해주는 값을 사용하면 되며, DString 은 크게 필요치 않습니다. 단 처리가 끝나면 Tcl_DStringFree를 사용하여, 메모리에서 삭제는 필수입니다.
마지막으로 External로부터 UTF-8로 변환하는 API인 Tcl_ExternalToUtfDString의 사용법은 거의 동일합니다.