深入理解关于Lua与C数据通信的栈

Lua和C程序通过一个堆栈交换数据: struct lua_State 
  堆栈的序号可以从栈顶和栈底计数,从栈底计数,则栈底是1,向栈顶方向递增。从栈顶计数,则栈顶是-1,向栈底方向递减。一般都用从栈顶计数的方式。堆栈的默认大小是20,可以用lua_checkstack修改.用lua_gettop则可以获得栈里的元素数目。并不是说在栈顶有一个整形元素。而是计算了一下栈顶元素在栈里的正index,相当于元素数目。 
  Lua 调用C函数用的堆栈是临时的,调用结束之后就被销毁了。 
  如何从堆栈中获取从Lua脚本中的参数 
  如果知道Lua脚本中某个全局变量的名字,可以用void lua_getglobal (lua_State *L, const char *name) 。这个函数会将name所指Lua变量的值放在栈顶. 
  如果是在C 函数中要获取Lua调用函数使用的参数: 
  首先用lua_gettop检查参数数量 
  用lua_is...类函数检测参数的类型,做好错误处理 
  用lua_to...类函数将参数转换为number或者string.(对Lua来说,只有这两种简单类型) 
  lua_tonumber返回的是double 
  lua_tostring返回的是char* 
  用lua_remove从栈中删除掉元素 
  继续获取下一个元素. 因为每次都调用lua_remove,所以每次调用lua_tonumber,使用的index都将固定是-1,即栈顶。 
  如果lua_istable成立,那么说明栈顶是一个table.注意table是不能取出来的,只能把table里的元素一个个取出来。 
  首先把元素的名字压入栈顶: lua_pushstring(L,"i");然后就可以用lua_gettable调用,值会放在栈顶。同时刚才压入的元素名字被弹出。用上面的办法,可以把这个值取出来。记得也应该lua_remove。 如果table的某一个元素也是table,重复即可。当table的所有元素都取完了,记住这个table本身还在堆栈里,要用lua_remove把它删除。 
  如果要获取的是一个数组(所谓数组,其实就是key是从1开始的数字序列的table,并且值类型相同),用lua_next可以遍历这个数组: 
  首先lua_pushnil,压入一个空值,然后 
  while (lua_next(L, -2) != 0)
  { 
    if(lua_isnumber(L,-1)) //判断元素类型,也可能是string 
    { 
      arrf.add((float)lua_tonumber(L, -1));//获取元素的值 
      lua_remove(L,-1); 
    }
  }
  lua_remove(L,-1);//删除NIL 

  如何从C返回数据给Lua脚本

  用lua_push...类函数压入数据到堆栈中,并用return n;来告诉Lua返回了几个返回值。 Lua是天生支持多个返回值的,如 x,y = Test()。 Lua会根据n从栈里取相应的数据。

  如果要返回一个table: 
  lua_newtable(L);  //创建一个表格,放在栈顶 
  lua_pushstring(L, "mydata");  //压入key 
  lua_pushnumber(L,66);  //压入value 
  lua_settable(L,-3);  //弹出key,value,并设置到table里面去 
  lua_pushstring(L, "subdata");  //压入key 
  lua_newtable(L);  //压入value,也是一个table 
  lua_pushstring(L, "mydata");  //压入subtable的key 
  lua_pushnumber(L,53);  //value 
  lua_settable(L,-3);  //弹出key,value,并设置到subtable 
  lua_settable(L,-3);  //这时候父table的位置还是-3,弹出key,value(subtable),并设置到table里去 
  lua_pushstring(L, "mydata2");  //同上 
  lua_pushnumber(L,77); 
  lua_settable(L,-3); 
  return 1;  //堆栈里现在就一个table.其他都被弹掉了。
