본문으로 바로가기

파일 채널 I/O

category 카테고리 없음 2025. 8. 5. 14:55

Tcl 8.0의 파일 채널 I/O

Tcl 8.0과 8.1에서의 파일 채널 I/O는 완전히 다릅니다. 8.1에서는 문자열의 내부 표현이 유니코드(UTF-8)로 바뀌었고, euc-kr 등의 Externals 인코딩을 UTF-8로 자동 변환하는 API가 등장했습니다. 이에 따라 파일 채널 I/O에도 이 인코딩을 자동으로 변환하면서 파일을 읽고 쓰는 API 함수가 추가되었습니다. 이에 따라 인코딩 기능이 없는 8.0 시대의 I/O API는 구식 취급을 받게 되었습니다.


Tcl 8.1의 파일 채널 I/O를 사용하려면 Tcl 오브젝트나 바이너리 데이터 처리, 인코딩 등의 지식이 필요하기 때문에 초보자가 이해하기란 쉽지 않은 일입니다. Tcl_ExternalToUtf와 Tcl_ExternalToUtfDString이라는 외부와 UTF-8 변환 API를 사용하면 일반 FILE 구조체를 사용한 파일 I/O로 Tcl 언어에서 데이터를 읽고 쓰는 것은 충분히 가능하므로, 파일 채널 I/O를 무리하게 사용할 필요가 없을 수도 있습니다.


우선 인코딩이나 바이너리 데이터 처리에 대한 이야기가 들어가면 복잡해지고 어려울 수 있기에, 인코딩을 전혀 고려하지 않은 텍스트 파일을 가정한 파일 채널 I/O의 예를 들어보겠습니다. readfilechan은 filename의 파일을 Tcl 오브젝트 objptr로 저장하는 함수이며, writefilechan은 쓰기 함수입니다. 8.0에서는 이 방법이 표준 방식이었지만, 8.1에서는 이 방법이 구식으로 취급됩니다. 8.1에서는 이 루틴으로 읽은 오브젝트를 Tcl의 puts 명령으로 출력해도 원본과 동일한 데이터가 되지 않으므로(그 반대의 경우도 마찬가지) 실제로는 사용하지 않도록 주의해야 합니다.

static int readfilechan(Tcl_Interp* interp, char* filename, Tcl_Obj** objptr)
{
  Tcl_Channel chan;
  Tcl_Obj*    inobj;

  if((chan = Tcl_OpenFileChannel(interp, filename, "r", 0644)) == NULL){
    Tcl_AppendResult(interp, "cannot open", filename, "! : ",
	             Tcl_PosixError(interp), NULL);
    return TCL_ERROR;
  }
  if(Tcl_SetChannelOption(interp, chan, "-translation", "binary") == TCL_ERROR){
    Tcl_Close(chan);
    Tcl_SetResult(interp, "SetChannelOption failed.", TCL_STATIC);
    return TCL_ERROR;
  }
  inobj = Tcl_NewObj();
  Tcl_IncrRefCount(inobj);
  for(; ; ){
    int len;
    char tbuf[1024];
    if((len = Tcl_Read(chan, tbuf, 1024)) == -1){
      Tcl_AppendResult(interp, "read error: ", Tcl_PosixError(interp), NULL);
      Tcl_DecrRefCount(inobj);
      Tcl_Close(interp, chan);
      return TCL_ERROR;
    }
    else if(len == 0) break;
    Tcl_AppendToObj(inobj, tbuf, len);
  }
  Tcl_Close(interp, chan);
  *objptr = inobj;
  return TCL_OK;
}

static int writefilechan(Tcl_Interp* interp, char* filename, Tcl_Obj* objptr){
  Tcl_Channel chan;
  char* buf;
  int   len;
  if((chan = Tcl_OpenFileChannel(interp, filename, "w", 0644)) == NULL){
    Tcl_AppendResult(interp, "cannot open", filename, "! : ", Tcl_PosixError(interp), NULL);
    return TCL_ERROR;
  }
  if(Tcl_SetChannelOption(interp, chan, "-translation", "auto") == TCL_ERROR){
    Tcl_Close(chan);
    Tcl_SetResult(interp, "SetChannelOption failed.", TCL_STATIC);
    return TCL_ERROR;
  }
  buf = Tcl_GetStringFromObj(objptr, & len);
  Tcl_Write(chan, buf, len);
  Tcl_Close(interp, chan);
  return TCL_OK;
}

 

