본문으로 바로가기

이벤트 제어하기

category 카테고리 없음 2025. 8. 6. 22:05

Tcl/Tk 인터프리터는 표준으로 몇 가지 이벤트 제어 메커니즘을 제공하고 있습니다. 대표적인 예가 일정 시간이 지나면 지정된 처리를 하는 타이머 핸들러, 파일을 읽고 쓸 수 있게 되면 처리를 하는 파일 핸들러, 화면 조작이 발생하면 지정한 처리를 하는 GUI(Windows 또는 X) 이벤트 핸들러가 있는데, 이러한 이벤트 핸들러를 직접 만들 수도 있습니다.

 

예를 들어, Windows의 탐색기는 표시하고 있는 디렉터리(폴더)의 파일 구성이 변경되면 몇 초 이내에 자동으로 표시가 갱신되는데, 디렉터리 내의 파일이 편집되거나 삭제되면 발생되는 이벤트를 탐색기가 모니터링하고 있다고 생각할 수 있습니다. Tcl/Tk에서 가장 간단하게 이런 감시를 하는 방법은 after 명령으로 일정 시간마다 파일 구성을 검사하는 방법이 있습니다. 사실 이벤트를 직접 만든다는 것은 after 명령어로 일정 시간 간격으로 이벤트가 발생했는지 확인하는 것과 같은 것을 아주 낮은 수준으로 구현할 수 있는데, 그 부분을 이 샘플에서 구현해 보려고 합니다.

 

아래 샘플은 'watchfile'이라는 Tcl 커맨드를 추가하는 공유 라이브러리 형태의 소스 프로그램입니다. 사용법을 먼저 보면,

watchfile transaction.log

위와 같이 파일 이름을 지정하면 해당 파일이 새로 생성되거나 삭제될 때마다, 또는 파일 크기가 변경될 때마다

[handleproc]trapped:transaction.log 10825

이런 메시지를 표준 출력으로 표시합니다. 이것은 단순히 예제용으로 만들었기 때문에 한번 등록하면 인터프리터가 종료될 때까지 지울 수 없습니다. 또한, 하나의 파일을 모니터링하기 위해 하나의 이벤트 메커니즘을 만들었기 때문에 많은 파일을 모니터링하기에는 부적합니다.

#include <sys/stat.h>
#include "tcl.h"

typedef struct {
  char filename[256];
  int  size;
} InfoStruct;

typedef struct {
  Tcl_Event   ev;
  InfoStruct* is;
} MyEvent;

static int getfilesize(char* filename)
{
  struct stat statbuf;
  int r;
  r = stat(filename, & statbuf);
  return (r < 0) ? -1 : statbuf.st_size;
}

static int WatchHandlerEventProc(Tcl_Event* e, int flags)
{
  MyEvent* me = (MyEvent* )e;
  fprintf(stdout, "[handleproc]trapped:%s %d\n",
	  me->is->filename, me->is->size);
  return 1;
}

static void WatchSetupProc(ClientData clientData, int flags)
{
  Tcl_Time blockTime;
  InfoStruct* is = (InfoStruct* )clientData;

  if (flags & TCL_IDLE_EVENTS){
    blockTime.sec  = 1;
    blockTime.usec = 0;
    Tcl_SetMaxBlockTime(&blockTime;);
  }
}

static void WatchCheckProc(ClientData clientData, int flags)
{
  MyEvent* me;
  InfoStruct* is = (InfoStruct* )clientData;
  int sz = getfilesize(is->filename);
  if(sz != is->size){
    /* 이벤트 발생됨. 구조체에 해당 정보를 가지고 이벤트 큐에 등록. */
    is->size = sz;
    me = (MyEvent* ) Tcl_Alloc(sizeof(MyEvent));
    me->ev.proc = WatchHandlerEventProc;
    me->is = is;
    Tcl_QueueEvent((Tcl_Event* )me, TCL_QUEUE_TAIL);
  }
}

static void WatchExitHandler(ClientData clientData)
{
  fprintf(stdout, "Good bye our original event source!\n");
  Tcl_DeleteEventSource(WatchSetupProc, WatchCheckProc, clientData);
  if(clientData) Tcl_Free((char* )clientData);
}

