Lua与宿主语言(C/C++)交互原理

来源:互联网 发布:55开淘宝店地址 编辑:程序博客网 时间:2024/06/05 14:55

Lua语言可研究的东西真是多,各种机制原理:与宿主语言(下文均指C/C++)的交互、内存管理(垃圾回收)、虚拟机实现、协程、闭包、异常捕获机制等。如取其一进行研究,要吃透还是需要点时间和精力。相信只要一点点慢慢啃,终究还是会将其吸收。

以下的相关原理介绍是基于Lua-5.1.5版本的源码,不排除与之后版本的源码中有少部分差异存在,但基本原理应该相同。

一、Lua与宿主语言(C/C++)的交互

1、lua_State、global_State

1). lua_State

lua_StateLua语言中的一种基本类型,类似TStringTable等,主要用来管理一个lua虚拟机的执行环境,一个lua虚拟机可以有多个执行环境,lua_State最主要的功能就是用于函数调用以及和C/C++的交互。

主要功能包括:

1. 数据栈管理,包括交互过程中参数压栈和出栈、函数注册的临时数据存储等。

2. 调用栈管理,其中CallInfo结构表示一次调用,包括指向数据栈中数据边界指针topbase、被调用函数指针func

3. 全局表l_gt管理,注意:它其实只是在当前lua_State范围内是全局唯一的,和global_Statel_registry注册表不同,l_registrylua虚拟机范围内是全局唯一的。

4. gc的一些管理和当前栈中upvalue的管理(出现闭包应用场景时)。

5. hook相关的,包括hookmaskhookcounthook函数等(暂未了解)。

以上12点是LuaC/C++交互时操作最频繁的步骤,因此整理数据栈和调用栈的操作流程是理解交互的重中之重。

2). global_State

Lua 虚拟机全局状态的存储的地方,管理lua虚拟机的全局环境,所有的lua_State共享这个全局状态,由于Lua被设计为单线程的,所以global_State上的状态控制没有考虑多线程问题。主要功能可分为:内存分配策略、全局字符串hashtable管理、注册表、gc管理、lua_State集合管理、元表管理(暂时尚未知用来干嘛)等。

最后记住一条:一个Lua虚拟机有且只有一个global_State对象,一个进程中允许多个Lua虚拟机同时运行。运行形态例如下图所示:


2、启动Lua虚拟机过程

LuaAPI中提供了宏lua_open()用于启动一个Lua虚拟机,之所以起了这个名,估计是为了与lua_close()对应,openclose看着就知道明显是一对。

启动Lua虚拟机的过程总结成一句话就是:构造global_Statelua_State对象,并初始化。具体包括以下几个步骤:

1、通过内存分配策略(l_alloc)分配两个对象(global_Statelua_State)大小的内存空间。

2、初始化lua_State对象,例如:类型设置、数据栈空间分配(45*TValue)、调用栈空间分配(8*CallInfo)等。

3、初始化global_State对象,例如:主lua_State设置、内存分配方法、全局字符串表等。

4、加载所有标准库到Lua虚拟机执行环境中,实际操作就是将各种库提供库函数通过现有的注册机制注册到当前lua_Statel_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脚本进行相互调用。

3、栈操作原理

1). DataStack、CallStack

与宿主语言交互时的栈主要涉及两个,为了方便理解,暂且将它归纳为: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虚拟机启动后,两个栈原始状态的直观感觉:


2). 一次调用的栈操作

         在了解完以上所有前提知识点后,从一次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的状态图:

 

0 0
原创粉丝点击