如果要返回一个数组,用如下代码:(注意那个关于trick的注释,我在等官方的解释。经过验证,这个问题只在windows版本调用dll中方法的时候出现。WinCE正常)

  lua_pushstring(L,"arri"); 
  lua_newtable(L); 
  { 
    //a trick:otherwise the lua engine will crash. This element is invisible in Lua script 
    lua_pushnumber(L,-1); 
    lua_rawseti(L,-2,0); 
    for(int i = 0; i < arri.size();i++) 
    { 
      lua_pushnumber(L,arri); 
      lua_rawseti(L,-2,i+1); 
    }
   }
   lua_settable(L,-3);
  这样产生的数组可以在Lua中如下遍历:

  for i,v in ipairs(data.arri) do 
    print(v) 
  end
  或者是
  for i=1,table.getn(data.arri) do 
    print(data.arri) 
  end
  只有数组才能这样,name,value构成的Record不行,table.getn也只对数组有效。

  由于上述代码的高度相似性,所以很容易实现自动生成这些代码。比如,根据C的一个struct定义:

  typedef enum { BR_9600, BR_4800,} BaudRate;
  typedef struct flag{ int onoff; int j; long l; double d; char* name; BaudRate rate;} flag;  可以自动产生如下代码:
  bool DataToLua(flag data,lua_State *L)
  { 
    lua_newtable(L); 
    lua_pushstring(L,"onoff"); 
    lua_pushnumber(L,(double)data.onoff); 
    lua_settable(L,-3); 
    lua_pushstring(L,"j"); 
    lua_pushnumber(L,(double)data.j); 
    lua_settable(L,-3); 
    lua_pushstring(L,"l"); 
    lua_pushnumber(L,(double)data.l); 
    lua_settable(L,-3); 
    lua_pushstring(L,"d"); 
    lua_pushnumber(L,(double)data.d); 
    lua_settable(L,-3); 
    lua_pushstring(L,"name"); 
    lua_pushstring(L,data. name.c_str()); 
    lua_settable(L,-3); 
    lua_pushstring(L,"rate"); 
    lua_pushnumber(L,(double)(int)data.rate); 
    lua_settable(L,-3); 
    return true;
  }
  LuaToData也是类似的。

  如果使用面向对象的方式封装起flag来,把DataToLua变成flag类的一个方法,就更加方便了。

摘自:http://baike.baidu.com/view/416116.htm#8





http://www.tuicool.com/articles/QFzIre

Lua与C交互的栈是一个重要的概念。文章首先解释了为什么要引入Lua栈,然后对访问栈常用的API进行了总结,并使用这些API的注意事项,最后从Lua源代码来看栈的实现原理。

Lua栈概述


      我们知道Lua是一种嵌入式语言,所有的Lua程序最后都需要通过Lua解释器(即Lua虚拟机)把其解析成字节码的形式才能执行。 一方面,我们可以在一个应用程序(拥有主动权)中嵌入Lua解释器,此时使用Lua的目的是方便扩展这个应用程序,用Lua实现相应的工作;另一方面,我们在Lua程序(此时用Lua语言编写的程序拥有主动权)中也可以使用那些用C语言实现的函数(比如string.find())。
   

      在上面两个描述中,都涉及到Lua与C之间数据交换,而在这两种语言交换数据时,我们自然面临两个问题,一个是Lua是动态类型语言,在Lua语言中没有类型定义的语法,每个值都携带了它自身的类型信息,而C语言是静态类型语言;另一个是Lua使用垃圾收集,可以自动管理内存,而C语言要求程序自己释放分配的内存,需应用程序自身管理内存。为了解决这个两个问题,Lua引入了一个虚拟栈。

    

      为了方便Lua与C交互,比如在C代码中调用Lua函数,Lua官方提供了一系列的API和库。利用这些API,C语言就可以方便从Lua中获取相应的值,也可以方便地把值返回给Lua,当然,这些操作都是通过栈作为桥梁来实现的。

