关于Lua与C数据通信的栈深入理解
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手册。