static int watchfileHandleProc(ClientData data, Tcl_Interp* interp, int argc, char* argv[])
{
  InfoStruct* is;
  if(argc < 2){
    Tcl_AppendToObj(Tcl_GetObjResult(interp), "too few arguments!", -1);
    return TCL_ERROR;
  }
  is = (InfoStruct* )Tcl_Alloc(sizeof(InfoStruct));
  strcpy(is->filename, argv[1]);
  is->size = getfilesize(is->filename);

  Tcl_CreateEventSource(WatchSetupProc, WatchCheckProc, is);
  Tcl_CreateExitHandler(WatchExitHandler, is);
  return TCL_OK;
}

DLLEXPORT int Watchfile_Init(Tcl_Interp* interp)
{
  Tcl_CreateCommand(interp, "watchfile", watchfileHandleProc, NULL, NULL);
  return TCL_OK;
}

소스의 뒷부분부터 보자면 먼저 라이브러리의 초기화 함수 Watchfile_Init에서는 이미 익숙한 Tcl 커맨드를 등록하고 있을 뿐입니다. 그리고 여기서 등록한 'watchfile' 커맨드가 사용되었을 때 호출되는 함수는 watchfileHandleProc입니다.

static int watchfileHandleProc(ClientData data, Tcl_Interp* interp, int argc, char* argv[])
{
  /* ... */
  Tcl_CreateEventSource(WatchSetupProc, WatchCheckProc, is);
  Tcl_CreateExitHandler(WatchExitHandler, is);
  return TCL_OK;
}

Tcl_CreateExitHandler는 인터프리터가 종료되기 직전에 첫 번째 인수로 등록한 함수를 실행하도록 등록하는 API로, 여기서는 등록한 이벤트 소스를 삭제하는 처리를 하는 함수 WatchExitHandler를 등록하였습니다.

static void WatchExitHandler(ClientData clientData)
{
  /* 함수의 인수는 다음과 같은 형태여야 합니다 ↑ */
  fprintf(stdout, "Good bye our original event source!\n");
  Tcl_DeleteEventSource(WatchSetupProc, WatchCheckProc, clientData);
  if(clientData) Tcl_Free((char* )clientData);
}

이벤트 소스 등록릉 Tcl_CreateEventSource를 사용합니다.

Tcl_CreateEventSource(WatchSetupProc, WatchCheckProc, is);

첫 번째, 두 번째 인수는 모두 정해진 형태의 함수에 대한 포인터입니다. 세 번째 인수는 ClientData 타입의 데이터 영역에 대한 포인터이며, 여기서 지정한 포인터는 첫 번째, 두 번째 인수로 등록한 함수에 인자로 전달됩니다. 이 WatchSetupProc와 WatchCheckProc가 이벤트 메커니즘의 핵심이 됩니다.

static void WatchSetupProc(ClientData clientData, int flags)
{
  Tcl_Time blockTime;
  InfoStruct* is = (InfoStruct* )clientData;

  if (flags & TCL_IDLE_EVENTS){
    blockTime.sec  = 1;
    blockTime.usec = 0;
    Tcl_SetMaxBlockTime(&blockTime;);
  }
}

WatchSetupProc 함수는 몇 초마다 감시할지를 설정하는 함수라고 생각해도 무방합니다. 모니터링을 하고 싶은 경우에는 그 시간 간격을 Tcl_Time 타입의 구조체에 값으로 설정하고 Tcl_SetMaxBlockTime을 호출합니다. 여기서는 1초 간격으로 지정했습니다. 즉, 1초 간격으로 함수를 호출하기 때문에 파일이 편집되거나 삭제된 순간부터 늦어도 1초 후까지 '파일이 편집되거나 삭제된' 이벤트가 발생하게 됩니다.

 

여기서 간격을 0으로 설정할 수도 있는데, 이 경우 현상이 발생하면 이벤트가 즉시 발생합니다. 이는 반대로 감시 간격을 0으로 설정하기 위해서는 WatchSetupProc가 실행되는 시점에 이미 현상 감지 여부를 판단할 수 있는 코드를 갖추고 있어야 한다는 의미이기도 합니다. 

 

또한 Tcl_SetMaxBlockSize를 함수 내에서 호출하지 않으면 모니터링 함수를 호출하지 않습니다.

 

