[루아1.1] table.h table.c 읽기

7 분 소요

Table.h

저는 처음에 파일 이름만 보고 Table.c와 Table.h 파일은 루아의 테이블 타입 자료 구조를 구현한 것인줄 알았습니다. 그런데 Table.h에 익스턴 변수 이름을 보니 심볼 테이블을 구현한 것 같습니다.

extern Symbol *lua_table;
extern Word    lua_ntable;

extern char  **lua_constant;
extern Word    lua_nconstant;

extern char  **lua_string;
extern Word    lua_nstring;

extern Hash  **lua_array;
extern Word    lua_narray;

extern char   *lua_file[];
extern int     lua_nfile;

extern Word    lua_block;
extern Word    lua_nentity;

위에서부터 순서대로 심볼 테이블, 상수 테이블, 문자열 테이블, 배열 테이블 인것 같습니다. lua_file과 lua_block, lua_nentity는 뭔지 모르겠군요. 코드를 보면 알겠지요.

Table.c

#define streq(s1,s2)	(s1[0]==s2[0]&&strcmp(s1+1,s2+1)==0)

#ifndef MAXSYMBOL
#define MAXSYMBOL	512
#endif
static Symbol  		tablebuffer[MAXSYMBOL] = {
                                    {"type",{T_CFUNCTION,{lua_type}}},
                                    {"tonumber",{T_CFUNCTION,{lua_obj2number}}},
                                    {"next",{T_CFUNCTION,{lua_next}}},
                                    {"nextvar",{T_CFUNCTION,{lua_nextvar}}},
                                    {"print",{T_CFUNCTION,{lua_print}}},
                                    {"dofile",{T_CFUNCTION,{lua_internaldofile}}},
                                    {"dostring",{T_CFUNCTION,{lua_internaldostring}}}
                                                 };
Symbol	       	       *lua_table=tablebuffer;
Word   	 		lua_ntable=7;

streq() 매크로는 여기에 또 정의했네요. 구현은 조금 다릅니다. 비교하는 두 문자열 첫 글자가 다르면 strcmp() 함수 호출을 하지 않고 바로 false를 리턴합니다. 그런데 제가 알기로 strcmp() 함수 구현도 첫 글자부터 다르면 바로 리턴하거든요. 개인적 의견으로는 쓸데없는 구현이라고 생각합니다. MAXSYMBOL은 512이고 Symbol 타입으로 512개 배열을 선언했습니다.

typedef struct
{
 char   *name;
 Object  object;
} Symbol;

Symbol 구조체는 이렇게 생겼습니다. name과 object인데, tablebuffer 초기화 데이터를 보면 name에는 함수 이름이나 명령어 이름 같이 보이는 문자열이 있고, Object는 함수 포인터입니다. 아직은 어디에 쓰려고 이렇게 만든건지 모르겠습니다. 초기값으로 함수 포인터 7개를 넣었으므로 lua_ntable은 7입니다.

struct List
{
 Symbol *s;
 struct List *next;
};

static struct List o6={ tablebuffer+6, 0};
static struct List o5={ tablebuffer+5, &o6 };
static struct List o4={ tablebuffer+4, &o5 };
static struct List o3={ tablebuffer+3, &o4 };
static struct List o2={ tablebuffer+2, &o3 };
static struct List o1={ tablebuffer+1, &o2 };
static struct List o0={ tablebuffer+0, &o1 };
static struct List *searchlist=&o0;

struct List는 Symbol 타입 자료 구조를 링크드 리스트로 만들려고 만든 구조체입니다. 이 링크드 리스트 구조체에 tablebuffer를 하나씩 넣어서 링크드 리스트로 만듭니다. 저는 이 코드를 이해할 수 없습니다. 이미 tablebuffer가 배열이고 크기도 512로 한정되 있는데 이걸 왜 굳이 링크드 리스트로 다시 만드는지 알 수 없군요.

