Tcl의 오브젝트와 바이너리 데이터
Tcl 8.0부터는 바이너리 데이터를 내부적으로 처리할 수 있게 되어 바이너리 데이터를 읽거나 쓰는 명령어도 만들 수 있게 되었습니다. 하지만 C 언어로 직접 확장할 때 Tcl 언어로 바이너리 데이터를 처리할 것인지 아닌지는 프로그래밍 스타일이나 사용하는 Tcl API에 매우 큰 영향을 미칩니다. 바이너리 데이터를 다루게 되면 사용하는 Tcl API의 수가 단숨에 늘어나지만, 매뉴얼에서 정식으로 Tcl 오브젝트를 사용하여 설명이 되어 있으므로 향후를 생각한다면 과감히 Tcl 오브젝트를 사용하는 것을 추천합니다. 단 여기서 Tcl 오브젝트를 사용한다고 해서 객체지향적인 프로그래밍을 생각하면 안 됩니다. (오브젝트라는 단어에 그런 방향의 향기 나 맛을 떠올리지 않는 것이 좋습니다.)
Tcl 8.0 이상에서 Tcl 변수는 내부적으로 '오브젝트'라는 단위로 처리됩니다. 오브젝트란 간단하게 말하면 데이터의 내용과 그 타입(정수나 부동소수점 숫자나 문자열), 그리고 문자열이라면 그 길이 등 몇 가지 정보가 적혀있는 구조체와 비슷한 것이라고 생각하면 됩니다. 그리고 어떤 Tcl 변수가 값을 가지고 있다는 것은 그 Tcl 변수에 값을 갖는 오브젝트에 '끈이 달려있다'라고 생각하면 됩니다. 즉, C언어로 Tcl 변수의 값을 가져오거나 설정하는 절차는,
- (읽기) 변수의 이름에서 그것이 문자열로 연결된 오브젝트를 꺼낸다.
- 그 오브젝트가 내부적으로 가지고 있는 숫자 또는 문자열 정보를 읽어온다.
- (쓰기) 준비한 숫자나 문자열을 새로 만든 오브젝트에 기록한다.
- 변수의 이름에서 해당 오브젝트에 문자열을 붙인다.
위와 같이 2단계로 구성되어 있습니다. 물론 변수가 갖는 값이 C 타입의 문자열로 제한되는 경우에는 앞서 언급한 Tcl_GetVar나 Tcl_SetVar라는 간단한 API도 준비되어 있습니다.
strcpy(command, "set a {Hello, World}");
Tcl_Eval(interp, command);
위와 같이 Tcl 언어적인 처리만을 고집할 수도 있지만, 오브젝트를 사용할 수밖에 없는 경우는 변수의 값으로 바이너리 데이터를 고려해야 하는 경우입니다.
Tcl 변수에서 오브젝트 추출하기
C 언어의 Tcl 객체는 Tcl_Obj 타입의 구조체로 표현됩니다. Tcl 변수명에서 해당 객체를 받으려면 Tcl_GetVar2Ex를 사용합니다.
Tcl_Obj* inobj;
if((inobj = Tcl_GetVar2Ex(interp, varname, NULL, 0)) == NULL){
sprintf(errmsg, "Error! no such variable: %s\n", varname);
Tcl_SetResult(interp, errmsg, TCL_STATIC);
return TCL_ERROR;
}
이것은 C에서 변수 varname에 꺼내려는 Tcl 변수의 이름을 넣어두고, 해당 오브젝트를 꺼내는 것입니다. 해당 이름의 변수가 없으면 NULL을 반환합니다. Tcl 배열 변수의 요소(예를 들어, 'arr(index)')의 객체를 꺼내고자 할 때는
strcpy(varname, "arr(index)");
로 설정한 후 위와 동일하게 해도 되고, 이게 불편하다면 varname1, varname2라는 두 개의 C 변수를 준비하면 됩니다.
strcpy(varname1, "arr");
strcpy(varname2, "index");
inobj = Tcl_GetVar2Ex(interp, varname1, varname2, 0);
오브젝트에서 설정된 값을 가져오기
여기서 꺼낸 오브젝트에는 문자열이 설정되어 있다고 가정해 봅니다. 그 내용을 꺼내려면,
- 문자열이 NULL 문자(0x00)로 끝나는 C에서 일반적으로 사용되는 의미의 '문자열'의 경우라면 Tcl_GetString
- 문자열이 완전한 바이너리, 즉 단순한 바이트의 나열인 경우라면 Tcl_GetStringFromObj
를 사용합니다. 후자의 사용법은 다음과 같습니다.
char* str;
int len;
str = (char* )Tcl_GetStringFromObj(inobj, &len);
먼저 내용을 가리키는 포인터를 str로, 바이트 수를 len으로 얻습니다. Tcl_GetString은 바이트 수를 구하는 인수가 없는데 strlen(str)으로 바로 알 수 있기 때문입니다. 이들 함수가 반환하는 주소는 Tcl_Obj 구조체 내부에 확보된 영역이므로 사용자가 직접 조작해서는 안 된다고 매뉴얼에서 경고하고 있습니다. 내용을 수정하고 싶을 경우에는 어딘가에 복사본을 가져와서 Tcl_SetStringObj 등을 통해 다시 오브젝트에 대입해야 합니다. 참고로, 한글 등 비영어권 문자열을 Tcl 객체에 설정하거나 Tcl 객체에서 가져오고 싶을 때 Tcl_GetStringFromObj나 Tcl_SetStringObj를 사용하면 낭패를 볼 수 있는데, 대신 Tcl_GetByteArrayFromObj나 Tcl_SetByteArrayObj를 사용해야 하는 경우가 있습니다.
또한 문자열이 아닌 1이나 0, 긴 정수, 부동소수점 숫자 등을 직접 구하고 싶을 때는 Tcl_GetBooleanFromObj, Tcl_GetLongFromObj, Tcl_GetDoubleFromObj라는 API가 있습니다. 이들 API는 Tcl_GetStringFromObj와 달리 Tcl 인터프리터 interp를 인수로 받습니다. 이유는, 전달된 Tcl 오브젝트에 수치로 가져올 수 있는 데이터가 없는 경우, interp->result에 해당 에러를 설정해야 하기 때문입니다.
if(Tcl_GetLongFromObj(interp, inobj, & longvalue) == TCL_ERROR) {
return TCL_ERROR;
}
비슷한 이름의 API인 Tcl_GetBoolean, Tcl_GetLong, Tcl_GetDouble은 오브젝트와 무관한 처리를 하는 API이므로 혼동하지 않도록 주의해야 합니다. 이것들은 ANSI 표준의 sscanf나 strtol 대신 사용하면 원하는 타입의 데이터로 변환하지 못했을 경우 에러 메시지를 설정해 주기 때문에 더 편리합니다.
새로운 오브젝트 만들기
새로운 객체를 생성하고 문자열 값을 설정하는 API는 Tcl_NewStringObj입니다. String 부분이 Int, Long, Double로 바뀐 이름의 API도 있습니다. 각각의 기능은 여러분이 상상하는 대로입니다.
Tcl_Obj* outobj;
outobj = Tcl_NewStringObj(str, length);
str은 설정하고자 하는 문자열을 가리키는 포인터이고, length는 그 길이입니다. 이보다 더 네이티브 한 처리를 하는 코드는 다음과 같습니다.
Tcl_Obj* outobj;
outobj = Tcl_NewObj();
Tcl_SetStringObj(outobj, str, length);
일단 Tcl_NewObj를 호출하여 Tcl 객체를 생성한 후, Tcl_SetStringObj로 문자열을 설정합니다. 길이를 지정할 수 있다는 것은 중간에 NULL 문자(0x00)가 포함된 데이터(바이너리 데이터)도 처리할 수 있다는 뜻입니다. 마찬가지로 Tcl_SetBooleanObj, Tcl_SetLongObj, Tcl_SetDoubleObj라는 API도 있습니다. 참고로 이들 API들은, 이미 데이터가 Tcl_Alloc으로 확보되어 있는 경우(문자열 데이터 등이 해당) 자동으로 이를 해제해 주기 때문에 프로그래머는 메모리 문제에 대해 의식할 필요가 없습니다.
오브젝트를 Tcl 변수로 설정하기
기존 오브젝트를 수정하거나 새로 만든 오브젝트를 Tcl 변수로 설정하는 경우에는 Tcl_SetVar2Ex를 사용합니다.
Tcl_SetVar2Ex(interp, varname, NULL, outobj, 0);
이 함수는 5개의 인수를 취하는데, 첫 번째와 마지막 인수는 메뉴얼을 참고하면 되고, 두 번째와 세 번째 인수의 사용법은 Tcl_GetVar2Ex와 동일하며, 네 번째 인수는 문제의 새로운 오브젝트를 가리키는 포인터가 됩니다.
사용한 오브젝트는 어떻게 하나요?
보통의 경우는 그냥 놔둬도 괜찮습니다. 아닌 경우는 다음에 설명합니다.
오브젝트 생성 및 삭제
오브젝트는 Tcl_NewObj 또는 이를 호출하는 상위 함수 Tcl_NewStringObj, Tcl_NewLongObj 등으로 만들어집니다. 하지만 오브젝트 삭제를 하는 API는 어디에도 없습니다. 오브젝트가 언제 삭제되는지에 대해 간단히 살펴봅니다.
이 글의 서두에서 Tcl 변수가 값을 갖는다는 것은 그 Tcl 변수에 값을 갖는 오브젝트에 '끈이 달려있다'고 생각하면 된다고 썼습니다. 즉, 그 Tcl 변수에서 끈이 뻗어 있는 한 그 오브젝트는 아직 필요한 것이기 때문에 마음대로 삭제되지는 않습니다. 두 개 이상의 변수에서 같은 오브젝트에 끈이 연결되는 경우도 있습니다. 이 경우 오브젝트는 두 곳 이상에서 필요하다고 생각하게 됩니다. 하지만 여기서 해당 Tcl 변수가 unset 명령 등으로 삭제되거나, 새로 생성된 다른 오브젝트에 문자열을 붙이면 해당 변수에서 원래의 오브젝트에 대한 문자열이 끊어지게 됩니다. 그래서 그 오브젝트에 연결되는 끈이 완전히 끊어지면, 그 오브젝트는 누구에게도 '필요 없어진' 것이기 때문에, 자동으로 그 오브젝트는 자동으로 삭제되는 것입니다.
이 '끈의 개수'를 나타내는 것이 Tcl_Obj 구조체의 멤버 refCount입니다. 이 값은 C 언어 내에서 절대로 직접 조작해서는 안 됩니다.
Tcl_GetVar2Ex와 같은 API로 변수에서 가져온 객체를 어느 정도 오랫동안 자신의 코드 내에 보관하고 싶은 프로그래머에게는 조금 문제가 있습니다.
char* GlobalInfo;
void func1(Tcl_Interp* interp)
{
Tcl_Obj* inobj;
int length;
if((inobj = Tcl_GetVar2Ex(interp, "variable", NULL, 0)) != NULL){
GlobalInfo = Tcl_GetStringFromObj(inobj, &length);
/* 여기 */
}
}
void func2(void)
{
fprintf(stdout, "%s", GlobalInfo);
}
이런 예를 생각해보면, func1이 호출되고 나서 func2가 호출되기까지 사이에 Tcl 변수 $variable이 unset 되었다면, 위의 예에서 inobj에 해당하는 메모리 영역에 있는 Tcl 오브젝트는 $variable의 끈이 끊어짐으로써 삭제되어 버릴 가능성이 있습니다. 이 때는 func2로 꺼낸 GlobalInfo 영역에는 전혀 다른 데이터가 들어있을 것입니다. 그래서 이런 경우를 위한 API가 하나 준비되어 있는데 Tcl_IncrRefCount입니다. 그 반대인 Tcl_DecrRefCount라는 API도 있습니다. 위 예제에서 /* 여기 */ 위치에서 Incr 하면, 스스로 Decr 할 때까지 해당 오브젝트는 확실히 삭제되지 않습니다. Tcl_DecrRefCount는 확실히 자신이 Tcl_IncrRefCount로 묶은 오브젝트에 대해서만 사용해야 하며, 너무 함부로 사용하면 오동작의 원인이 될 수 있습니다.