lua堆栈

来源 https://blog.csdn.net/suhuaiqiang_janlay/article/details/56702381

来源 https://blog.csdn.net/suhuaiqiang_janlay/article/details/63683036

一、Lua脚本语言

1. 概述

Lua是一种脚本编程语言,与一般脚本语言不同,被称为是嵌入式的脚本语言。Lua最著名的应用是在暴雪公司的网络游戏魔兽世界中。

Lua语言可以独立进行编程,但这不是其主要的使用方式。Lua最典型的用法,是作为一个库,嵌入到其他大型语言(称为宿主语言)的应用程序之中,为应用程序提供参数配置或逻辑描述等功能,带来前所未有的灵活性。

lua堆栈-LMLPHP

Lua常见的宿主语言有:C/C++、Java、.NET,甚至脚本语言如PHP、Ruby。

2. Lua与相似解决方案的比较

lua堆栈-LMLPHP

Lua体积很小,往往使用静态链接嵌入到程序内部,在发布应用时不需要附带任何的运行时支持。

3. 宿主语言中嵌入Lua的工作流程

(1)宿主语言建立Lua解释器对象

(2)将宿主语言实现的Lua扩展,如函数等,注册到Lua解释器中,供其使用。

(3)读入Lua源程序或预先编译好的Lua程序。

(4)执行读入的Lua程序。

二、Lua虚拟机的初始化

Lua工作的核心是Lua虚拟机,宿主语言在加载和执行Lua脚本时,做的第一件事情就是创建并初始化Lua虚拟机。

1. 创建Lua虚拟机

lua_State *lua_newstate(lua_Alloc f, void *ud) API可以为我们创建一个新的独立的Lua虚拟机。

参数指定了虚拟机中的内存分配策略,例如我们已经在自己的代码中实现了内存池,这时候只需要写一个符合lua_Alloc原型的适配器,然后指定为Lua的内存分配器就可以了,使得内存分配更加灵活。当然,如果不想自定义内存分配策略,也可以使用luaL_newstate,这样Lua会帮你定义默认的内存分配策略。

我们可以先来看一下luaL_newstate的源码:

// luaconf.h
/*
@@ LUA_EXTRASPACE defines the size of a raw memory area associated with
** a Lua state with very fast access.
** CHANGE it if you need a different size.
*/
#define LUA_EXTRASPACE (sizeof(void *)) // lstate.c
/*
** thread state + extra space
*/
typedef struct LX {
lu_byte extra_[LUA_EXTRASPACE];
lua_State l;
} LX; /*
** Main thread combines a thread state and the global state
*/
typedef struct LG {
LX l;
global_State g;
} LG; // lstate.c
LUA_API lua_State *lua_newstate (lua_Alloc f, void *ud) {
int i;
lua_State *L;
global_State *g;
LG *l = cast(LG *, (*f)(ud, NULL, LUA_TTHREAD, sizeof(LG)));
if (l == NULL) return NULL;
L = &l->l.l;
g = &l->g;
......
return L;
}

可见,通过luaL_newstate 创建Lua虚拟机时,第一块申请的内存将用来存储global_State(全局状态机)和lua_State(主线程)实例。为了避免内存碎片的产生,同时减少内存分配和释放的次数,Lua采用了一个小技巧:利用一个LG结构,把分配lua_State和global_State的行为关联在一起。这个LG结构是在C文件内部定义,而不存在公开的H文件中,仅供该C代码文件使用,因此这种依赖数据结构内存布局的用法负作用不大。

lua堆栈-LMLPHP

2. 关于global_State和lua_State

在一个独立的Lua虚拟机中,global_State是一个全局的结构, 而lua_State可以有多个。

lua堆栈-LMLPHP

global_State

global_State结构,我们可以称之为Lua全局状态机。从Lua的使用者角度来看,global_State结构是完全感知不到的:我们无法用Lua公开的API获取到它的指针、句柄或引用,而且实际上我们也并不需要引用到它。但是对于Lua的实现来说,global_State是十分重要的部分。

它管理着Lua中全局唯一的信息,主要是以下功能:

(1)内存分配策略及其参数

在调用lua_newstate的时候配置它们. 也可以通过lua_getallocf和lua_setallocf随时获取和修改它

(2)字符串的hashtable

全局的字符串哈希表,即保存那些短字符串,使得整个虚拟机中短字符串只有一份实例。具体参见 Lua字符串处理

(3)垃圾回收相关的信息,内存使用统计量

(4)panic, 当无保护调用发生时, 会调用该函数, 默认是null, 可以通过lua_atpanic配置.(用于异常处理)

(5)注册表, 注册表是一个全局唯一的table

(6)记录lua中元方法名称 和 基本类型的元表

[注意, lua中table和userdata每个实例可以拥有自己的独特的元表--记录在table和userdata的mt字段, 其他类型是每个类型共享一个元表--就是记录在这里].

(7)upvalue链表