访问Lua栈的API

      Lua提供了大量的API用于操作栈,这些API方便我们向栈中压入元素、查询栈中的元素、修改栈的大小等操作。下面对常用的API使用做一个简单总结,尤其在使用这些API的需要注意的地方。


        1、向栈中压入元素 
       向栈中压入元素的API,通常都是以lua_push*开头来命名,比如lua_pushnunber、lua_pushstring、lua_pushcfunction、lua_pushcclousre等函数都是向栈顶中压入一个Lua值。通常在Lua代码中调用C实现的函数并且被调用的C函数有返回值时,被调用的C函数通常就要用到这些接口,把返回值压入栈中,返回给Lua(当然这些C函数也要求返回一个值,告诉Lua一共返回(压入)了多少个值)。值得注意的是,向栈中压入一个元素时,应该确保栈中具有足够的空间,可以调用lua_checkstack来检测是否有足够的空间。

      实质上这些API是把C语言里面的值封装成Lua类型的值压入栈中的,对于那些需要垃圾回收的元素,在压入栈时,都会在Lua(也就是Lua虚拟机中)生成一个副本。比如lua_pushstring(lua_State *L, const char *s)会向中栈压入由s指向的以'\0'结尾的字符串,在C中调用这个函数后,我们可以任意释放或修改由s指向的字符串,也不会出现问题,原因就是在执行lua_pushstring过程中Lua会生成一个内部副本。 实质上,Lua不会持有指向外部字符串的指针,也不会持有指向任何其他外部对象的指针(除了C函数,因为C函数总是静态的)。

       总之,一旦C中值被压入栈中,Lua就会生成相应的结构(实质就是Lua中实现的相应数据类型)并管理(比如自动垃圾回收)这个值,从此不会再依赖于原来的C值。

      2、获取栈中的元素 
     从栈中获取一个值的函数,通常都是以lua_to*开头来命名,比如lua_tonumber、lua_tostring、lua_touserdata、lua_tocfunction等函数都是从栈中指定的索引处获取一个值。通常在C函数中,可以用这些接口获取从Lua中传递给C函数的参数。如果指定的元素不具有正确的类型,调用这些函数也不会出问题的。在这种情况下,lua_toboolean、lua_tonumber、lua_tointeger和lua_objlen会返回0,而其他函数会返回NULL。对于返回NULL的函数,可以直接通过返回值,即可以知道调用是否正确;对于返回0的函数,通常先需要使用lua_is*系列函数,判断调用是否正确。

       注意lua_to*和lua_is*系列函数都是试图转换栈中元素为相应中的值。比如lua_isnumber不会检查是否为数字类型,而是检查是否能转换为数字类型;lua_isstring也类似,它对于任意数字,lua_isstring都返回真。要想真正返回栈中元素的类型,可以用函数lua_type。每种类型对应于一个常量(LUA_TNIL,LUA_TBOOLEAN,LUA_TNUMBER等),这些常量定义在头文件lua.h中。


       值得一提是lua_tolstring函数,它的函数原型是const char *lua_tolstring (lua_State *L, int index, size_t *len)。它会把栈中索引为index的Lua值装换为一个C字符串。若参数Len不为NULL,则*Len会保存字符串的长度。栈中的Lua值必须为string或number类型,否则函数返回NULL。若栈中Lua值为number类型,则该函数实质会改变栈中的值为string类型,由于这个原因, 在利用lua_next遍历栈中的table时,对key使用lua_tolstring尤其需要注意,除非知道key都是string类型。 lua_tolstring函数返回的指针,指向的是Lua虚拟机内部的字符串,这个字符串是以'\0'结尾的,但字符串中间也可能包含值为0的字符。 由于Lua自身的垃圾回收,因此当栈中的字符串被弹出后,函数返回的指针所有指向的字符串可能就不能再有效了。也说明了,当一个C函数从Lua收到一个字符串参数时,在C函数中,即不能在访问字符串时从栈中弹出它,也不能修改字符串。


      3、其他操作栈的函数 
      int lua_call (lua_State *L, int nargs, int nresults); 
      调用栈中的函数,在调用lua_call之前,程序必须首先要保证被调用函数已压入栈,其次要被调用函数需要的参数也已经按顺序压入栈,也就是说,第一个参数最先被压入栈,依次类推。nargs是指需要压入栈中参数的个数,当函数被调用后,之前压入的函数和参数都会从栈中弹出,并将函数执行的结果按顺序压入栈中,因此最后一个结果压入栈顶,同时,压入栈的个数会根据nresults的值做调整。与lua_call相对应的是lua_pcall函数,lua_pcall会以保护模式调用栈中的函数。以保护模式调用意思是,当被调用的函数发生任何错误时,该错误不会传播,不像lua_call会把错误传递到上一层,lua_pcall所调用的栈中函数发送错误时,lua_pcall会捕捉这个错误,并向栈中压入一个错误信息,并返回一个错误码。在应用程序中编写主函数时,应该使用lua_pcall来调用栈中的函数,捕获所有错误。而在为Lua编写扩展的C函数时,应该调用lua_call,把错误返回到脚本层。

      void lua_createtable (lua_State *L, int narr, int nrec); 
      创建一个新的table,并把它压入栈顶,参数narr和nrec分别指新的table将会有多少个数组元素和多少需要hash的元素,Lua会根据这个两个值为新的table预分配内存。对于事先知道table结构,利用这两个参数能提高创建新table的性能。对于事先不知道table结构,则可以使用void lua_newtable (lua_State *L),它等价于lua_createtable(L, 0, 0)。


       除了上面提到的C API,还有许多其他有用的C API,比如操作table的接口有:lua_getfield、lua_setfield、lua_gettable、lua_settable等接口,在具体使用时,可以参照Lua手册。