꽤 긴 예제이지만, 파일명과 읽고 쓸 오브젝트를 인수로 받는 읽기/쓰기 각 함수는 다음과 같습니다. Tcl_OpenFileChannel이 fopen에 해당하는 파일 채널을 여는 API입니다. 세 번째 인수는 주의해야 하는데, 바이너리 I/O를 원하더라도 "rb"나 "wb"는 사용할 수 없습니다. 네 번째 인수는 리눅스 파일 시스템에서 사용되는 파일 퍼미션(쓰기 오픈 시에만 유효)입니다.

 

오픈 실패 시 에러 메시지를 얻기 위해 Tcl_PosixError가 사용되고 있는데, 이것은 일반 C 언어에서 사용되는 strerror(errno)와 같은 것입니다. 이를 사용하면 OS 레벨에서 에러가 발생했을 때(지정된 파일이 존재하지 않아 열 수 없다든지) 해당 시스템 표준의 에러 메시지를 가져올 수 있습니다. 이 예시에서는 리눅스를 사용해 본 사람이라면 한 번쯤은 봤을 법한 'no such file or directory'라는 메시지가 나올 것입니다.

 

이제 성공적으로 열리면 Tcl_Channel 타입의 포인터가 반환되는데, 이것이 FILE 구조체나 파일 기술자에 해당하는 것으로, Tcl 언어의 open 명령어가 반환하는 채널 ID와 1:1로 대응합니다. 위와 같이 C 언어에서 파일을 여는 것이 아니라, Tcl 언어의 open 명령어가 반환하는 채널 ID에서 Tcl_Channel을 받으려면 Tcl_GetChannel을 사용합니다.


Tcl_SetChannelOption은 Tcl 언어의 fconfigure 명령어와 동일합니다. 사용법도 한눈에 알아볼 수 있을 것입니다. 여기서는 굳이 사용하지 않아도 되지만, 일단 auto로 설정해 두었습니다.

 

파일 전체를 읽을 때 8.0에서 가장 편리한 방법은 미리 Tcl_NewObj로 Tcl 오브젝트를 만들어 놓고 EOF가 될 때까지 Tcl_Read(fread 같은 것)로 읽은 후 Tcl_AppendToObj로 오브젝트 뒤에 붙이는 방법입니다. 또한, 파일에 쓸 때는 Tcl_Write(fwrite 같은 것)를 사용하면 되는데, fprintf와 같은 함수는 없지만 참고로 알고 넘어가도록 합니다.

 

마지막으로 파일을 닫는 것은 fclose에 해당하는 Tcl_Close로 처리합니다. 이 외에도 Tcl_Flush, Tcl_Seek 등 API 이름만으로도 무엇을 하는지 쉽게 알 수 있는 API도 있어 파일 채널 I/O는 Tcl API 중에서도 특히 큰 비중을 차지하고 있습니다.

Tcl 8.1 이상의 파일 채널 I/O

앞의 예제는 8.0 버전 용이라 8.1에서는 이상한 결과가 나올 겁니다. 아래 8.1 버전의 샘플 두 가지를 올려보겠습니다.

 

첫 번째는 파일의 내용을 읽고 Tk의 message 위젯으로 표시하는 예제입니다.

#include <stdio.h>
#include <string.h>
#include "tcl.h"
#include "tk.h"

static int readfile(Tcl_Interp* interp, Tcl_Obj* inobj, char* filename, char* codename)
{
#define BUFSIZE 1024
  Tcl_Channel chan;
  int appendf = 0;

  if((chan = Tcl_OpenFileChannel(interp, filename, "r", 0644)) == NULL){
    fprintf(stderr, "cannot open %s\n", filename);
    return TCL_ERROR;
  }
  Tcl_SetChannelOption(interp, chan, "-encoding", codename);
  while(1){
    if(Tcl_ReadChars(chan, inobj, BUFSIZE, appendf) <= 0) break;
    if(appendf == 0) appendf = 1;
  }
  Tcl_Close(interp, chan);
}