(8)主lua_State, 一个lua虚拟机中, 可以有多个lua_State, lua_newstate会创建出一个lua_State(称为主线程), 并邦定到global_state的主lua_State上

lua_State

线程,这里线程的概念区别于操作系统的线程,实际上也是Lua中定义的一种状态机。lua_State主要是管理一个lua虚拟机的执行环境, 一个lua虚拟机可以有多个执行环境。

(1)要注意的是, 和nil, string, table一样,lua_State也是lua中的一种基本类型, lua中的表示是TValue {value = lua_State, tt = LUA_TTHREAD}

(2)lua_State的成员和功能

a. 栈的管理, 包括管理整个栈和当前函数使用的栈的情况

b. CallInfo的管理, 包括管理整个CallInfo数组和当前函数的CallInfo

c. hook相关的, 包括hookmask, hookcount, hook函数等

d. 全局表l_gt, 注意这个变量的命名, 很好的表现了它其实只是在本lua_State范围内是全局唯一的的, 和注册表不同, 注册表是lua虚拟机范围内是全局唯一的
e. gc的一些管理和当前栈中upvalue的管理 
f.  错误处理的支持
(3)从lua_State的成员可以看出来, lua_State最主要的功能就是函数调用以及和c的通信.

3. lua_newstate函数的流程

(1)新建一个global_state和一个lua_State

(2)初始化, 包括给g_s创建注册表, g_s中各个类型的元表的默认值全部置为0

(3)给l_s创建全局表, 预分配l_s的CallInfo和stack空间

(4)其中涉及到了内存分配统统使用lua_newstate传进来的内存分配器分配

1:lua_push* 压栈API

lua_push*这些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栈大小

lua 只保证在从 Lua 进入 C 的边界上提供额外的 LUA_MINSTACK 个 slot 。这个值默认为 20 ,一般是够用的。正因为一般够用,反而容易被编写 C 扩展的同学忽视。尤其是在 C 扩展的代码里有 C 层次上的递归时,非常容易在边界情况下栈溢出。因为 Lua 的 stack 实际上又经常留出超过 LUA_MINSTACK 的空间,这种 bug 不易察觉。记住:如果你在 C 扩展中做复杂的事情,一定要记得在使用 lua stack 前,用 luaL_checkstack 留够你需要的空间。

OK,这篇文章主要是借lua_newstate讲述global_State和lua_State的结构与作用,希望对大家了解Lua工作环境有一点帮助。

下一篇将讲述Lua栈相关的内容,更新中。。。

参考文献:

http://www.cnblogs.com/ringofthec/archive/2010/11/09/lua_State.html

http://blog.csdn.net/maximuszhou/article/details/46277695

一、Lua栈

1. 什么是lua栈

lua的栈类似于以下的定义, 它是在创建lua_State的时候创建的:  TValue stack[max_stack_len]  // 欲知内情可以查 lstate.c 的stack_init函数

存入栈的数据类型包括数值, 字符串, 指针, talbe, 闭包等, 下面是一个栈的例子:

lua堆栈-LMLPHP

2. TValue结构

压入的类型有数值, 字符串, 表和闭包[在c中看来是不同类型的值], 但是最后都是统一用TValue这种数据结构来保存的:), 下面用图简单的说明一下这种数据结构:

lua堆栈-LMLPHP

p -- 可以存一个指针, 实际上是lua中的light userdata结构
  n -- 所有的数值存在这里, 不过是int , 还是float

b -- Boolean值存在这里, 注意, lua_pushinteger不是存在这里, 而是存在n中, b只存布尔
  gc -- 其他诸如table, thread, closure, string需要内存管理垃圾回收的类型都存在这里
  gc是一个指针, 它可以指向的类型由联合体GCObject定义, 从图中可以看出, 有string, userdata, closure, table, proto, upvalue, thread

从上面的图可以的得出如下结论:
   1. lua中, number, boolean, nil, light userdata四种类型的值是直接存在栈上元素里的, 和垃圾回收无关.

2. lua中, string, table, closure, userdata, thread存在栈上元素里的只是指针, 他们都会在生命周期结束后被垃圾回收.

lua value 和 c value的对应关系

c         lua
         nil          无   {value=0, tt = t_nil}
      boolean      int  非0, 0   {value=非0/0, tt = t_boolean}
      number      int/float等   1.5   {value=1.5, tt = t_number}
   lightuserdata   void*, int*, 各种*  point   {value=point, tt = t_lightuserdata}
      string         char  str[]   {value=gco, tt = t_string}   gco=TString obj
      table           无   {value=gco, tt = t_table}  gco=Table obj
      userdata           无   {value=gco, tt = t_udata} gco=Udata obj
      closure           无   {value=gco, tt = t_function} gco=Closure obj

lua堆栈-LMLPHP

二、通过Lua栈实现和C++的通讯

1. Lua和C通讯的约定

