본문으로 바로가기

새로운 Tcl 수학 함수 등록하기

category 카테고리 없음 2025. 8. 6. 17:18

새로운 Tcl 수학 함수를 등록하려면

expr 명령어로 사용할 수 있는 sin, cos 등의 수학 함수를 직접 등록할 수도 있습니다. 굳이 수학적이지 않아도 상관없으며,

  • 매개변수는 몇 개든 상관없지만, 모두 숫자여야 합니다.
  • 값으로 수치를 반환해야 합니다.

위의 조건을 만족하면 됩니다. 여기서는 샘플로 'enad'라는 함수를 만들어 보겠습니다. 이 함수는 1개의 인수를 받아 200보다 크면 그대로, 그렇지 않으면 1900이 더해진 숫자를 정수로 반환합니다.

static int enadMathProc(ClientData clientData, Tcl_Interp* interp, Tcl_Value* args, Tcl_Value* result)
{
  int v;
  if(args[0].type == TCL_DOUBLE)
    v = (int)args[0].doubleValue;
  else
    v = args[0].intValue;
  result->type = TCL_INT;
  result->intValue = (v >= 200) ? v : (v+1900);
  return TCL_OK;
}

DLLEXPORT int Enad_Init(Tcl_Interp* interp)
{
  Tcl_ValueType typeInitializer[] = {TCL_EITHER};
  Tcl_CreateMathFunc(interp, "enad", 1, typeInitializer, enadMathProc, NULL);
  return TCL_OK;
}

수학 함수 등록 방법은 Tcl_CreateCommand나 Tcl_RegisterObjType과 마찬가지로 인터프리터 초기화 시 Tcl_CreateMathFunc를 사용합니다.

Tcl_ValueType typeInitializer[] = {TCL_EITHER};
Tcl_CreateMathFunc(interp, "enad", 1, typeInitializer, enadMathProc, NULL);

두 번째 인수는 등록할 수학 함수의 이름, 세 번째 인수는 그 매개변수(인수)의 개수이고, 네 번째 인수는 해당 개수의 요소를 가진 Tcl_ValueType 타입의 배열 주소를 지정한다. 이것은 각 인수의 타입이 정수(TCL_INT)인지 부동소수점수(TCL_DOUBLE)인지(TCL_EITHER)에 따라 처리가 달라지는 기호 상수로 지정합니다. TCL_INT 또는 TCL_DOUBLE을 지정한 인수는 사전에 해당 인자로 자동 변환됩니다. TCL_EITHER의 경우는 함수 내에서 구분하여 처리를 합니다. 함수는

static int enadMathProc(ClientData clientData, Tcl_Interp* interp, Tcl_Value* args, Tcl_Value* result)

 

위와 같은 형태이며, TCL_OK 또는 TCL_ERROR를 반환합니다. args는 등록 시 지정한 인수 개수만큼의 요소로 구성된 Tcl_Value 타입의 배열로, args[i].type이 TCL_INT라면 args[i].intValue, TCL_DOUBLE이라면 args[i].doubleValue에 값이 들어있으므로 이를 가져와서 사용합니다. 계산 결과는 세 번째 인수로 전달된 Tcl_Value 타입의 포인터에 대입하여 반환합니다. 계산 결과가 정수라면 result->type에 TCL_INT, 부동소수점수라면 TCL_DOUBLE을 설정하고, 전자의 경우 result->intValue, 후자의 경우 result->doubleValue에 해당 값을 설정합니다.


만약 아래와 같은 경우라면

expr enad(80)

 

아래와 같은 값이 설정됩니다.

args[0].type = TCL_INT
args[0].intValue = 1980

알고 있으면 편리한 API들