static char showmesg(Tcl_Interp* interp, Tcl_Obj* inobj)
{
  char command[] = "message .msg -text $message\n\
                    bind .msg <Button-1> exit\n\
                    pack .msg -side top -fill both";
  Tcl_SetVar2Ex(interp, "message", NULL, inobj, 0);
  if(Tcl_Eval(interp, command) != TCL_OK){
    fprintf(stderr, "Eval error: %s\n", Tcl_GetStringResult(interp));
  }
  Tcl_UnsetVar(interp, "message", 0);
}

int main(int argc, char* argv[])
{
  int i;
  char* codename = "shiftjis";
  char* filename = NULL;
  Tcl_Interp* interp;
  Tcl_Obj*    inobj;

  for(i=1; i<argc; i++){
    if(! filename) filename = argv[i];
    else           codename = argv[i];
  }
  if(! filename){
    fprintf(stderr, "usage>%s filename [codename]\n", argv[0]);
    return 1;
  }
  interp = Tcl_CreateInterp();
  Tcl_FindExecutable(argv[0]);
  if(Tcl_Init(interp) != TCL_OK){
    fprintf(stderr, "Tcl initialization error\n"); return 1;
  }
  if(Tk_Init(interp) != TCL_OK){
    fprintf(stderr, "Tk initialization error\n"); return 1;
  }
  inobj = Tcl_NewObj();
  readfile(interp, inobj, filename, codename);
  showmesg(interp, inobj);
  Tk_MainLoop();
  return 0;
}

 

다음과 같이 컴파일하여 실행합니다.

% gcc -o chanmesg chanmesg.c -I/usr/local/include -L/usr/local/lib -O2 -Wall -ltk8.1 -ltcl8.1 -lX11 -lm
% ./chanmesg filename euckr

 

filename의 파일 내용이 표시될 것입니다. 파일이 지정한 것과 같은 한글 언어로 작성되어 있고, 해당 글꼴이 OS에 포함되어 있다면 그 글꼴도 제대로 표시될 것입니다.

 

8.1 이상에서 파일을 읽고 쓰는 방법은 먼저 8.0과 동일하게 Tcl_OpenFileChannel을 사용하면 되지만, Tcl_SetChannelOption에서 fconfigure 명령의 -translation이나 -encoding 등과 같은 방법으로 옵션을 지정할 수 있습니다. 위 예제와 같이 한글이 포함된 파일의 내용을 화면에 표시하는 등의 목적으로 사용할 경우 반드시 -encoding으로 해당 externals를 지정합니다.

 

파일의 내용을 읽으려면 8.1 이상에서는 보통 지정한 바이트 읽기(fread 같은 것)는 Tcl_ReadChars, 한 줄 읽기(fgets 같은 것)는 Tcl_GetsObj를 사용하는데, 여기서는 전자만 다루기로 합니다. Tcl_ReadChars의 두 번째 인수에는 Tcl_NewObj로 확보한 Tcl 오브젝트를, 세 번째 인수에는 한 번 읽을 때 최대 몇 바이트까지 읽을 것인지 지정합니다. 이 API는 읽은 내용을 Tcl 오브젝트 내에 직접 저장하기 때문에 버퍼용 공간을 별도로 확보할 필요가 없고, 입력 데이터가 전체 몇 바이트가 될지 예측할 수 없을 때 매우 유용합니다. 그리고 4번째 인수는 같은 파일 채널에서 여러 번 반복해서 읽을 때, 1을 지정하면 현재 2번째 인수의 Tcl 오브젝트에 들어있는 내용 뒤에 뒤에 이어 붙여서 저장해 줍니다. 0을 지정하면 현재 들어있는 내용을 버리고 새로 읽은 데이터를 오브젝트에 다시 저장합니다.  위의 예시에서는 처음 한 번만 0을 지정하고, 이후부터는 1을 지정합니다. 그러면 두 번째 이후부터는 읽으면 이전 데이터 뒤에 새로운 데이터를 추가해 주므로, 다 읽으면 Tcl 오브젝트에는 파일의 내용 전체가 들어있는 상태가 됩니다. 읽기가 끝나면 Tcl_Close를 사용하는 것은 8.0과 동일 동일합니다.

 

  • 인코딩을 지정하지 않은 경우, 해당 시스템과 로케일(언어 환경 정보)에 따라 임의로 결정됩니다. 예를 들어 한글 윈도즈 버전에서는 'cp949'가 기본값입니다. 파일의 내용이 순수한 ASCII 코드로만 구성된 경우에는 -encoding을 지정하지 않아도 됩니다.
  • 읽기가 끝났는지 여부는 Tcl_Eof라는 API 함수로 확인할 수 있으며, 위의 예제와 같이 Tcl_ReadChars가 0을 반환하는지 여부로도 알 수 있습니다.