从源码看Lua栈

      在应用程序(比如用C++编写的)中,为了加载和执行Lua脚本,我们首先需要在应用程序主函数中,调用luaL_newstate()(该函数就会创建Lua与C交互的栈)初始化Lua虚拟机。该函数返回的是一个指向lua_State类型的指针L,几乎所有的API的第一个参数类型都是lua_State*,需要传入的值就是luaL_newstate()函数返回的指针,这样做的目的是使得每个Lua状态机是各自独立的,不共享任何数据。

       lua_State表示的一个Lua程序的执行状态,它代表一个新的线程(注意是指Lua中的thread类型,不是指操作系统中的线程),每个线程拥有独立的数据栈以及函数调用链,还有独立的调试钩子和错误处理方法。 实质上几乎所有的API操作,都是围绕这个结构lua_State来进行的,包括把数据压入栈、从栈中取出数据、执行栈中的函数等操作。结构lua_State定义在lstate.h中,代码如下:

struct   lua_State {
  CommonHeader;
  lu_byte status;
  StkId top;	          /* 指向数据栈中,第一个可使用的空间*/
  global_State *l_G;
  CallInfo *ci;	       /* 保存着当前正在调用的函数的运行状态 */
  const Instruction *oldpc;  
  StkId stack_last;	   /* 指向数据栈中,最后一个可使用的空间 */
  StkId stack;	        /* 指向数据栈开始的位置 */
  int stacksize;	      /*栈当前的大小,注意并不是可使用的大小*/
  unsigned short nny;  
  unsigned short nCcalls;  
  lu_byte hookmask;
  lu_byte allowhook;
  int basehookcount;
  int hookcount;
  lua_Hook hook;
  GCObject *openupval; 
  GCObject *gclist;
  struct lua_longjmp *errorJmp;  
  ptrdiff_t errfunc;  
  CallInfo base_ci;	     /* 保存调用链表的第一个节点*/
};
         lua_State中主要包含两个重要的数据结构,一个是数据栈,另外一个是调用栈(实质上是一个双向链表)。数据栈实质就是一个动态数组,数组中每个元素类型为TValue。Lua中任何数据类型(nil,number,stirng,userdata,function等)都是用该结构体TValue来实现的。   其定义如下(源码里面使用了大量的宏和typedef来定义TValue,为了方便阅读,把它展开了):  


       Lua中所有的数据类型都是由结构体TValue来实现的,它把值和类型绑在一起,每个数据都携带了它自身的类型信息。 用成员tt_保存数据的类型,成员value_用来保存数据值,它使用的一个联合体来实现的: 