#ifndef MAXCONSTANT
#define MAXCONSTANT	256
#endif
/* pre-defined constants need garbage collection extra byte */ 
static char tm[] = " mark";
static char ti[] = " nil";
static char tn[] = " number";
static char ts[] = " string";
static char tt[] = " table";
static char tf[] = " function";
static char tc[] = " cfunction";
static char tu[] = " userdata";
static char  	       *constantbuffer[MAXCONSTANT] = {tm+1, ti+1,
						       tn+1, ts+1,
						       tt+1, tf+1,
						       tc+1, tu+1
                                                      };
char  	      	      **lua_constant = constantbuffer;
Word    		lua_nconstant=T_USERDATA+1;

위 코드의 주석을 봐도 그렇고, 상수 배열 초기값을 봐도 그렇고 첫 바이트 인덱스 위치를 비워둡니다. 지난번에 읽었던 함수에서도 첫 바이트 인덱스를 마크용으로 비워두는 코드가 있었습니다. 그걸 가비지 콜렉션 용으로 쓴다는 것 같은데 어떻게 쓰는건지 영 모르겠군요. 루아의 데이터 타입 이름을 상수 배열 초기값으로 넣었습니다. 상수 배열인데 예약어 관리도 같이 해주는 건가하는 생각이 듭니다.

#ifndef MAXSTRING
#define MAXSTRING	512
#endif
static char 	       *stringbuffer[MAXSTRING];
char  		      **lua_string = stringbuffer;
Word    		lua_nstring=0;

#define MAXFILE 	20
char  		       *lua_file[MAXFILE];
int      		lua_nfile;


#define markstring(s)   (*((s)-1))

/* Variables to controll garbage collection */
Word lua_block=10; /* to check when garbage collector will be called */
Word lua_nentity;   /* counter of new entities (strings and arrays) */

문자열 배열은 초기값이 없습니다. 그냥 lua_string 변수에 포인터를 넘기고 끝나네요. lua_block, lua_nentity는 분명 가비지 컬랙션과 관계가 있는 것 같은데 어떻게 하는 것인지 궁금합니다.

/*
** Given a name, search it at symbol table and return its index. If not
** found, allocate at end of table, checking oveflow and return its index.
** On error, return -1.
*/
int lua_findsymbol (char *s)
{
 struct List *l, *p;
 for (p=NULL, l=searchlist; l!=NULL; p=l, l=l->next)
  if (streq(s,l->s->name))
  {
   if (p!=NULL)
   {
    p->next = l->next;
    l->next = searchlist;
    searchlist = l;
   }
   return (l->s-lua_table);
  }

 if (lua_ntable >= MAXSYMBOL-1)
 {
  lua_error ("symbol table overflow");
  return -1;
 }
 s_name(lua_ntable) = strdup(s);
 if (s_name(lua_ntable) == NULL)
 {
  lua_error ("not enough memory");
  return -1;
 }
 s_tag(lua_ntable) = T_NIL;
 p = malloc(sizeof(*p)); 
 p->s = lua_table+lua_ntable;
 p->next = searchlist;
 searchlist = p;

 return lua_ntable++;
}

lua_findsymbol() 함수는 전에 읽었던 함수 같네요. 또 읽죠 뭐. 주석에 동작이 잘 설명되 있으니 기본 동작은 주석을 읽으면 됩니다. 사실 그냥 읽고 넘어가는 것이 목적이기에 주석만 보고 이런 기능을 하는 구나하면 됩니다. 그런데 for 문 블록 코드가 특이해서 좀 자세히 읽어 봤습니다. 특히 아래 코드 부분이 눈으로만 봐서는 직관적으로 이해가 안되더라구요.

   if (p!=NULL)
   {
    p->next = l->next;
    l->next = searchlist;
    searchlist = l;
   }
   return (l->s-lua_table);