두 번째는 아주 간단한 파일 암호화 프로그램입니다. 알고리즘은 ASCII 코드를 하나하나씩 어긋나면서 더합니다.

#include <stdio.h>
#include <string.h>
#include "tcl.h"

static void encrypt(Tcl_Channel ochan, Tcl_Channel ichan, int decodef)
{
#define BUFSIZE 1024
  Tcl_Obj* inobj, * outobj;
  unsigned char* p, * q, destbuf[BUFSIZE];
  int      i, len, index = 1;

  inobj  = Tcl_NewObj();
  outobj = Tcl_NewObj();
  while(1){
    if(Tcl_ReadChars(ichan, inobj, BUFSIZE, 0) <= 0) break;
    p = Tcl_GetByteArrayFromObj(inobj, & len);
    for(q=destbuf,i=0; i<len; i++){
      *q++ = decodef ? ( ((*p++)-index) & 255 )
	: ( ((*p++)+index) & 255 );
      if(++index == 255) index = 1;
    }
    Tcl_SetByteArrayObj(outobj, destbuf, len);
    Tcl_WriteObj(ochan, outobj);
  }
}

int main(int argc, char* argv[])
{
  int i, decodef = 0;
  Tcl_Interp* interp;
  Tcl_Channel ichan, ochan;
  if(argc < 3){
    fprintf(stderr, "usage>$s infile outfile [-decode]\n", argv[0]);
    return 1;
  }
  if(argc >= 4 && ! strncmp(argv[3], "-d", 2))
    decodef = 1;
  interp = Tcl_CreateInterp();
  if((ichan = Tcl_OpenFileChannel(interp, argv[1], "r", 0644)) == NULL){
    fprintf(stderr, "cannot open %s\n", argv[1]); return 1;
  }
  Tcl_SetChannelOption(interp, ichan, "-translation", "binary");
  if((ochan = Tcl_OpenFileChannel(interp, argv[2], "w", 0644)) == NULL){
    fprintf(stderr, "cannot create %s\n", argv[2]); return 1;
  }
  Tcl_SetChannelOption(interp, ochan, "-translation", "binary");
  encrypt(ochan, ichan, decodef);
  Tcl_Close(interp, ochan);
  Tcl_Close(interp, ichan);
  Tcl_DeleteInterp(interp);
  return 0;
}

 

다음과 같이 컴파일하여 실행합니다.

% gcc -o chancrypt chancrypt.c -I/usr/local/include -L/usr/local/lib -O2 -Wall -ltcl8.1 -lm
% ./chancrypt infile outfile
% ./chancrypt outfile infile-recovered -decode

 

이 예제는 파일을 순수 바이너리 데이터로 취급하므로 Tcl_SetChannelOption에서 반드시 -translation을 'binary'로 설정합니다. 그리고 Tcl_ReadChars로 읽은 데이터를 Tcl_GetStringFromObj가 아닌 Tcl_GetByteArrayFromObj로 가져오는 것이 핵심입니다. 이 작업을 마친 후에는 다시 Tcl_SetByteArrayObj로 Tcl 오브젝트에 다시 쓰기만 하면 됩니다. 8.1 이상에서는 보통 Tcl_WriteObj를 사용하여 Tcl 오브젝트에 저장된 유니코드 데이터를 지정된 인코딩으로 변환하여 기록합니다. 여기서는 인코딩이 binary로 되어 있으므로 인코딩 변환을 하지 않고 Tcl_SetByteArrayObj로 설정한 것을 그대로 출력합니다.