[루아2.1] opcode.c (1)

5 분 소요

Opcode.h

Opcode.h에는 루아 VM 명령어 종류와 VM에서 쓰는 자료형 선언이 있습니다. 해당 자료형을 실제로 쓰는 코드를 읽기 전까지 자료형을 정확히 어떤 용도로 쓰는지 알 수 없기 때문에 Opcode.c 파일을 읽기 전에 미리 연습하는 느낌으로 빠르게 읽고 지나가겠습니다.

#ifndef STACKGAP
#define STACKGAP	128
#endif 

#ifndef real
#define real float
#endif

#define FIELDS_PER_FLUSH 40

#define MAX_TEMPS	20

루아 구현 코드 스타일을 보니 xxxGAP이라고 이름 붙은 상수는 한 번에 메모리를 할당하는 단위입니다. STACKGAP이 128이니 VM 스택은 한 번에 128개씩 새로 할당받는 것같습니다. 나중에 코드를 읽으면서 확인해 보겠습니다. 루아의 number 타입은 루아 구현에서 float으로 일괄 처리됩니다. 이 이름을 real로 부르고 싶었나 봅니다. FIELDS_PER_FLUSH는 flush_list() 함수를 호출할 때 숫자 단위를 계산할 때 쓰는 상수입니다. Lua.stx 파일을 읽을 때 한 번 언급했습니다. MAX_TEMPS는 스택 크기를 확인 할 때 사용합니다. 어떻게 쓰는지는 lua_execute() 함수를 읽을 때 읽어보겠습니다.

그리고 이어서는 긴 열거형 선언이 나옵니다. 루아 opcode를 그냥 열거형으로 선언했습니다. 루아는 opcode에 특정한 값을 할당한게 아니라 그냥 열거형으로 선언해서 사용합니다.

#define MULT_RET	255

숫자에 특별한 의미가 있는 것이 아니라, 그냥 쓰지 않는 숫자를 쓰고, 함수가 리턴 값을 여러 개 리턴하는 것을 표시하는 용도로 씁니다. 숫자보다는 이름에 더 큰 의미가 있는 상수 선언입니다.

typedef void (*Cfunction) (void);
typedef int  (*Input) (void);

루아에서 호스트 프로그램의 C 언어 함수를 핸들링하기위해 Cfunction이라는 함수 포인터 타입을 만들어 놓고 씁니다. Input은 루아 소스를 파일에서 받아 읽는지 문자열에서 한 글자씩 읽는지에 상관없이 낱말 분석에서 한 글자를 받아 오게끔 동일한 인터페이스를 제공하는 인터페이스입니다.

typedef union
{
 Cfunction     f;
 real          n;
 TaggedString *ts;
 Byte         *b;
 struct Hash    *a;
 void           *u;
} Value;

typedef struct Object
{
 lua_Type  tag;
 Value value;
} Object;

Object는 루아가 데이터를 다루는 최소 단위입니다. 루아에서 핸들링하는 모든 데이터는 내부적으로 Object로 변환되어 처리됩니다. 그래서 Value 타입 공용체 선언을 보면 루아에서 처리하는 모든 타입이 다 있습니다. 상황과 필요에 따라 공용체 변수 이름을 따로 쓰는 것이지요.

typedef struct
{
 Object  object;
} Symbol;

루아 인터프리터 내부에서 사용하는 심볼 테이블에서 사용하는 심볼에 대한 자료 구조 선언인데, 그냥 Object 타입 그대로 입니다. 이렇게 선언할 것이면 typedef를 왜 안 썼는지 모르겠군요. 코드를 다 다듬지 않은 것같습니다.

/* Macros to access structure members */
#define tag(o)		((o)->tag)
#define nvalue(o)	((o)->value.n)
#define svalue(o)	((o)->value.ts->str)
#define tsvalue(o)	((o)->value.ts)
#define bvalue(o)	((o)->value.b)
#define avalue(o)	((o)->value.a)
#define fvalue(o)	((o)->value.f)
#define uvalue(o)	((o)->value.u)

/* Macros to access symbol table */
#define s_object(i)	(lua_table[i].object)
#define s_tag(i)	(tag(&s_object(i)))
#define s_nvalue(i)	(nvalue(&s_object(i)))
#define s_svalue(i)	(svalue(&s_object(i)))
#define s_bvalue(i)	(bvalue(&s_object(i)))
#define s_avalue(i)	(avalue(&s_object(i)))
#define s_fvalue(i)	(fvalue(&s_object(i)))
#define s_uvalue(i)	(uvalue(&s_object(i)))

바로 위의 Value 공용체 접근할 때 타이핑을 적게 하려고 만든 매크로들입니다.

typedef union
{
 struct {char c1; char c2;} m;
 Word w;
} CodeWord;
#define get_word(code,pc)    {code.m.c1 = *pc++; code.m.c2 = *pc++;}

typedef union
{
 struct {char c1; char c2; char c3; char c4;} m;
 float f;
} CodeFloat;
#define get_float(code,pc)   {code.m.c1 = *pc++; code.m.c2 = *pc++;\
                              code.m.c3 = *pc++; code.m.c4 = *pc++;}

typedef union
{
 struct {char c1; char c2; char c3; char c4;} m;
 Byte *b;
} CodeCode;
#define get_code(code,pc)    {code.m.c1 = *pc++; code.m.c2 = *pc++;\
                              code.m.c3 = *pc++; code.m.c4 = *pc++;}

루아 바이트 코드 버퍼에 1 바이트 크기가 넘는 데이터를 넣을 때, 반복적 코드를 줄이려고 만든 공용체와 매크로입니다.