이 코드 보자마자 뭐 하는 코드인지 머릿속에서 흐름이 그려지는 분도 있겠지만, 저는 그게 안돼서 그림 그려보고서야 이해했습니다. 간략히 설명하면 심볼 테이블에서 심볼을 찾으면 방금 찾은 심볼 위치를 searchlist에 저장해 뒀다가 다음에는 직전 검색에서 hit 했던 위치에서부터 심볼 검색을 다시 시작하는 것입니다. 아마도 프로그래밍 언어론에 나오는 변수 지역성 이런것을 고려해서 직전에 검색한 심볼을 바로 이어서 다시 검색할 가능성이 많으니 시간을 줄이려고 코드를 위와 같이 만든것 같습니다. 만약 심볼 테이블에 찾고자하는 심볼이 없으면 심볼 테이블에 심볼을 추가하고 갯수를 늘립니다.

/*
** Given a constant string, search it at constant table and return its index.
** If not found, allocate at end of the table, checking oveflow and return 
** its index.
**
** For each allocation, the function allocate a extra char to be used to
** mark used string (it's necessary to deal with constant and string 
** uniformily). The function store at the table the second position allocated,
** that represents the beginning of the real string. On error, return -1.
** 
*/
int lua_findconstant (char *s)
{
 int i;
 for (i=0; i<lua_nconstant; i++)
  if (streq(s,lua_constant[i]))
   return i;
 if (lua_nconstant >= MAXCONSTANT-1)
 {
  lua_error ("lua: constant string table overflow"); 
  return -1;
 }
 {
  char *c = calloc(strlen(s)+2,sizeof(char));
  c++;		/* create mark space */
  lua_constant[lua_nconstant++] = strcpy(c,s);
 }
 return (lua_nconstant-1);
}

lua_findconstant() 함수는 lua_constant 배열에서 파라메터 s를 검색합니다. constant를 보통 상수라고 번역하는데, 루아 구현에서는 변수 이름이 lua_findconstant() 함수에 입력으로 들어갑니다. 그럼 심볼은 뭐지?하는 의문이 바로 듭니다. 심볼에는 파라메터 이름이나 함수 이름이 들어갑니다. 혼란스럽군요. 왜 이런식으로 구분했는지 모르겠네요.

/*
** Traverse symbol table objects
*/
void lua_travsymbol (void (*fn)(Object *))
{
 int i;
 for (i=0; i<lua_ntable; i++)
  fn(&s_object(i));
}


/*
** Mark an object if it is a string or a unmarked array.
*/
void lua_markobject (Object *o)
{
 if (tag(o) == T_STRING)
  markstring (svalue(o)) = 1;
 else if (tag(o) == T_ARRAY)
   lua_hashmark (avalue(o));
}

lua_travsymbol() 함수는 심볼 테이블 배열을 순회하면서 파라메터로 넘어온 함수에 적용하는 것입니다. 그런데 루아 1.1 소스 코드에서는 lua_markobject() 함수 하나만 사용합니다. lua_markobject() 함수는 Object 구조체에 mark를 1로 바꾸는 동작을 합니다.

/*
** Garbage collection. 
** Delete all unused strings and arrays.
*/
void lua_pack (void)
{
 /* mark stack strings */
 lua_travstack(lua_markobject);
 
 /* mark symbol table strings */
 lua_travsymbol(lua_markobject);

 lua_stringcollector();
 lua_hashcollector();

 lua_nentity = 0;      /* reset counter */
} 

/*
** Garbage collection to atrings.
** Delete all unmarked strings
*/
void lua_stringcollector (void)
{
 int i, j;
 for (i=j=0; i<lua_nstring; i++)
  if (markstring(lua_string[i]) == 1)
  {
   lua_string[j++] = lua_string[i];
   markstring(lua_string[i]) = 0;
  }
  else
  {
   free (lua_string[i]-1);
  }
 lua_nstring = j;
}