매뉴얼에 유틸리티 프로시저(Utility Procedure)라고 부르는 함수들이 있습니다. Tcl 인터프리터 핵심 부분(구문 분석, 변수 관리 등)과 직접적으로 관련이 없지만, Tcl 언어의 커맨드 구현에 사용되어 공식 라이브러리가 된 API들입니다. 이들은 일반적으로 Tcl_Interp 타입의 변수 interp를 인수로 받지 않고, Tcl 언어와 분리되어 사용이 가능하므로 부담 없이 사용할 수 있습니다.

문자열에서 숫자로

Tcl 커맨드를 만들다 보면 문자열을 숫자로 변환할 경우가 매우 많을 것입니다. 보통은 strtol이나 sscanf와 같은 ANSI C 표준 함수를 사용하지만, Tcl 언어의 숫자 리터럴과 동일한 변환 방식으로 이를 수행해 주는 API들이 있습니다. 에러가 발생하면 인터프리터에 에러도 설정해 주기 때문에 보통은 해당 API들을 사용하는 것이 좋습니다.

double d;
Tcl_ResetResult(interp);
if(Tcl_GetDouble(interp, buf, &d) == TCL_ERROR)
  fprintf(stdout, "cannot get double: %s\n", Tcl_GetStringResult(interp));
else
  fprintf(stdout, "double: %g\n", d);

위의 Tcl_GetDouble은 두 번째 인자 buf에 들어있는 문자열을 double 타입의 숫자로 변환하여 세 번째 인자로 전달된 주소로 설정합니다. 숫자로 해석할 수 없는 문자열이 전달되면 interp에 에러 메시지가 설정되어 TCL_ERROR가 반환됩니다. Tcl_GetBoolean과 Tcl_GetInt도 사용법은 거의 동일하지만, 상위 호환 API인 Tcl_GetBooleanFromObj나 Tcl_GetLongFromObj와 다른 점이 한 가지 있습니다. Tcl_GetLongFromObj에서는 변환 대상 문자열이 '10.05'와 같은 경우 소수점이 떨어져 10이라는 숫자로 변환되지만, Tcl_GetInt에 '10.05'와 같은 소수를 전달하면 에러가 발생합니다.

숫자에서 문자열로

숫자에서 문자열로 변환하려면 물론 printf 함수를 사용하면 되지만, Tcl_PrintDouble이라는 API도 있습니다. 이를 사용하면 printf의 "%g" 스타일로 Tcl 언어에서 정수와 명확하게 구분하기 위해 'e' 또는 '.' 중 하나가 반드시 포함된 문자열을 생성합니다. 사용법은 다음과 같습니다.

char buf[50];
double doubleValue;
/* ... */
Tcl_PrintDouble(interp, doubleValue, buf);

buf는 충분한 크기를 확보해 두는 것이 좋습니다.

"인수의 개수가 잘못되었습니다." 오류 메시지 자동 생성

Tcl 커맨드를 직접 작성할 때, 먼저 반드시 '인수의 개수를 검사하고, 잘못된 경우 에러 메시지를 interp로 설정하고 TCL_ERROR를 반환하는 방식으로 작성하게 되는데, 이를 쉽게 할 수 있는 API가 있습니다.

static int xxxxHandleProc(ClientData data, Tcl_Interp* interp, int objc, Tcl_Obj* CONST objv[])
{
  if(objc != 3){
    Tcl_WrongNumArgs(interp, 1, objv, "channelId message");
    return TCL_ERROR;
  }
}

Tcl_WrongNumArgs 하나로 오류 메시지까지 처리해 주는 편리한 함수로, Tcl 표준 명령어 구현 부분에서도 많이 사용되고 있습니다.

옵션 분석 편의 함수

Tcl_WrongNumArgs와 함께 비슷한 상황에서 사용할 수 있는 전용 API가 하나 더 있습니다. Tcl 명령어를 만들 때 유용하게 사용할 수 있으므로, 사용법을 익혀두는 것이 좋습니다.