Opcode.c

루아 1.1에서는 Lua.stx 파일이 제일 길고 Opcode.c 파일이 두 번째로 길었는데 루아 2.1에서는 Opcode.c 파일이 제일 깁니다. 최대한 대충 빨리 읽어 보겠습니다. 반복해서 읽다 보니 대충 읽어도 큰 그림은 그려지는 것같습니다. 바라건데, 대충 읽어서 두 번 정도에 읽을 수 있었음 좋겠군요.

#define tonumber(o) ((tag(o) != LUA_T_NUMBER) && (lua_tonumber(o) != 0))
#define tostring(o) ((tag(o) != LUA_T_STRING) && (lua_tostring(o) != 0))

루아는 숫자와 문자열을 섞어서 연산해도 문자열을 알아서 숫자로 바꿔서 계산을 해 줍니다. 마찬가지로 문자열과 숫자를 섞어서 문자열 관련 연산을 해도 알아서 숫자를 문자열로 바꿔서 잘 처리합니다. 저는 개인적으로 이런 것을 꽤 싫어하지만, 동적 타이핑 언어의 주된 장점으로 꼽는 기능이기도 합니다. 이렇게 알아서 데이터 타입을 바꿔주는 기능을 구현하는 매크로입니다. 루아는 number를 string으로 string을 number로 바꾸는 두 동작만 지원합니다.

#define STACK_BUFFER (STACKGAP+128)

typedef int StkId;  /* index to stack elements */

static Long    maxstack = 0L;
static Object *stack = NULL;
static Object *top = NULL;

STACK_BUFFER는 루아 VM 스택의 초기 크기입니다. STACKGAP이 128이니까 256이겠네요. 나머지 변수는 스택을 제어하는 변수들입니다.

/* macros to convert from lua_Object to (Object *) and back */
 
#define Address(lo)     ((lo)+stack-1)
#define Ref(st)         ((st)-stack+1)

주석을 보면 루아 Object 타입 값을 Object 포인터로 바꾸는 매크로라고 하는데 아직 의미 파악은 안됩니다. 파라메터인 lo와 st는 스택 인덱스 단위로 숫자여야 위 코드가 성립하긴 할 것같습니다.

static StkId CBase = 0;  /* when Lua calls C or C calls Lua, points to */
                          /* the first slot after the last parameter. */
static int CnResults = 0; /* when Lua calls C, has the number of parameters; */
                         /* when C calls Lua, has the number of results. */

static  jmp_buf *errorJmp = NULL; /* current error recover point */

루아가 C 함수를 호출하거나, C에서 루아 함수를 호출할 때, 스택을 조정하고 리턴 값을 처리하려고 선언한 변수입니다.

Object *luaI_Address (lua_Object o)
{
  return Address(o);
}


/*
** Error messages
*/

static void lua_message (char *s)
{
  lua_pushstring(s);
  do_call(&luaI_fallBacks[FB_ERROR].function, (top-stack)-1, 0, (top-stack)-1);
}

/*
** Reports an error, and jumps up to the available recover label
*/
void lua_error (char *s)
{
  if (s) lua_message(s);
  if (errorJmp)
    longjmp(*errorJmp, 1);
  else
  {
    fprintf (stderr, "lua: exit(1). Unable to recover\n");
    exit(1);
  }
}

루아 VM 동작에 참여하는 함수는 아니고 Address() 매크로를 호출해서 리턴 타입만 명확히 해주는 랩퍼(wrapper) 함수와 에러 메시지를 출력하는 함수 그리고 에러 핸들링을 하는 함수 입니다.

/*
** Init stack
*/
static void lua_initstack (void)
{
 maxstack = STACK_BUFFER;
 stack = newvector(maxstack, Object);
 top = stack;
}


/*
** Check stack overflow and, if necessary, realloc vector
*/
#define lua_checkstack(n)  if ((Long)(n) > maxstack) checkstack(n)

static void checkstack (StkId n)
{
 StkId t;
 if (stack == NULL)
   lua_initstack();
 if (maxstack >= MAX_INT)
   lua_error("stack size overflow");
 t = top-stack;
 maxstack *= 2;
 if (maxstack >= MAX_INT)
   maxstack = MAX_INT;
 stack = growvector(stack, maxstack, Object);
 top = stack + t;
}

lua_initstack() 함수는 최초에 한 번 실행되어 stack 변수에 메모리 포인터를 할당합니다. newvector() 함수로 메모리를 할당하는데요. 이 메모리 공간이 VM이 사용할 스택입니다. checkstack() 함수는 lua_checkstack() 매크로를 통해서 호출됩니다. 스택에 데이터를 저장하는 push 계열 명령어를 처리할 때 lua_checkstack() 매크로로 스택 오버플로우를 검사합니다. 스택 오버플로우는 아니지만 스택이 부족하면 growvector() 함수를 호출해서 스택을 늘립니다.

Opcode.c 파일에 이후로 많은 함수가 더 남아 있습니다. 이 함수들은 모두 루아 바이트 코드 명령어를 처리하는 함수입니다. 그래서 조금 다른 접근 방법으로 읽어 보겠습니다. 함수를 먼저 읽는것이 아니라 VM의 엔트리 포인트 격인 lua_execute() 함수를 먼저 읽으면서 호출하는 함수를 찾아서 따라 읽어가는 방식으로 해보겠습니다. 함수를 먼저 읽고 lua_execute() 함수를 읽으니 좀 지루해서요.

댓글남기기