주석에 따르면 lua_pack() 함수는 가비지 컬랙션을 수행하는 함수입니다. 함수 내부에서는 특별한 로직없이 다른 함수를 연달아 호출만 하는 배치(batch) 함수입니다. lua_travstack()과 lua_travsymbol()은 lua_markobject() 함수 포인터를 각각 스택과 심볼 테이블을 대상으로 돌리는 함수입니다. 그리고 lua_stringcollector() 함수와 lua_hashcollector() 함수를 호출합니다. lua_hashcollector() 함수는 Hash.h와 Hash.c 파일을 읽을 때 읽었습니다. 그래서 지금은 lua_stringcollector() 함수만 읽어 보겠습니다. 구현은 간단합니다. 마크용으로 비워둔 문자열의 첫 번째 바이트가 1이면 마크를 다시 0으로 바꾸고 lua_string 배열에 다시 쌓습니다. 만약 1이 아니면 해당 문자열 버퍼를 해제합니다.

음.. 확실치는 않으나 루아의 가비지 컬랙션은 이런식으로 동작하는 것 같습니다. 모든 문자열은 lua_string 배열에 일단 들어가고 그 중 레퍼런스되는 문자열이나 심볼은 각각 심볼 테이블이나 스택에 포인터를 복사합니다. 그리고 일정 시점마다 lua_pack() 함수를 돌려서 현재 레퍼런스가 살아 있는 포인터에는 표시를 합니다. 그런다음 lua_stringcollector() 함수와 lua_hashcollector() 함수를 돌려서 마크가 없는 포인터를 해제합니다. 확실치는 않으나 이런것 같군요. 뭔가 이런 방식보다는 더 직관적으로 이해하기 쉽고 구현도 깔끔한 방법이 있을 것같다는 느낌이 듭니다만, 아직 1.1이라 그런것이라고 생각하고 넘어가겠습니다.

char *lua_createstring (char *s)
{
 int i;
 if (s == NULL) return NULL;
 
 for (i=0; i<lua_nstring; i++)
  if (streq(s,lua_string[i]))
  {
   free(s-1);
   return lua_string[i];
  }

 if (lua_nentity == lua_block || lua_nstring >= MAXSTRING-1)
 {
  lua_pack ();
  if (lua_nstring >= MAXSTRING-1)
  {
   lua_error ("string table overflow");
   return NULL;
  }
 } 
 lua_string[lua_nstring++] = s;
 lua_nentity++;
 return s;
}

lua_createstring() 함수도 문법 구현 읽을 때 한 번 봤던 함수입니다. 그때는 lua_pack() 함수를 왜 저기서 호출하는지 몰랐는데 이제는 대충 알 것 같습니다. lua_block이 10인데 대충 문자열이 10개 새로 생길 때 마다 한 번씩 가비지 컬랙션을 수행하는 것으로 이해됩니다.

/*
** Add a file name at file table, checking overflow. This function also set
** the external variable "lua_filename" with the function filename set.
** Return 0 on success or 1 on error.
*/
int lua_addfile (char *fn)
{
 if (lua_nfile >= MAXFILE-1)
 {
  lua_error ("too many files");
  return 1;
 }
 if ((lua_file[lua_nfile++] = strdup (fn)) == NULL)
 {
  lua_error ("not enough memory");
  return 1;
 }
 return 0;
}

/*
** Delete a file from file stack
*/
int lua_delfile (void)
{
 lua_nfile--; 
 return 1;
}

/*
** Return the last file name set.
*/
char *lua_filename (void)
{
 return lua_file[lua_nfile-1];
}

루아 파일을 하나씩 읽을 때마다 lua_file 배열 변수에 파일 이름을 추가합니다. 루아 코드 파싱이 끝나면 lua_delfile을 호출합니다. lua_filename() 함수는 에러 상황에서 에러 메시지를 출력할 때 마지막으로 로딩한 파일 이름을 받을 때 사용합니다.

댓글남기기