UTF-8과 인코딩(Encoding)

admin의 아바타

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의 사용법은 거의 동일합니다.