Lua除了是一门扩展的言语外,还是一门“胶水语言”。最著名的就是作为游戏的脚本开发。
这里主要说lua怎样与c交互。Lua和c语言通信的主要方法是一个无所不在的虚拟栈。几乎所有的API调用都会操作这个栈上的值。所有的数据交换,无论是Lua到C语言或C语言到Lua都通过这个栈来完成。栈可以解决Lua和C语言之间存在的两大差异,第一种差异是Lua使用垃圾收集,而C语言要求显式地释放内存;第二种是Lua使用动态类型,而C语言使用静态类型。
环境搭建
1 | lua_State *L = luaL_newstate();//创建一个新的环境 |
luaL_newstate用c运行库的内存分配函数。lua_newstate可自定义内存分配函数。
头文件lua.h定义了lua提供的基础函数,包括创建lua环境、调用lua函数(如lua_pcall)、读写lua环境中全局变量,以及注册供lua调用的新函数等。Lua.h中定义所有内容都有一个lua_前缀。
头文件lauxlib.h定义了辅助库(auxiliary library,auxlib)提供的函数。它的所有定义都以luaL_开头(如luaL_loadbuffer)。辅助库是一个使用lua.h中API编写出的一个较高的抽象层。Lua的所有标准库编写都用到了辅助库。
Lua脚本的编译执行是相互独立的,在不同的线程上执行。通过luaL_newstate()函数可以申请一个虚拟机,返回指针类型lua_State。今后其他所有Lua Api函数的调用都需要此指针作为第一参数,用来指定某个虚拟机。所以lua_State代表一个lua虚拟机对像,luaL_newstate()分配一个虚拟机。lua类库管理着所有的虚拟机。销毁指定虚拟机的所有对像(如果有垃圾回收相关的无方法则会调用该方法)并收回所有由该虚拟机动态分配产生的内存,在有些平台下我们不需要调用此函数,因为当主程序退出时,资源会被自然的释放掉,但是但一个长时间运行的程序,比如后台运行的web服务器,需要立即回收虚拟机资源以避免内存过高占用。
Lua虚拟机相关的数据结构与栈
解释器要做的就是模拟计算机的执行,这主要分为以下两大块。
CPU:用于指令的执行。
内存:用于数据的存储。
指令执行的部分在lua虚拟机(lua虚拟机工作流程)大体介绍过,即解释器分析Lua文件之后生成Proto结构体,最后到luaV_execute函数中依次取出指令来执行。
而“内存”部分,在Lua解释器中就存放在Lua栈中。Lua中也是把栈的某一个位置称为寄存器(非真CPU中的寄存器)。
每个Lua虚拟机对应一个lua_State结构体,它使用TValue数组来模拟栈,其中包括几个与找相关的成员。stack:栈数组的起始位置。base: 当前函数栈的基地址 。top: 当前栈的下一个可用位置 。
这些成员的初始化操作在stack_init函数中完成。
1 | static void stack_init (lua_State *L1, lua_State *L) { |
再看看lua_State。
1 | struct lua_State { |
lua_State里面存放的是一个Lua虚拟机的全局状态,当执行到一个两数时,需要有对应的数据结构来表示函数相关的信息。这个数据结构就是CallInfo,这个结构体中同样有top、base这两个与栈相关的成员。
1 | /* |
无论函数怎么执行,有多少函数,最终它们引用到的栈都是当前Lua虚拟机的栈。 这好比一个操作系统中的进程无论有多少,最终引用的内存实际上都还是由操作系统内核来管理的。
在lua_State结构体中,有一个base_ci的CallInfo数组,存储的就是CallInfo的信息。而另一个ci成员,指向的就是当前函数的CallInfo指针。
在调用函数之前,一般会调用luaD_precall函数,它主要完成如下几个操作 。
(1)保存当前虚拟机执行的指令
savedpc到当前CallInfo的savedpc中。此处保存下来是为了后面调用完毕之后恢复执行
(2)分别计算出待调用函数的base、top值,这些值的计算依赖于函数的参数数量
(3)从lua_State的base_ci数组中分配一个新的CallInfo指针,存储前面两步计算出来的信息,切换到这个函数中准备调用。
可以看到,lua_State结构体中的top、base指针是与函数执行相关的变量,在函数执行前后都会有所变化。
需要注意的是,前后调用的函数中Lua栈的大小是有限的,同时CallInfo数组的大小也是有限的。 栈的使用和函数的嵌套层次都不能过多,以防这些资源、用尽了。 这就好比操作系统内核不可能无限制新建进程,也不可能无限制分配内存,资源总是有限的。