Lua调用C函数的实现

本文出自:【InTheWorld的博客】

        看Lua有一段时间了,说实话进展挺慢的。归根到底是因为“动机不纯”,我确实不是抱着学Lua的心态去看资料的。本来看Lua就是听说Lua的实现比较简单,可以借Lua理解解释性语言的一些细节实现。当然Lua也确实不简单的,闭包什么的概念以前都没听说过。没用过Lua却去硬啃Lua语言实现,我也是蛮拼的!
    我主要的参考资料是云风大神的《Lua源码赏析》和高手翻译的《LUA中文教程》。这里我想记录一下函数调用的过程。按照函数调用这条线串联一下各各知识点。首先要从字节码开始了。Lua虚拟机的指令集中有两条用于函数调用的字节码。

OP_CALL,/*  A B C   R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */  
OP_TAILCALL,/*  A B C   return R(A)(R(A+1), ... ,R(A+B-1))      */ 

    后一个适用于尾调用的情况,第一个字节码适用于一般情况。我们知道Lua代码的执行其实就是一个大大的for循环里面套一个大大的switch语句。当虚拟机读到了OP_CALL字节码后,就会调用对应的分支。对应的代码段如下:

vmcase(OP_CALL,  
    int b = GETARG_B(i);  
    int nresults = GETARG_C(i) - 1;  
    if (b != 0) L->top = ra+b;  /* else previous instruction set top */  
    if (luaD_precall(L, ra, nresults)) {  /* C function? */  
      if (nresults >= 0) L->top = ci->top;  /* adjust results */  
      base = ci->u.l.base;  
    }  
    else {  /* Lua function */  
      ci = L->ci;  
      ci->callstatus |= CIST_REENTRY;  
      goto newframe;  /* restart luaV_execute over new Lua function */  
    }  
  )  

     Lua既可以调用Lua函数,也可以调用C函数。我先看看调用C函数吧!重点是luaD_precall(L, ra, nresults))。

int luaD_precall (lua_State *L, StkId func, int nresults) {  
  lua_CFunction f;  
  CallInfo *ci;  
  int n;  /* number of arguments (Lua) or returns (C) */  
  ptrdiff_t funcr = savestack(L, func);  
  switch (ttype(func)) {  
    case LUA_TLCF:  /* light C function */  
      f = fvalue(func);  
      goto Cfunc;  
    case LUA_TCCL: {  /* C closure */  
      f = clCvalue(func)->f;  
     Cfunc:  
      luaD_checkstack(L, LUA_MINSTACK);  /* ensure minimum stack size */  
      ci = next_ci(L);  /* now 'enter' new function */  
      ci->nresults = nresults;  
      ci->func = restorestack(L, funcr);  
      ci->top = L->top + LUA_MINSTACK;  
      lua_assert(ci->top <= L->stack_last);  
      ci->callstatus = 0;  
      luaC_checkGC(L);  /* stack grow uses memory */  
      if (L->hookmask & LUA_MASKCALL)  
        luaD_hook(L, LUA_HOOKCALL, -1);  
      lua_unlock(L);  
      n = (*f)(L);  /* do the actual call */  
      lua_lock(L);  
      api_checknelems(L, n);  
      luaD_poscall(L, L->top - n);  
      return 1;  
    }  
        .........  
  }  
}  

    ptrdiff_t funcr = savestack(L, func);   ci->func = restorestack(L, funcr); 这两句执行完之后,得到的结果就是ci->func = func。n = (*f)(L);通过这一句,实际的C函数就被调用了。 一个典型的lua调用的C函数如下:

static int l_sin (lua_State *L) {  
        double d = lua_tonumber(L, 1); /* get argument */  
        lua_pushnumber(L, sin(d)); /* push result */  
        return 1; /* number of results */  
}  

    这个函数的第一行的作用是获取参数,lua_tonumber(L, 1)是一个宏。它实际调用了lua_tonumberx()。而在这个函数中通过间接的方式调用TValue *o = ci->func + idx;
获得了第idx个参数变量。Call指令明确规定函数参数存放在R(A)后的B-1个栈内。而ci->func就是指向了R(A)。那么Lua是如何保证参数个数的正确的呢?玄机就在lua_tonumberx()所调用的index2addr()中。

static TValue *index2addr (lua_State *L, int idx) {  
   CallInfo *ci = L->ci;  
   if (idx > 0) {  
   TValue *o = ci->func + idx;  
   api_check(L, idx <= ci->top - (ci->func + 1), "unacceptable index");  
   if (o >= L->top) return NONVALIDVALUE;  
   else return o;  
   /* ..... */  
 }  

    这个函数会比较o和L->top的位置,如果大于或等于,就会返回错误。而L->top是在OP_CALL的分支语句里面就改写成了ra+b,也就是最后一个参数的下一个位置。这样的机制就保证了C函数不会把参数个数弄错了。然后是参数返回的问题了,虚拟机指令规定返回值应该在R(A), ... ,R(A+C-2)。Lua是如何实现这种规定的呢?首先看函数lua_pushnumber(),其定义如下:

LUA_API void lua_pushnumber (lua_State *L, lua_Number n) {  
 lua_lock(L);  
 setnvalue(L->top, n);  
 luai_checknum(L, L->top,  
   luaG_runerror(L, "C API - attempt to push a signaling NaN"));  
 api_incr_top(L);  
 lua_unlock(L);  

    这个函数的工作就是把n写到L->top所指向的栈空间,然后把L->top向高位移动一个位置。很明显这个存储位置是不符合Lua虚拟机规定的。所以需要luaD_poscall(L, L->top - n)来完成这个任务。这个n是C函数的返回值个数,所以L->top其实是指向了C函数的第一个返回值。

int luaD_poscall (lua_State *L, StkId firstResult) {  
  StkId res;  
  int wanted, i;  
  CallInfo *ci = L->ci;  
  if (L->hookmask & (LUA_MASKRET | LUA_MASKLINE)) {  
    if (L->hookmask & LUA_MASKRET) {  
      ptrdiff_t fr = savestack(L, firstResult);  /* hook may change stack */  
      luaD_hook(L, LUA_HOOKRET, -1);  
      firstResult = restorestack(L, fr);  
    }  
    L->oldpc = ci->previous->u.l.savedpc;  /* 'oldpc' for caller function */  
  }  
  res = ci->func;  /* res == final position of 1st result */  
  wanted = ci->nresults;  
  L->ci = ci = ci->previous;  /* back to caller */  
  /* move results to correct place */  
  for (i = wanted; i != 0 && firstResult < L->top; i--)  
    setobjs2s(L, res++, firstResult++);  
  while (i-- > 0)  
    setnilvalue(res++);  
  L->top = res;  
  return (wanted - LUA_MULTRET);  /* 0 iff wanted == LUA_MULTRET */  
}  

    其实luaD_poscall也比较简单,它其实就是一个返回值的“搬运工”。res被赋值为ci->func,实际上就指向了前面的R(A)。然后一个for循环实现了拷贝的工作。对应C函数的ci结构也就功成身退了。到此,一个C函数的调用过程就完成了。

发表评论