본문으로 바로가기

UTF-8과 인코딩(Encoding)

category 카테고리 없음 2024. 5. 13. 17:20

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