[루아2.1] opcode.c (2)

13 분 소요

Opcode.c

루아 2.1을 릴리즈하면서 기능을 추가해서 그런지 Opcode.c 파일 내용이 더 많아 졌습니다. 그래서 읽는데 살짝 버거움이 느껴져, 읽는 방식을 바꿔보기로 했습니다. 루아 VM의 본체격인 lua_execute() 함수를 먼저 읽으면서 lua_execute() 함수에서 호출하는 함수를 따라 가는 방식으로 읽어보겠습니다.

static StkId lua_execute (Byte *pc, StkId base)
{
 lua_checkstack(STACKGAP+MAX_TEMPS+base);

lua_execute() 함수는 lua_checkstack() 함수를 호출하면서 시작합니다. 이 함수는 스택을 검사하는 역할도 하고, 스택을 새로 할당하거나 늘이는 일도합니다. 그래서 lua_execute()를 시작하자마자 호출하면 아마 스택을 새로 할당할 것입니다.

 while (1)
 {
  OpCode opcode;
  switch (opcode = (OpCode)*pc++)
  {
   case PUSHNIL: tag(top++) = LUA_T_NIL; break;

while 루프를 한 바퀴 돌아서 switch 구문에 오면 그 자리에는 정확하게 opcode가 있어야 합니다. case 구문에서 명령어를 처리하면서 약속된 개수만큼 파라메터 데이터를 처리하고 pc 위치를 opcode 위치에 맞게 갖다 놔야합니다.

   case PUSH0: case PUSH1: case PUSH2:
     tag(top) = LUA_T_NUMBER;
     nvalue(top++) = opcode-PUSH0;
     break;

스택에 0, 1, 2를 넣는 (저장하는) 명령어입니다. 자주 사용하는 숫자인 0, 1, 2를 PUSHBYTE로 처리하면 바이트 코드 2개 (2바이트)가 필요한데, 이렇게 개별 명령어로 만들면 1바이트로 가능하기 때문에 만든 명령어 세 개입니다. 현재 스택 꼭대기의 루아 오브젝트 타입 태그를 number로 설정합니다. 이러면 스택 꼭대기 오브젝트는 루아 number 타입이 됩니다. 그리고 스택 꼭대기에 0, 1, 2 중 하나를 넣고 스택 꼭대기 포인터를 늘립니다.

   case PUSHBYTE: tag(top) = LUA_T_NUMBER; nvalue(top++) = *pc++; break;

0, 1, 2가 아닌 1바이트 값을 스택에 넣는 함수입니다. 스택 꼭대기 루아 오브젝트를 루아 number로 설정하고 바이트 코드 버퍼에서 값을 읽어서 스택에 넣습니다.

   case PUSHWORD:
   {
    CodeWord code;
    get_word(code,pc);
    tag(top) = LUA_T_NUMBER; nvalue(top++) = code.w;
   }
   break;

   case PUSHFLOAT:
   {
    CodeFloat code;
    get_float(code,pc);
    tag(top) = LUA_T_NUMBER; nvalue(top++) = code.f;
   }
   break;

PUSHWORD와 PUSHFLOAT도 기본적으로 PUSHBYTE와 같은 동작을 하는 명령어입니다. PUSHWORD는 바이트 코드 버퍼에서 2바이트를 읽고 PUSHFLOAT는 바이트 코드 버퍼에서 4바이트를 읽습니다.

   case PUSHSTRING:
   {
    CodeWord code;
    get_word(code,pc);
    tag(top) = LUA_T_STRING; tsvalue(top++) = lua_constant[code.w];
   }
   break;

lua_constant는 Tree.h에 아래 코드로 선언되 있습니다.

TaggedString **lua_constant;
static Word lua_nconstant = 0;
static Long lua_maxconstant = 0;

TaggedString 타입 포인터입니다. 그러므로 lua_constant 변수로 문자열 포인터를 관리하는 것으로 보입니다. 바이트 코드 버퍼에서 2 바이트를 읽어 lua_constant 배열 인덱스로 사용해서 문자열 포인터를 스택 꼭대기에 넣었으므로 바이트 코드 버퍼에는 lua_constant의 인덱스 값을 넣어 놓는가보네요.

   case PUSHFUNCTION:
   {
    CodeCode code;
    get_code(code,pc);
    tag(top) = LUA_T_FUNCTION; bvalue(top++) = code.b;
   }
   break;

바이트 코드 버퍼에는 함수 포인터를 직접 저장해 놓습니다. 그러므로 그 값을 그대로 읽어서 스택에 넣습니다. 따라서 동작 자체는 4바이트를 바이트 코드 버퍼에서 읽어서 스택에 넣는 것과 동일합니다.

   case PUSHLOCAL0: case PUSHLOCAL1: case PUSHLOCAL2:
   case PUSHLOCAL3: case PUSHLOCAL4: case PUSHLOCAL5:
   case PUSHLOCAL6: case PUSHLOCAL7: case PUSHLOCAL8:
   case PUSHLOCAL9: *top++ = *((stack+base) + (int)(opcode-PUSHLOCAL0)); break;

   case PUSHLOCAL: *top++ = *((stack+base) + (*pc++)); break;

같은 코드를 두 번 읽어도 base가 의미하는 것을 파악할 수가 없네요. 느낌으로 가정해보면 함수 스택의 시작 위치라고 봤을 때, 함수 스택 시작 위치에서 오프셋을 계산해 값을 읽고 이 값을 다시 스택 꼭대기에 넣습니다. 명령어 이름을 보면 로컬 변수값을 읽는 명령어인데 이미 스택에 있는 값을 왜 또 스택에 다시 넣는지 모르겠습니다. 레지스터가 없는 구조라서 그런건가…

   case PUSHGLOBAL:
   {
    CodeWord code;
    get_word(code,pc);
    *top++ = s_object(code.w);
   }
   break;

로컬 변수에 비해 전역 변수 핸들링은 이해가 쉽습니다. 바이트 코드 버퍼에서 전역 변수 테이블 인덱스 값을 읽습니다. 그리고 전역 변수 테이블에서 실제 전역 변수 인스턴스 포인터를 읽어서 스택에 넣습니다.

   case PUSHINDEXED:
    pushsubscript();
    break;

바로 pushsubscript() 함수 내용을 보겠습니다.

/*
** Function to index a table. Receives the table at top-2 and the index
** at top-1.
*/
static void pushsubscript (void)
{
  if (tag(top-2) != LUA_T_ARRAY)
    do_call(&luaI_fallBacks[FB_GETTABLE].function, (top-stack)-2, 1, (top-stack)-2);
  else 
  {
    Object *h = lua_hashget(avalue(top-2), top-1);
    if (h == NULL || tag(h) == LUA_T_NIL)
      do_call(&luaI_fallBacks[FB_INDEX].function, (top-stack)-2, 1, (top-stack)-2);
    else
    {
      --top;
      *(top-1) = *h;
    }
  }
}

fallback이라는게 대체 뭘까요? 무슨 용도로 fallback이라는 용어를 가져다 썼는지 모르겠습니다. 일단 (top-2) 위치에서 루아 오브젝트의 타입을 읽습니다. 그 것이 루아 array 타입일 때는 luaI_fallBacks에서 FB_GETTABLE 인덱스에 함수를 읽어다가 do_call() 함수에 던집니다.

struct FB  luaI_fallBacks[] = {
{"error", {LUA_T_CFUNCTION, errorFB}},
{"index", {LUA_T_CFUNCTION, indexFB}},
{"gettable", {LUA_T_CFUNCTION, gettableFB}},
{"arith", {LUA_T_CFUNCTION, arithFB}},
{"order", {LUA_T_CFUNCTION, orderFB}},
{"concat", {LUA_T_CFUNCTION, concatFB}},
{"settable", {LUA_T_CFUNCTION, gettableFB}},
{"gc", {LUA_T_CFUNCTION, GDFB}},
{"function", {LUA_T_CFUNCTION, funcFB}}
};

luaI_fallBacks 배열은 위 코드처럼 생겼습니다. 그냥 보기에는 그냥 함수 포인터 배열처럼 보입니다. FB_GETTABLE은 2로 “gettable”에 해당하는 오프셋입니다. 그러면 gettableFB() 함수를 호출합니다.

static void gettableFB (void)
{
  lua_reportbug("indexed expression not a table");
}

내용이 아무것도 없네요. 음.. 그럼 정말 fallback의 사전적 정의처럼 어디선가 재정의한 함수 포인터를 쓰는건가 하는 생각이 듭니다.

void luaI_setfallback (void)
{
  int i;
  char *name = lua_getstring(lua_getparam(1));
  lua_Object func = lua_getparam(2);
  if (name == NULL || !(lua_isfunction(func) || lua_iscfunction(func)))
  {
    lua_pushnil();
    return;
  }
  for (i=0; i<N_FB; i++)
  {
    if (strcmp(luaI_fallBacks[i].kind, name) == 0)
    {
      luaI_pushobject(&luaI_fallBacks[i].function);
      luaI_fallBacks[i].function = *luaI_Address(func);
      return;
    }
  }
  /* name not found */
  lua_pushnil();
}

분명 luaI_fallBacks의 함수 포인터를 재정의하는 함수가 있긴합니다. 그런데 이걸 어떻게 쓰는지에 대한 정보는 소스 코드 안에서 찾을 수 없었습니다. fallback이 뭔지는 계속 코드를 읽다보면 알게 되겠지요.

   case STOREINDEXED:
   {
    int n = *pc++;
    if (tag(top-3-n) != LUA_T_ARRAY)
    {
      *(top+1) = *(top-1);
      *(top) = *(top-2-n);
      *(top-1) = *(top-3-n);
      top += 2;
      do_call(&luaI_fallBacks[FB_SETTABLE].function, (top-stack)-3, 0, (top-stack)-3);
    }
    else
    {
     Object *h = lua_hashdefine (avalue(top-3-n), top-2-n);
     *h = *(top-1);
     top--;
    }
   }
   break;

이 명령어를 이해하려면 루아에서 indexed라는 용어를 어떻게 쓰는지 먼저 파악해야할 것같습니다. 그러나 아직 모르겠네요. 그래서 일단 이름에서 얻을 수 있는 힌트 없이 코드를 읽겠습니다. 우선 짧은 쪽을 먼저 보겠습니다. else 구문이 앞의 if 구문보다 짧습니다. n은 바이트 코드 버퍼에서 받아오는 값입니다. 저는 n이 리스트나 배열등의 아이템 개수라고 추정하고 있습니다. 그런데 3을 추가로 왜 빼는 건지는 아직 모르겠습니다. lua_hashdefine() 함수는 루아 구현 내부에서 유지하는 어떤 해시 테이블에서 아이템을 검색해서 있으면 리턴하고 없으면 추가하는 함수입니다. 위 코드만으로 추정컨데, 아이템 개수 n개 만큼 스택을 거슬러간다음 나오는 값이 해시 테이블 레퍼런스이고 그 아래에 저장되어 있는 값이 해시 테이블 자체의 포인터입니다. 그런데 이 데이터는 어디서 미리 넣어 두는건지 힌트가 없습니다. 긴 쪽인 if 구문을 읽겠습니다. 이쪽으로는 확인하고자 하는 스택의 값이 배열이 아닐 때 진입합니다. 코드 자체는 매우 단순한 코드지만 의미를 알 수 없습니다. fallback은 또 나오는 군요.

   case STORELIST0:
   case STORELIST:
   {
    int m, n;
    Object *arr;
    if (opcode == STORELIST0) m = 0;
    else m = *(pc++) * FIELDS_PER_FLUSH;
    n = *(pc++);
    arr = top-n-1;
    while (n)
    {
     tag(top) = LUA_T_NUMBER; nvalue(top) = n+m;
     *(lua_hashdefine (avalue(arr), top)) = *(top-1);
     top--;
     n--;
    }
   }
   break;

STORELIST 명령어는 명령어 다음에 파라메터가 두 개인 것으로 보입니다. 첫 번째 파라메터가 m이고 두 번째 파라메터가 n입니다. 스택 꼭대기에서 n 만큼 내려간 위치에 루아 배열 포인터가 있나봅니다. 그리고 그 위로 n개 아이템이 배열 인덱스와 값입니다. 그럼 아이템 한 개당 스택을 두 칸씩 먹어야 할텐데요. 스택을 역행하면서 값과 인덱스를 읽어서 루아 배열에 입력합니다.

   case STORERECORD:
   {
    int n = *(pc++);
    Object *arr = top-n-1;
    while (n)
    {
     CodeWord code;
     get_word(code,pc);
     tag(top) = LUA_T_STRING; tsvalue(top) = lua_constant[code.w];
     *(lua_hashdefine (avalue(arr), top)) = *(top-1);
     top--;
     n--;
    }
   }
   break;

STORERECORD는 STORELIST와 비슷한 패턴입니다. n이 아마 레코드의 크기로 보입니다. 스택 n 칸에는 데이터가 있을 것으로 추정됩니다. 그 아래에는 루아 배열 포인터가 있고요. 그리고 바이트 코드 버퍼에는 레코드의 인덱스 키인 문자열 정보(lua_constant 배열의 인덱스)가 있습니다. 그래서 바이트 코드 버퍼에서 lua_constant 배열의 인덱스 번호를 읽어서 문자열을 스택 꼭대기에 넣습니다. 이 스택 꼭대기 오브젝트를 그대로 해시 테이블 검색 함수인 lua_hashdefine() 함수에 인덱스로 던지면 값에 해당하는 오브젝트 포인터가 리턴되고 그 오브젝트 포인터에 아래칸에 있는 값을 넣습니다. 그리고 스택을 하나 줄이면 다음 루프를 도는 것이지요.

   case ADJUST0:
     adjust_top(base);
     break;

   case ADJUST:
     adjust_top(base + *(pc++));
     break;

ADJUST 명령어는 스택 꼭대기 포인터 위치를 바꾸는 명령어입니다. 구현인 adjust_top() 함수를 읽겠습니다.

/*
** Adjust stack. Set top to the given value, pushing NILs if needed.
*/
static void adjust_top (StkId newtop)
{
  Object *nt = stack+newtop;
  while (top < nt) tag(top++) = LUA_T_NIL;
  top = nt;  /* top could be bigger than newtop */
}

간단한 코드입니다. 파라메터인 newtop 위치로 top 변수를 바꿉니다. newtop은 음수도 가능하므로 스택 꼭대기를 높이거나 낮출 수 있습니다. 높일 때는 새로 생기는 스택 칸을 nil로 초기화합니다.

   case CREATEARRAY:
   {
    CodeWord size;
    get_word(size,pc);
    avalue(top) = lua_createarray(size.w);
    tag(top) = LUA_T_ARRAY;
    top++;
   }
   break;

이름을 보면 루아 배열을 만드는 명령어입니다. 배열 크기는 바이트 코드 버퍼에서 읽어옵니다. lua_createarray() 함수를 호출해서 루아 배열 포인터를 받아오면 그 포인터를 스택 꼭대기에 저장하고 스택 포인터 꼭대기를 하나 늘립니다.

   case EQOP:
   {
    int res = lua_equalObj(top-2, top-1);
    --top;
    tag(top-1) = res ? LUA_T_NUMBER : LUA_T_NIL;
    nvalue(top-1) = 1;
   }
   break;

같다(==) 연산을 처리하는 명령어입니다. lua_equalObj() 함수에 스택 꼭대기와 그 아래 값 두 개를 전달해서 비교합니다. 비교 결과가 true면 스택 꼭대기에 1을 넣습니다. 아니면 nil을 넣습니다. lua_equalObj() 함수는 Hash.c 파일에 있습니다. 나중에 Hash.c 파일을 읽을 때 보겠습니다.

    case LTOP:
      comparison(LUA_T_NUMBER, LUA_T_NIL, LUA_T_NIL, "lt");
      break;

   case LEOP:
      comparison(LUA_T_NUMBER, LUA_T_NUMBER, LUA_T_NIL, "le");
      break;

   case GTOP:
      comparison(LUA_T_NIL, LUA_T_NIL, LUA_T_NUMBER, "gt");
      break;

   case GEOP:
      comparison(LUA_T_NIL, LUA_T_NUMBER, LUA_T_NUMBER, "ge");
      break;

같은 패턴으로 작성한 코드입니다. 각각 작다, 작거나 같다, 크다, 크거나 같다를 처리합니다. 모두 comparison() 함수 하나로 처리합니다. 그러면 comparison() 함수의 코드를 보겠습니다.

static void comparison (lua_Type tag_less, lua_Type tag_equal, 
                        lua_Type tag_great, char *op)
{
  Object *l = top-2;
  Object *r = top-1;
  int result;
  if (tag(l) == LUA_T_NUMBER && tag(r) == LUA_T_NUMBER)
    result = (nvalue(l) < nvalue(r)) ? -1 : (nvalue(l) == nvalue(r)) ? 0 : 1;
  else if (tostring(l) || tostring(r))
  {
    lua_pushstring(op);
    do_call(&luaI_fallBacks[FB_ORDER].function, (top-stack)-3, 1, (top-stack)-3);
    return;
  }
  else
    result = strcmp(svalue(l), svalue(r));
  top--;
  nvalue(top-1) = 1;
  tag(top-1) = (result < 0) ? tag_less : (result == 0) ? tag_equal : tag_great;
}

파라메터 이름만 봐도 어떤 동작을 할지 감이 옵니다. 각각 작을 때, 같을 때, 클 때에 대한 리턴 값을 파라메터로 미리 주고 값을 비교하는 작업을 합니다. 코드가 복잡한 것 같은데 결론은 l이 r보다 작으면 result에 -1을 넣고 같으면 0, 크면 1을 넣는 것입니다. 그리고 마지막에 스택 꼭대기에 result에 따라서 파라메터를 선택에서 넣습니다.

   case ADDOP:
   {
    Object *l = top-2;
    Object *r = top-1;
    if (tonumber(r) || tonumber(l))
      call_arith("add");
    else
    {
      nvalue(l) += nvalue(r);
      --top;
    }
   }
   break;

   case SUBOP:
   {
    Object *l = top-2;
    Object *r = top-1;
    if (tonumber(r) || tonumber(l))
      call_arith("sub");
    else
    {
      nvalue(l) -= nvalue(r);
      --top;
    }
   }
   break;

   case MULTOP:
   {
    Object *l = top-2;
    Object *r = top-1;
    if (tonumber(r) || tonumber(l))
      call_arith("mul");
    else
    {
      nvalue(l) *= nvalue(r);
      --top;
    }
   }
   break;

   case DIVOP:
   {
    Object *l = top-2;
    Object *r = top-1;
    if (tonumber(r) || tonumber(l))
      call_arith("div");
    else
    {
      nvalue(l) /= nvalue(r);
      --top;
    }
   }
   break;

사칙연산을 처리하는 명령어입니다. tonumber() 매크로가 참이면 call_arith() 함수에 연산자를 문자열로 보내서 처리합니다. 함수 내용을 보면,

static void call_arith (char *op)
{
  lua_pushstring(op);
  do_call(&luaI_fallBacks[FB_ARITH].function, (top-stack)-3, 1, (top-stack)-3);
}

또 fallBack이 나왔네요. 함수 포인터를 모아놓은 건데 함수 본체 내용은 찾을 수 없으니, 대체 어떻게 동작하는 것인지 알 수가 없습니다.

   case POWOP:
    call_arith("pow");
    break;

제곱 연산입니다. 앞에서 봤던 call_arith() 함수에 “pow”라고 문자열로 연산자 종류를 전달하는 것으로 구현이 끝입니다. 그러면 fallBack이 또 호출되어서 처리됩니다. 루아 2.1에서 관건은 fallBack이군요. 루아 3.0까지 fallBack이 계속 살아 있으면 진지하게 찾아봐야겠습니다.

   case CONCOP:
   {
    Object *l = top-2;
    Object *r = top-1;
    if (tostring(r) || tostring(l))
      do_call(&luaI_fallBacks[FB_CONCAT].function, (top-stack)-2, 1, (top-stack)-2);
    else
    {
      tsvalue(l) = lua_createstring (lua_strconc(svalue(l),svalue(r)));
      --top;
    }
   }
   break;

문자열 두 개를 합쳐서 하나로 만드는 연산자를 처리하는 명령어입니다. 이제 fallback은 무시하겠습니다. 어차피 봐도 이해 못하는것 계속 언급할 필요 없지요. lua_createstring() 함수는 문자열을 새로 만드는 함수입니다. Tree.c에 구현이 있으므로 Tree.c를 읽을 때 보겠습니다. lua_strconc() 함수가 문자열 두 개를 합치는 함수입니다.

/*
** Concatenate two given strings. Return the new string pointer.
*/
static char *lua_strconc (char *l, char *r)
{
 static char *buffer = NULL;
 static int buffer_size = 0;
 int nl = strlen(l);
 int n = nl+strlen(r)+1;
 if (n > buffer_size)
  {
   buffer_size = n;
   if (buffer != NULL)
     luaI_free(buffer);
   buffer = newvector(buffer_size, char);
  }
  strcpy(buffer,l);
  strcpy(buffer+nl, r);
  return buffer;
}

buffer 정적 변수에 메모리를 할당하고 이 buffer에 첫 번째 문자열을 복사합니다. 그리고 첫 번째 문자열 길이만큼 포인터를 옮기고 두 번째 문자열을 복사합니다. 그러면 결과적으로 buffer 변수에 문자열 두 개가 하나로 합쳐져 저장됩니다.

   case MINUSOP:
    if (tonumber(top-1))
    {
      tag(top++) = LUA_T_NIL;
      call_arith("unm");
    }
    else
      nvalue(top-1) = - nvalue(top-1);
   break;

변수나 숫자에 마이너스(-)를 앞에 붙이면 그 값에 역수를 취하게 됩니다. 그 동작을 처리하는 명령어가 MINUSOP 명령어입니다. 구현은 간단합니다. 스택 꼭대기 값에 마이너스를 붙여서 역수를 만든 다음 다시 스택 꼭대기에 넣습니다.

   case NOTOP:
    tag(top-1) = (tag(top-1) == LUA_T_NIL) ? LUA_T_NUMBER : LUA_T_NIL;
    nvalue(top-1) = 1;
   break;

루아에서 거짓(false)는 nil입니다. nil이 아니면 0도 참(true)입니다. 저는 이 특징이 참 마음에 안들지만 루아 개발자들은 좋았나 봅니다. 아무튼 그래서 참, 거짓을 반전하는 이 연산자 구현에서도 스택 꼭대기 값의 타입이 nil인지만 봅니다. 그래서 nil이면 number로 바꾸고 nil이 아니면 nil로 바꿉니다.

   case ONTJMP:
   {
    CodeWord code;
    get_word(code,pc);
    if (tag(top-1) != LUA_T_NIL) pc += code.w;
   }
   break;

스택 꼭대기가 nil이 아니면 (참이면) 바이트 코드 버퍼의 포인터인 pc를 움직입니다.

   case ONFJMP:	
   {
    CodeWord code;
    get_word(code,pc);
    if (tag(top-1) == LUA_T_NIL) pc += code.w;
   }
   break;

스택 꼭대기가 nil이면 (거짓이면) 바이트 코드 버퍼의 포인터인 pc를 움직입니다.

   case JMP:
   {
    CodeWord code;
    get_word(code,pc);
    pc += code.w;
   }
   break;

무조건 점프합니다.

   case UPJMP:
   {
    CodeWord code;
    get_word(code,pc);
    pc -= code.w;
   }
   break;

pc에서 word 타입 값을 뺍니다. 그래서 이름이 UPJMP입니다. 루아의 word 타입이 unsigned int이기 때문에 음수로 JMP 명령을 수행할 수 없어서 만든 명령으로 보입니다.

   case IFFJMP:
   {
    CodeWord code;
    get_word(code,pc);
    top--;
    if (tag(top) == LUA_T_NIL) pc += code.w;
   }
   break;

if 문 때문에 스택 꼭대기에 뭔가 불필요한 값이 있는가 봅니다. 스택 꼭대기를 그냥 한 칸 줄이고, 줄어든 스택 꼭대기의 값이 거짓일 때 점프합니다.

   case IFFUPJMP:
   {
    CodeWord code;
    get_word(code,pc);
    top--;
    if (tag(top) == LUA_T_NIL) pc -= code.w;
   }
   break;

마찬가지로 스택 꼭대기를 줄이고 줄어든 스택 꼭대기가 거짓일 때 역방향으로 점프합니다.

   case POP: --top; break;

그냥 스택 꼭대기를 하나 줄입니다.

   case CALLFUNC:
   {
     int nParams = *(pc++);
     int nResults = *(pc++);
     Object *func = top-1-nParams; /* function is below parameters */
     StkId newBase = (top-stack)-nParams;
     do_call(func, newBase, nResults, newBase-1);
   }
   break;

함수를 호출하는 명령어입니다. 바이트 코드 버퍼에는 파라메터 개수, 리턴값 개수가 저장되어 있습니다. 그리고 순서대로 파라메터 값이 바이트 코드 버퍼에 있습니다. 파라메터 값 다음에 함수 포인터가 있습니다. 함수에게 전달할 스택 메모리 위치를 계산합니다. 스택 꼭대기에서 파라메터 개수 만큼 인덱스를 빼는 것인데 수식에서 stack 변수값은 왜 빼는지 모르겠습니다. 마지막으로 함수 호출은 do_call() 함수를 호출해서 루아 함수를 실행합니다.

/*
** Call a function (C or Lua). The parameters must be on the stack,
** between [stack+base,top). When returns, the results are on the stack,
** between [stack+whereRes,top). The number of results is nResults, unless
** nResults=MULT_RET.
*/
static void do_call (Object *func, StkId base, int nResults, StkId whereRes)
{
  StkId firstResult;
  if (tag(func) == LUA_T_CFUNCTION)
    firstResult = callC(fvalue(func), base);
  else if (tag(func) == LUA_T_FUNCTION)
    firstResult = lua_execute(bvalue(func), base);
  else
  { /* func is not a function */
    call_funcFB(func, base, nResults, whereRes);
    return;
  }
  /* adjust the number of results */
  if (nResults != MULT_RET && top - (stack+firstResult) != nResults)
    adjust_top(firstResult+nResults);
  /* move results to the given position */
  if (firstResult != whereRes)
  {
    int i;
    nResults = top - (stack+firstResult);  /* actual number of results */
    for (i=0; i<nResults; i++)
      *(stack+whereRes+i) = *(stack+firstResult+i);
    top -= firstResult-whereRes;
  }
}

함수 오브젝트의 타입이 cfunction이면 callC() 함수로 처리합니다. 그냥 function 즉, 루아 함수이면 lua_execute() 함수로 처리합니다. 둘다 아니면 사실 루아에서는 에러여야 하는데 또 fallback 관련 함수인 call_funcFB() 함수를 호출합니다. fallback에 대해서 이해하는 것은 일단 보류이니 그냥 함수 구현 자체만 읽어 보겠습니다. 그렇게 함수 호출과 처리를 끝내고 나면 리턴 값을 스택에서 정리하는 코드가 이어서 나옵니다. 루아는 리턴을 여러개 한 번에 할 수 있기 때문에 함수 호출을 하고 스택 꼭대기 위치를 조정하려고 adjust_top() 함수를 호출합니다.

/*
** Call a C function. CBase will point to the top of the stack,
** and CnResults is the number of parameters. Returns an index
** to the first result from C.
*/
static StkId callC (lua_CFunction func, StkId base)
{
  StkId oldBase = CBase;
  int oldCnResults = CnResults;
  StkId firstResult;
  CnResults = (top-stack) - base;
  /* incorporate parameters on the stack */
  CBase = base+CnResults;
  (*func)();
  firstResult = CBase;
  CBase = oldBase;
  CnResults = oldCnResults;
  return firstResult;
}

C 함수를 루아 스크립트에서 호출할 수 있게 하는 함수입니다. C 함수 전용으로 스택 베이스 포인터를 따로 관리합니다. C 언어 함수도 루아에서 파라메터를 받고 루아로 리턴값을 전달하려면 루아 API를 사용해야 합니다. 파라메터와 리턴값 전달 관련 루아 API에서 CBase, CnResults 변수를 사용하고 값을 변경합니다. 그래서 C 언어 함수를 호출하고 나면 CBase, CnResults 변수 값이 변경됩니다. 이 값을 기준으로 C 언어 함수의 리턴 값을 처리해서 루아 스택을 정리하는데 사용합니다.

   case RETCODE0:
     return base;

   case RETCODE:
     return base+*pc;

일반적인 함수 스택을 생각해보면 함수 동작이 다 끝나면 함수에서 사용하던 스택 포인터는 함수를 시작할 때 값으로 돌아가야 합니다. 그러므로 다시 base 주소가 되어야 하지요. 리턴값이 하나면 base 주소 하나만 리턴하고 리턴값이 여러개면 base 주소에 바이트 코드 버퍼에 저장되있는 리턴값 개수만큼 앞에 있는 스택 주소를 넘깁니다. 조정은 받는 쪽에서 합니다.

밑으로 명령어가 몇개 있는데 디버깅 정보 표시용 명령어라서 넘어가겠습니다. 힘드네요. 그래도 두 편에 Opcode.c를 다 읽어서 다행입니다. 길어졌으면 지칠 뻔 했습니다.

댓글남기기