lua和c通信时有这样的约定: 所有的lua中的值由lua来管理, c++中产生的值lua不知道, 类似表达了这样一种意思: "如果你(c/c++)想要什么, 你告诉我(lua), 我来产生, 然后放到栈上, 你只能通过api来操作这个值, 我只管我的世界", 这个很重要, 因为:
     "如果你想要什么, 你告诉我, 我来产生"就可以保证, 凡是lua中的变量, lua要负责这些变量的生命周期和垃圾回收, 所以, 必须由lua来创建这些值(在创建时就加入了生命周期管理要用到的簿记信息)
     "然后放到栈上, 你只能通过api来操作这个值", lua api给c提供了一套完备的操作界面, 这个就相当于约定的通信协议, 如果lua客户使用这个操作界面, 那么lua本身不会出现任何"意料之外"的错误.
     "我只管我的世界"这句话体现了lua和c/c++作为两个不同系统的分界, c/c++中的值, lua是不知道的, lua只负责它的世界。

2. 栈的索引规则

栈底到栈顶索引呈+1递增的规律,同时索引有正数索引和负数索引两种表示方式:

1. 正数索引,不需要知道栈的大小,我们就能知道栈底在哪,栈底的索引永远是1

即:栈底是1,然后一直到栈顶逐渐+1
2. 负数索引,不需要知道栈的大小,我们就能知道栈顶在哪,栈顶的索引永远是-1

即:栈顶是-1,然后一直到栈底逐渐-1

3. Lua和C++通讯实例

假设在一个lua文件中有如下定义:

-- hello.lua 文件
myName = "beauty girl"

想要在C++中获取到myName的值,可以lua_getglobal 来获取:

/* 取得table变量,在栈顶 */
lua_getglobal(pL, "myName ");

lua_getglobal的处理过程如下:(请注意红色数字,代表通信顺序)

lua堆栈-LMLPHP

1) C++想获取Lua的myName字符串的值,所以它把myName放到Lua堆栈(栈顶),以便Lua能看到
2) Lua从堆栈(栈顶)中获取myName,此时栈顶再次变为空
3) Lua拿着这个myName去Lua全局表查找myName对应的字符串
4) 全局表返回一个字符串”beauty girl”
5) Lua把取得的“beauty girl”字符串放到堆栈(栈顶)
6) C++可以从Lua堆栈中取得“beauty girl”

现在,我们给helloLua.lua文件添加一个table全局变量:

-- helloLua.lua文件
myName = "beauty girl"
helloTable = {name = "mutou", IQ = }

我们看到,多了一个helloTable的变量,它和数组十分相似,又和HashMap有点类似,总之它很强大。
获取helloTable变量的方式和以前是一样的:

/* 取得table变量,在栈顶 */
lua_getglobal(pL, "helloTable");

这样,helloTable变量就被存放到栈顶。
可我们并不是要取table变量,因为C++中是无法识别Lua的table类型的,所以我们要取得table中具体的值,也就是name和IQ的值。
 
有一个和lua_getglobal类似的函数,叫做lua_gettable,顾名思义,它是用来取得table相关的数据的。
lua_gettable函数会从栈顶取得一个值,然后根据这个值去table中寻找对应的值,最后把找到的值放到栈顶。
lua_pushstring()函数可以把C++中的字符串存放到Lua的栈里;
然后再用lua_gettable()取执行前面所说的步骤,lua_gettable的第二个参数是指定的table变量在栈中的索引。

为了方便理解,我们画个图来表示:

lua堆栈-LMLPHP

这是初始状态,堆栈里还没有任何东西,那么,现在要先把helloTable变量放到栈顶:

/* 取得table变量,在栈顶 */
lua_getglobal(pL, "helloTable");

然后就变成了这样:

lua堆栈-LMLPHP

接着,我们要取得table的name对应的值,那么,先要做的就是把”name”字符串入栈:

/* 将C++的字符串放到Lua的栈中,此时,栈顶变为“name”, helloTable对象变为栈底 */
lua_pushstring(pL, "name");

然后变成这样:

lua堆栈-LMLPHP

注意了,我把栈的索引也加上了,因为我们即将要使用,这次我们用负数索引。
由于”name”的入栈,现在helloTable变量已经不在栈顶了。
接着,我们调用要做最重要的一步了,取得name在table中对应的值:

/*
从table对象寻找“name”对应的值(table对象现在在索引为-2的栈中,也就是当前的栈底),
取得对应值之后,将值放回栈顶
*/
lua_gettable(pL, -);

此时,栈变成这样:

lua堆栈-LMLPHP

lua_gettable倒底做了什么事情?
首先,我们来解释一下lua_gettable的第二个参数,-2是什么意思,-2就是刚刚helloTable变量在栈中的索引。
然后,Lua会去取得栈顶的值(之前的栈顶是”name”),然后拿着这个值去helloTable变量中寻找对应的值,当然就找到”mutou”了。
最后,Lua会把找到的值入栈,于是”mutou”就到了栈顶了。

最后,简单写了个Lua和C++相互调用的实例,代码地址:Lua和C++交互示例代码

============= End

05-28 20:44