WatchSetupProc는 어떤 이벤트가 처리될 때마다, 더 정확하게는 Tcl_DoOneEvent가 실행될 때마다 호출됩니다. 이것이 의미하는 바는 WatchSetupProc에서 Tcl_SetMaxBlockSize를 호출하지 않으면, 다른 이벤트(버튼이 눌리는 등의 GUI 이벤트)가 발생할 때까지 다시는 호출되지 않는다는 것입니다.

static void WatchCheckProc(ClientData clientData, int flags)
{
  MyEvent* me;
  InfoStruct* is = (InfoStruct* )clientData;
  int sz = getfilesize(is->filename);
  if(sz != is->size){
    /* 이벤트 발생. 구조체에 해당 정보를 가지고 이벤트 큐에 등록. */
    is->size = sz;
    me = (MyEvent* ) Tcl_Alloc(sizeof(MyEvent));
    me->ev.proc = WatchHandlerEventProc;
    me->is = is;
    Tcl_QueueEvent((Tcl_Event* )me, TCL_QUEUE_TAIL);
  }

WatchCheckProc는 감시 함수입니다. 이 함수에 대상 이벤트의 발생 여부를 실제로 체크하는 처리를 합니다. 위의 예시에서는 getfilesize로 현재 파일의 크기를 확인하고, 이전 크기 is->size와 다르면 파일이 편집되었거나 삭제되었다는 이벤트를 발생시킵니다. 이는 Tcl_QueueEvent를 사용하여 Tcl 이벤트 큐에 등록하여 수행합니다. Tcl_QueueEvent의 두 번째 인수는 상수로 큐의 어느 위치에 추가할지를 지정하는데, 보통은 맨 뒤를 나타내는 TCL_QUEUE_TAIL로 지정하면 됩니다. 첫 번째 인수는 사용자가 정의한 형태의 구조체에 대한 포인터를 Tcl_Event*에 캐스트(강제형 변환)하여 전달합니다. 이 구조체는 사용자가 이벤트의 내용을 알리기 위한 정보를 표현하기 위해 자유로운 형태로 만들 수 있지만, 구조체의 첫 번째 멤버는 반드시 Tcl_Event 타입의 변수여야 합니다.

typedef struct {
  Tcl_Event   ev;
  InfoStruct* is;
} MyEvent;

이 예제에서는 규칙에 따라 Tcl_Event 타입의 변수 ev와 자체 정보를 표현하기 위한 구조체에 대한 포인터 is를 정의한 MyEvent 구조체로 전달하고 있습니다. 그리고 이 구조체는 Tcl_Alloc(또는 ckalloc)을 사용하여 메모리 영역을 확보해야 있어야 합니다. 또한 ev.proc에는 실제로 이벤트를 처리할 함수에 대한 포인터를 반드시 설정해야 합니다.

me = (MyEvent* ) Tcl_Alloc(sizeof(MyEvent));
me->ev.proc = WatchHandlerEventProc;

그래서 이 예제에서는 위와 같이 설정하고 있습니다.

static int WatchHandlerEventProc(Tcl_Event* e, int flags)
{
  MyEvent* me = (MyEvent* )e;
  fprintf(stdout, "[handleproc]trapped:%s %d\n", me->is->filename, me->is->size);
  return 1;
}

위의 WatchHandlerEventProc가 실제로 이벤트를 처리하는 함수입니다. 첫 번째 인수는 Tcl_Event* 타입으로 전달되므로, 이를 사용자가 정의한 구조체로 다시 캐스트 하여 사용해야 합니다. 두 번째 인수에는 이 이벤트를 발생시킨 Tcl_DoOneEvent에 대한 인수( 반드시 같은 값은 아닙니다. Tcl_DoOneEvent의 인수가 0인 경우, 상수 TCL_ALL_EVENTS로 변환되어 전달됩니다.)가 전달됩니다. WatchHandlerEventProc는 이벤트가 정상적으로 처리되면 1을, 그렇지 않으면 0을 반환하도록 하며, 1을 반환하면 이벤트 큐는 해당 이벤트를 삭제하고, 앞서 Tcl_Alloc으로 확보 한 메모리 영역을 자동으로 해제하여 이 이벤트에 대한 처리를 완료합니다. 0을 반환하면, 이 이벤트를 그대로 다시 이벤트 큐의 맨 끝으로 돌려서 다시 시도하게 합니다.

 

등록한 이벤트 소스를 삭제하려면, Tcl_CreateEventSource와 완전히 동일한 인수로 Tcl_DeleteEventSource를 호출하면 됩니다.