static int xxxxHandleProc(ClientData data, Tcl_Interp* interp, int objc, Tcl_Obj* CONST objv[])
{
  int i;
  for(i=1; i<objc; i++){
    int index;
    static char* table[] = {
      "-a", "-b", "-c", "-d", "-e", NULL
    };
    r = Tcl_GetIndexFromObj(interp, objv[i], (char**)(&table),
			    "OPTION", 0, &index);
    if(r == TCL_OK){
      switch(index){
      case 0: /* -a옵션이 지정됨 */ break;
      case 1: /* -b옵션이 지정됨 */ break;
      case 2: /* -c옵션이 지정됨 */ break;
      /* ...... */
      }
    } else {
      /* "Bad option ..." 와 같은 오류 메시지가 이미 설정되어 있음 */
      return TCL_ERROR;
    }
  }

Tcl_GetIndexFromObj는 이 작업에 특화된 옵션 분석용 API로, 해당 명령어가 취할 수 있는 옵션의 테이블을 준비해 두고(위의 table), 현재 인수 objv[i]가 그중 몇 번째 옵션에 해당하는지를 여섯 번째 인수의 주소로 설정해 줍니다. 이 API는 옵션의 축약형도 지원합니다. (다섯 번째 인수에 TCL_EXACT를 지정하면 축약형을 허용하지 않습니다.) 예를 들어 취할 수 있는 옵션이 "-longoption"과 "-mottolongoption" 두 가지인 경우, 인수로 "-mo"가 주어지면, 그것이 유일한 해당 옵션의 축약형인 "-mottolongoption"이 지정된 것으로 인식시켜 줍니다. 또한, 인수가 어떤 옵션에도 해당하지 않는 경우, 예를 들어 위의 예에서 "-wrong"으로 지정한 경우,

bad OPTION "-wrong": must be -a, -b, -c, -d, or -e.

위와 같은 에러 메시지가 자동으로 생성됩니다. 네 번째 인수인 "OPTION"은 위의 메시지에 사용됩니다.

파일 경로 분석

Tcl 언어의 장점 중 하나는 파일 조작이 VisualBasic이나 Java 등(Perl, Python 등 일부 제외) 많은 언어보다 훨씬 충실하다는 점인데, 그 커맨드 구현시 사용되었던 파일명 조작을 하는 API도 사용할 수 있습니다.

int argc;
char** argv;
int i;

Tcl_SplitPath(path, &argc, &argv);
for(i=0; i<argc; i++)
  fprintf(stdout, "argv[%d]=<%s>\n", i, argv[i]);
Tcl_Free((char* )argv);

Tcl_SplitPath는 문자열로 주어진 파일 경로를 분석하여 백슬래시나 두 개 이상의 연속된 구분 기호 등을 적절히 정리하여 디렉터리 계층별로 이름을 분리하는 API입니다. 세 번째 인수는 포인터의 포인터로 전달되는데 argv와 같이 상위 계층부터 순서대로 이름이 저장됩니다. 경로가 절대 경로인 경우 argv[0]은 "/"이 되어 argv[1]부터 시작하지만, 상대 경로인 경우 argv[0]부터 시작합니다. 예를 들어, "/home/ihmin/local/a.c"인 경우

argv[0] ... /
argv[1] ... home
argv[2] ... ihmin
argv[3] ... local
argv[4] ... a.c

가 되지만, "home/ihmin/local/a.c"인 경우는

argv[0] ... home
argv[1] ... ihmin
argv[2] ... local
argv[3] ... a.c

가 됩니다. 사용이 끝나면 위와 같이 Tcl_Free로 배열 전체 메모리를 해제하면 됩니다. 

 

Tcl_JoinPath는 그 반대의 역할을 하는 API로, 첫 번째 인수로 지정한 수의 디렉터리 계층 배열을 두 번째 인수로 지정하고, 이를 연결한 것을 Tcl_DString 타입의 포인터로 반환하는 API입니다.

Tcl_DString ds;
Tcl_DStringInit(&ds);
Tcl_JoinPath(argc, argv, &ds);
fprintf(stdout, "joined=<%s>\n", Tcl_DStringValue(&ds));
Tcl_DStringFree(&ds);

이렇게 얻은 전체 경로는 Tcl_DStringValue로 꺼내어 사용하거나, Tcl_JoinPath 반환값도 동일하므로 이를 꺼내어 사용하면 됩니다. 사용이 끝나면 일반 DString과 마찬가지로 Tcl_DStringFree로 해제하면 됩니다.

Tcl 리스트 분석

Tcl_SplitPath가 파일 경로를 분석하는 반면, 전통적인 Tcl 리스트를 분리하거나 연결하는 API도 거의 동일한 방식으로 사용할 수 있도록 준비되어 있습니다.

int argc;
char** argv;
int i;

Tcl_SplitList(interp, listbuf, &argc, &argv);
for(i=0; i<argc; i++)
  fprintf(stdout, "argv[%d]=<%s>\n", i, argv[i]);
Tcl_Free((char* )argv);

Tcl_SplitList는 리스트 분리를 수행하는 API로, 사용법은 Tcl_SplitPath와 동일합니다.

char* p = Tcl_Merge(argc, argv);
fprintf(stdout, "joined=<%s>\n", p);
Tcl_Free(p);

Tcl_Merge는 리스트 연결을 수행하는 API로, Tcl_SplitList와 Tcl_Merge를 연속적으로 호출하면 2개 이상의 연속된 공백 문자 등을 하나로 줄여주기도 합니다. Tcl_Merge의 반환값이 필요 없어지면 Tcl_Free로 메모리를 해제합니다.

 

사용법을 보면 알겠지만, 이들 API는 바이너리 데이터를 갖는 리스트는 처리할 수 없습니다. 바이너리 데이터를 갖는 리스트를 처리하려면 Tcl_ListObjIndex와 같은 ListObj 함수군을 사용하면 됩니다.

안전한 사용을 위해

Tcl/Tk에는 현재 인터넷에서 사용되는 통신 프로토콜인 TCP/IP를 기반으로 통신하는 프로그램을 작성하기 위해 UNIX의 가장 표준적으로 구현된 BSD 소켓(socket)을 이용하여 통신하는 메커니즘이 준비되어 있습니다. 간단히 말해서, 웹에서 특정 URL의 페이지를 다운로드하는 프로그램, 혹은 반대로 컴퓨터에 있는 콘텐츠를 웹으로 전송하는 웹 서버(httpd) 등의 '네트워크 응용 프로그램'을 ( (직접 소켓으로 통신하는 프로그램을 작성하는 것보다 쉽게) 만들 수 있습니다. 하지만 이런 프로그램으로 Tcl 언어의 모든 기능을 제공하는 것은 매우 위험할 수도 있습니다. 예를 들어 네트워크로 연결된 외부의 컴퓨터에서 임의의 Tcl 명령이나 Tcl 스크립트를 실행할 수 있는 응용 프로그램을 만들었다고 가정하면, 'exec', 'glob', 'socket', 'file delete' 등의 Tcl 명령이 외부에서 실행되면 끔찍한 일이 벌어질 겁니다. 그래서 Tcl API에는 사용자가 tclsh의 기능을 확장할 수 있을 뿐만 아니라, 본래의 tclsh에서 기능을 축소한 버전을 만들 수 있는 기능도 준비되어 있습니다. 아래 예제는 겉보기에는 평범한 tclsh이지만, 어떤 Tcl 커맨드를 사용하려고 하면 '명령어가 없습니다'라고 메시지를 내보냅니다.

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

static int hideHandleProc(ClientData data, Tcl_Interp* interp, int argc, char* argv[])
{
  char buf[256], hname[256];
  Tcl_ResetResult(interp);
  if(argc != 2){
    sprintf(buf, "illegal number of arguments, should be: %s command", argv[0]);
    Tcl_AppendToObj(Tcl_GetObjResult(interp), buf, -1);
    return TCL_ERROR;
  }
  sprintf(hname, "%s_secret", argv[1]);
  if(*argv[0] == 'h')
    return Tcl_HideCommand(interp, argv[1], hname);
  else
    return Tcl_ExposeCommand(interp, hname, argv[1]);
}

int main(int argc, char* argv[])
{
  char command[1024];
  Tcl_Interp* interp;

  interp = Tcl_CreateInterp();
  Tcl_FindExecutable(argv[0]);
  if(Tcl_Init(interp) == TCL_ERROR){
    fprintf(stderr, "Tcl initialization error: %s\n", Tcl_GetStringResult(interp));
    return 1;
  }
  Tcl_MakeSafe(interp);

  Tcl_CreateCommand(interp, "hide",   hideHandleProc, NULL, NULL);
  Tcl_CreateCommand(interp, "unhide", hideHandleProc, NULL, NULL);

  fprintf(stdout, "TCLSH(initial)%% ");
  while(fgets(command, 1024, stdin)){
    char *p, *q;
    char arg[1024];
    if(Tcl_Eval(interp, command) == TCL_ERROR)
      fprintf(stderr, "ERROR: %s\n", Tcl_GetStringResult(interp));
    else
      fprintf(stdout, "%s\n", Tcl_GetStringResult(interp));
    fprintf(stdout, "TCLSH(%s)%% ",
	    Tcl_IsSafe(interp) ? "safe" : "normal");
  }
}

네트워크 상에서 사용될 때 위험하다고 판단되는 Tcl 커맨드를 정리하거나 안전한 기능만으로 축소하여 안전하게 만들어 주는 API가 Tcl_MakeSafe입니다. 또한 Tcl 인터프리터가 안전한지 여부를 알고 싶을 때는 Tcl_IsSafe를 사용합니다. 안전을 위해 축소되는 커맨드로는 'source', 'exec', 'open', 'file', 'exit', 'glob', 'pwd', 'stat', 'socket' 등이 있습니다. 이 외에도 Tcl_MakeSafe 이전에 사용자가 직접 자체 확장을 통해 Tcl 커맨드를 추가한 경우, 이 명령어들은 모두 삭제됩니다.

 

이와는 별도로 사용자가 사용하기에 불편한 Tcl 명령어를 일시적으로 사용하지 못하도록 했다가 다시 사용할 수 있도록 하는 API도 제공하고 있습니다.

  char* hname = "secret";
  /* 비활성화하기 */
  Tcl_HideCommand(interp, "expr", hname);
  /* 활성화 하기 */
  Tcl_ExposeCommand(interp, hname, "expr");

위와 같이 사용자가 사용하지 못하게 하려면 Tcl_HideCommand, 사용 가능하게 하려면 Tcl_ExposeCommand라는 API를 사용합니다. hide는 '숨기다', expose는 '노출시키다'라는 의미 그대로 받아들이면 됩니다. Tcl_HideCommand의 두 번째 인수에는 이미 노출되어 있는 Tcl 커맨드를 지정해야 합니다. (참고로 인터프리터 초기화 직후에는 모든 명령어가 노출되어 있습니다). Tcl_ExposeCommand의 세 번째 인수에는 이미 숨겨 있는 Tcl 커맨드를 지정해야 합니다. 또한 Tcl_ExposeCommand의 두 번째 인수에는 Tcl_HideCommand의 세 번째 인수에 사용된 비밀 커맨드 이름(위의 hname)을 알고 있어야 하며, 이를 정확히 지정해야 노출을 시킬 수 있습니다.

 

위 샘플을 컴파일한 tclsh는 'hide', 'unhide'라는 두 개의 Tcl 커맨드가 추가되어 있는데, 'hide expr'을 입력하면 이후 'unhide expr'을 입력할 때까지 'expr' 명령어를 사용할 수 없게 됩니다.

 

또한 Tcl_DeleteCommand라는 API도 있는데, 일시적으로 사용하지 못하게 하는 것이 아니라 인터프리터에서 완전히 지워버리는 역할을 합니다.