union  Value {
  GCObject *gc;   /*gc指向一个对象,这些对象是需要垃圾回收的数据类型,比如table、string等*/
  void *p;	 /* lua中的light userdata类型,实质上保存的就是一个指针 */
  int b;	   /*boolean类型*/
  lua_CFunction f;/*lua中light C functions(没有upvalue),即只是函数指针 */
  double n;	/*lua中的number类型*/
};

       上面提到到数据栈是在函数stack_init中创建的(初始化虚拟机时调用的luaL_newstate,就是通过调用lua_newstate函数,lua_newstate调用f_luaopen函数,最后f_luaopen函数调用stack_init来初始化栈的),函数stack_init在lstate.c中实现, 代码如下: 

static void stack_init (lua_State *L1, lua_State *L) {
  int i; CallInfo *ci;

  /* 为数据栈分配空间,并且初始化lua_State与数据栈相关的成员*/
  L1->stack = luaM_newvector(L, BASIC_STACK_SIZE, TValue);
  L1->stacksize = BASIC_STACK_SIZE;
  for (i = 0; i < BASIC_STACK_SIZE; i++)
    setnilvalue(L1->stack + i);  /* erase new stack */
  L1->top = L1->stack;
  L1->stack_last = L1->stack + L1->stacksize - EXTRA_STACK;

  /*初始化lua_State与调用链表相关的成员*/
  ci = &L1->base_ci;
  ci->next = ci->previous = NULL;
  ci->callstatus = 0;
  ci->func = L1->top;
  setnilvalue(L1->top++);  /* 'function' entry for this 'ci' */
  ci->top = L1->top + LUA_MINSTACK;
  L1->ci = ci;
}
    调用栈实质上用一个双向链表来实现的,链表中的每个节点是用一个CallInfo的结构体来实现,保存着正在调用的函数的运行状态。   结构体CallInfo在lstate.h定义的,代码如下:  
typedef struct CallInfo {
      StkId  func; /* 指向被调用的函数在栈中的位置*/
      StkId  top; /*指向被调用的函数可以使用栈空间最大的位置,即限定了调用一个函数可以栈空间的大小*/
      struct CallInfo *previous, *next; /* 指向调用链表的前一个节点和下一个节点 */
      short nresults;                  /* 当前被调用的函数期待返回结果的数量*/
      lu_byte callstatus;              /*用来标识当前调用的是C函数还是Lua函数*/
      union {
        struct {                       /* 当调用Lua调用函数时保存信息的结构体*/
          StkId base; 
          const Instruction *savedpc;
        } l;
        struct {                       /*当调用C调用函数时保存信息的结构体*/
          int ctx;  
          lua_CFunction k;  
          ptrdiff_t old_errfunc;
          ptrdiff_t extra;
          lu_byte old_allowhook;
          lu_byte status;
        } c;
    } u;
} CallInfo;
     从CallInfo的定义,可以知道它的成员变量func和top同样指向数据栈的位置,但不同的是,它所关注的是函数调用时相关的位置。   并且在用gdb调试有嵌入Lua的C代码时,我们可以除了查看C中的调用栈信息外,还可以用上面的调用链表获取完整的Lua调用链表,在链表中的每一个节点中,我们可以使用CallInfo中的成员变量func来获取每一个lua函数所在的文件名和行号等调试信息。

总结


       在Lua与C交互中间,我们可以直观想象,有一个叫栈的空间,无论是C还是Lua向对方需要什么数据,都首先需要对方把这数据压入到栈中,然后才能从栈中获取相应的数据。 在Lua与C所有的数据交换中,栈中的每个元素都能保存任何类型的Lua值,并且这个栈是由Lua管理的。

参考资料

ps: 点这里 有学习Lua与C交互的测试代码


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值