Lua语言可研究的东西真是多,各种机制原理:与宿主语言(下文均指C/C++)的交互、内存管理(垃圾回收)、虚拟机实现、协程、闭包、异常捕获机制等。如取其一进行研究,要吃透还是需要点时间和精力。相信只要一点点慢慢啃,终究还是会将其吸收。
以下的相关原理介绍是基于Lua-5.1.5版本的源码,不排除与之后版本的源码中有少部分差异存在,但基本原理应该相同。
lua_State是Lua语言中的一种基本类型,类似TString,Table等,主要用来管理一个lua虚拟机的执行环境,一个lua虚拟机可以有多个执行环境,lua_State最主要的功能就是用于函数调用以及和C/C++的交互。
主要功能包括:
1. 数据栈管理,包括交互过程中参数压栈和出栈、函数注册的临时数据存储等。
2. 调用栈管理,其中CallInfo结构表示一次调用,包括指向数据栈中数据边界指针top和base、被调用函数指针func。
3. 全局表l_gt管理,注意:它其实只是在当前lua_State范围内是全局唯一的,和global_State的l_registry注册表不同,l_registry是lua虚拟机范围内是全局唯一的。
4. gc的一些管理和当前栈中upvalue的管理(出现闭包应用场景时)。
5. hook相关的,包括hookmask,hookcount,hook函数等(暂未了解)。
以上1、2点是Lua与C/C++交互时操作最频繁的步骤,因此整理数据栈和调用栈的操作流程是理解交互的重中之重。
Lua 虚拟机全局状态的存储的地方,管理lua虚拟机的全局环境,所有的lua_State共享这个全局状态,由于Lua被设计为单线程的,所以global_State上的状态控制没有考虑多线程问题。主要功能可分为:内存分配策略、全局字符串hashtable管理、注册表、gc管理、lua_State集合管理、元表管理(暂时尚未知用来干嘛)等。
最后记住一条:一个Lua虚拟机有且只有一个global_State对象,一个进程中允许多个Lua虚拟机同时运行。运行形态例如下图所示:
Lua的API中提供了宏lua_open()用于启动一个Lua虚拟机,之所以起了这个名,估计是为了与lua_close()对应,open、close看着就知道明显是一对。
启动Lua虚拟机的过程总结成一句话就是:构造global_State、lua_State对象,并初始化。具体包括以下几个步骤:
1、通过内存分配策略(l_alloc)分配两个对象(global_State、lua_State)大小的内存空间。
2、初始化lua_State对象,例如:类型设置、数据栈空间分配(45*TValue)、调用栈空间分配(8*CallInfo)等。
3、初始化global_State对象,例如:主lua_State设置、内存分配方法、全局字符串表等。
4、加载所有标准库到Lua虚拟机执行环境中,实际操作就是将各种库提供库函数通过现有的注册机制注册到当前lua_State的l_gt中,之后Lua脚本中就可以直接调用注册过的库函数。
Lua现在支持的库有:协程库、表操作库、io库、系统库、string库、math库、debug库、包处理库,以string库为例:
static const luaL_Reg strlib[] =
{
{"byte", str_byte},
{"char", str_char},
{"dump", str_dump},
{"find", str_find},
{"format", str_format},
{"gmatch", gmatch},
{"gsub", str_gsub},
{"len", str_len},
{"lower", str_lower},
{"match", str_match},
{"rep", str_rep},
{"reverse", str_reverse},
{"sub", str_sub},
{"upper", str_upper},
{"pack", str_pack},
{"packsize", str_packsize},
{"unpack", str_unpack},
{NULL, NULL}
};
luaL_register(L, LUA_STRLIBNAME, strlib); //注册函数
经过以上若干步骤后,lua虚拟机执行环境已准备就绪,宿主语言就可以和lua脚本进行相互调用。
与宿主语言交互时的栈主要涉及两个,为了方便理解,暂且将它归纳为:DataStack和CallStack,其中栈操作指针变量均是lua_State的成员,DataStack可以理解为交互数据的实际存储地,而CallStack中记录着每次调用需要的数据在DataStack中的地址范围,具体如下:
// DataStack
typedef Tvalue*StkId;
StkId top; /* first free slot in the stack (栈顶指针) */
StkId base; /* base of current function (当前调用帧所在DataStack中的栈底) */
StkIdstack_last; /* last free slot in thestack (栈空间的上边界) */
StkIdstack; /* stack base (栈空间的下边界,即栈底)*/
// CallStack
CallInfo*ci; /* call info for current function (当前调用帧)*/
CallInfo *end_ci; /* points after end of ci array (调用栈空间的上边界)*/
CallInfo*base_ci; /* array of CallInfo's (调用栈空间的下边界,即栈底)*/
根据以上分类,清楚可知:在Lua和宿主语言交互时,实际就是这些指向栈的指针不停被更新的过程。下图描述了Lua虚拟机启动后,两个栈原始状态的直观感觉:
在了解完以上所有前提知识点后,从一次C++调用Lua方法的示例入手理解,逐步深入其中的栈操作过程,调用示例如下:
lua_settop(L,0);
lua_getglobal(L,“lua_Function”); //假设lua脚本中有一个名为lua_Function的方法
if( lua_pcall(L, 0, LUA_MULTERT, 0) )
{
/* 调用错误,从(L->top) + index的栈中取出错误信息,然后pop掉该栈帧*/
}
else
{
/* 调用正确,从(L->base) +(index – 1)的栈中取出返回信息,然后pop掉该栈帧 */
}
分解步骤:
(1)、lua_settop(L,0): 调整DataStack中top、base两个指向栈的指针值,使L->top== L->base,指向统一栈帧位置,属于调用的前期准备。
(2)、lua_getglobal(L,“lua_Function”):获取lua脚本中被调用方法“lua_Function”,并将其压入DataStack,此时L->base指向刚入栈栈帧,L->top + 1。检索“lua_Function”的步骤:
1). 计算“lua_Function”字符串的哈希值,然后与global_state的stringtable中匹配出该哈希值下的所有字符串对象所在的冲突链表,遍历链表查询是否存在“lua_Function”,存在则直接返回值,否则临时构造TString对象并返回;
2).根据返回的字符串对象从lua_State的l_gt(所有函数的注册地)中匹配出该函数名对
应的函数对象;
3). 将以上检索出来的函数对象压入DataStack。
(3)、如果调用的lua_Function方法有参数,则继续向DataStack压栈,L->top+ nArgs,nArgs为参数个数,L->base则继续指向被调用方法lua_Function所在的栈位置。
(4)、lua_pcall(L,nArgs, LUA_MULTERT, nRets):开始调用lua方法,在到达真正调用那一步前,需要有以下子操作:
1). 计算此前入栈的lua_Function所在栈位置:func=L->top–(nArgs+1),为何不直接
用L->base?
2). 更新L->base的值,L->base = func+ 1;
3). 更新CallStack中指向当前调用栈帧的L->ci,包括: ++ci、ci->base= L->base、
ci->top=L->base+nArgs、ci->func=func、ci->nresults=nresults等;
4). 解释执行lua_Fucntion函数脚本,该部分涉及Lua虚拟机具体如何实现解释执
行相关机制,此处暂不作过多说明。只需记住一点,执行过程中会将DataStack之前压
入的参数逐一弹出。
(5)、步骤(4)中如果出现递归调用非C函数(lua脚本函数),则重复步骤(4);如果递归调用已注册的C函数,则会触发更新L->ci值操作(类似步骤(4)的第二步子操作),然后重复进入步骤(1)开始新的交互流程。
(6)、最后将返回值压入DataStack中,根据返回值个数逐一将其取出,然后pop掉。
下图表示Lua和宿主语言进行多层调用时的DataStack和CallStack的状态图:
新闻热点
疑难解答
图片精选