源码赏析带你零基础掌握Lua!
Posted 西山居质量
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码赏析带你零基础掌握Lua!相关的知识,希望对你有一定的参考价值。
- Lua简介 -
Introduction
发展历史
Lua是一门非常小,但是五脏俱全的动态语言。于1993年诞生于巴西,由里约热内卢天主教大学的一个研究小组所完成。其一开始的设计目的是为了嵌入应用程序中,方便程序进行功能扩展。起初Lua并不怎么引人注目,直到二十一世纪,在2002年,国内有一个网络游戏引入Lua进行开发并取得了巨大的成功,那就是《大话西游2》。其实《大话西游1》用的是微软的JScript,不过这也导致了维护bug多,兼容性差等问题。后来网易在开发《大话2》的时候,决定换一门脚本语言,当时的技术负责人云风认为要挑不出名的语言,让做外挂的人搞不懂。因为当时大话1就是被外挂《月光宝盒》搞死的。于是他们就选择了Lua 4.0。
大话2当时可谓风靡全国,受此影响,后来很多游戏开发行业都采用Lua进行游戏开发。大话2开发的时候用的是Lua 4.0版本,当时Lua还不支持协程,直到2003年Lua发布了第五版,才开始支持协程,这是一次很重大的更新。
大话2只是让Lua在国内开始崭露头角,真正让Lua走上世界舞台的,是2004年发布的《魔兽世界》。除此,还有我们西山居研发的剑网3等。
语言特性
那么究竟是什么原因让各大游戏厂商会选择Lua进行开发呢?
轻量级
Lua是由标准C编写而成的,只有2万多行的代码,一个完整的Lua解释器才200kb左右,比其他脚本语言要小得多的。体积小,启动速度快,使得Lua特别适合嵌入程序。
可扩展
Lua的功能扩展是通过宿主语言,通常是C或C++来实现并提供接口的。在Lua中可以很方便地调用。Lua自身便内嵌了几个常用的库,包括string,table,math等。
速度快
虽然Lua比不上C,Java这类静态语言,但是在动态语言里面,Lua的运行效率是数一数二的。
自动内存管理
Lua有自己的内存管理机制,在用Lua开发的时候,不用担心对象内存的分配和释放。
通用表
Lua用一个常用数据类型table来代替了数组、哈希表。table的储存分为数组部分和哈希表部分,数组部分从1开始做整数数字索引,这可以提供紧凑且高效的随机访问,而不能存储在数组部分的数据会放在哈希表中。Lua的哈希表有一个高效的实现,几乎可以认为操作它的时间复杂度为O(1)。
应用场景
Lua使用最广泛的场景是游戏开发,因为它占用内存小、速度快、而且支持热更新。 其他方面还包括独立应用脚本、web应用脚本、数据库插件以及安全系统等等。
使用的项目
而使用Lua的项目除了前面说到的剑三,魔兽,大话2以外,还有孤岛危机,PS Lightroom等。
-Lua源文件划分-
Division
Lua前面已经做了大致的介绍,接下来了解一下Lua的源码。先从源码的文件结构开始了解,Lua的源文件主要分为4个部分。
虚拟机运转的核心部分
lapi.c C语言接口
lctype.c C标准库中
ctype 相关实现
ldebug.cDebug 接口
ldo.c 函数调用以及栈管理
lfunc.c 函数原型及闭包管理
lgc.c 垃圾回收
lmem.c 内存管理接口
lobject.c 对象操作的一些函数
lopcodes.c 虚拟机的字节码定义
lstate.c 全局状态机
lstring.c 字符串池
ltable.c 表类型的相关操作
ltm.c 元方法
lvm.c 虚拟机
lzio.c 输入流接口
这是Lua的核心代码。主要包括C语言接口,Debug接口,函数调用以及栈管理,垃圾回收,全局状态机,虚拟机等。这几个是比较关键的模块。
源代码解析以及预编译字节码
lcode.c 代码生成器
ldump.c 序列化预编译的 Lua 字节码
llex.c 词法分析器
lparser.c 解析器
lundump.c 还原预编译的字节码
这是跟编译相关的,重点关注解析器parser。
内嵌库
lauxlib.c 库编写用到的辅助函数库
lbaselib.c 基础库
lbitlib.c 位操作库
lcorolib.c 协程库
ldblib.c Debug 库
linit.c 内嵌库的初始化
liolib.c IO 库
lmathlib.c 数学库
loadlib.c 动态扩展库管理
loslib.c OS 库
lstrlib.c 字符串库
ltablib.c 表处理库
内嵌库部分主要是一些Lua内嵌的一些函数库以及库扩展需要用到的函数库。重点关注辅助函数库、基础库、协程库、IO库、数学库、OS库、字符串库和表处理库
解析器和编译器
lua.c 解释器
luac.c 字节码编译器
这是lua的解释器和字节码编译器,编译后会生成lua和luac的解释器和编译器。
- Lua源代码阅读顺序 -
Reading order
1. 首先阅读外围的库是如何实现功能扩展的,熟悉Lua的公开API,包括如何运用API进行开发。
2. 阅读API的实现,包括内部模块的实现。公开的API实际上是对内部模块的封装。
3. 了解Lua虚拟机的实现,熟悉Lua的执行过程。
4. 理解函数调用,string、table等是如何实现的。
5. 最后可以去了解paser等编译相关的部分,和垃圾回收。
- Lua C API -
Lua C API
先了解一个暴露给用户的数据类型lua_State。它表示一个Lua程序的执行状态,在官方文档中,它指代一个Lua的线程。每个线程拥有自己独立的数据栈和函数调用链,还有独立的钩子和错误处理。它是一组Lua程序的执行状态机,所有的Lua C API都是围绕这个状态机进行的,比如数据入栈出栈,执行栈顶的函数,继续上次被中断的执行过程等。Lua的线程并不是系统线程,所以不会为每一条Lua线程创建独立的系统堆栈,而是利用自己维护的线程栈,内存开销也就远小于系统线程。实际上Lua由于不同线程都同享了一个叫global_State的对象,而很难做到真正意义上的并发。这个global_State在lstate.h中定义,它才是真正的Lua虚拟机,不过被隐藏起来了。
struct lua_State
{
CommonHeader; unsigned short nci; /* number of items in 'ci' list */
lu_byte status;
StkId top; /* first free slot in the stack */
global_State *l_G;
CallInfo *ci; /* call info for current function */
const Instruction *oldpc; /* last pc traced */
StkId stack_last; /* last free slot in the stack */
StkId stack; /* stack base */
UpVal *openupval; /* list of open upvalues in this stack */
GCObject *gclist; struct lua_State *twups; /* list of threads with open upvalues */
struct lua_longjmp *errorJmp; /* current error recover point */
CallInfo base_ci; /* CallInfo for first level (C calling Lua) */
volatile lua_Hook hook; ptrdiff_t errfunc; /* current error handling function (stack index) */
int stacksize;
int basehookcount;
int hookcount;
unsigned short nny; /* number of non-yieldable calls in stack */
unsigned short nCcalls; /* number of nested C calls */
l_signalT hookmask;
lu_byte allowhook;
};
创建/关闭虚拟机
▪ lua_Alloc定义了一个Lua的内存管理函数。
▪ lua_newstate创建虚拟机,需要传入一个内存管理函数作为参数。
▪ lua_close是关闭虚拟机。
▪ lua_newthread()是创建Lua线程。
一般情况下,我们创建虚拟机用的更多的是luaL_newstate,它实际上对lua_newstate的封装,传入一个用C标准库中的函数实现的默认的内存管理器。这个内存管理器提供了内存块的原始大小、重新分配后的大小和内存块的指针,然后便可以实现内存的重新分配或释放。
typedef void * (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize);
LUA_API lua_State *(lua_newstate) (lua_Alloc f, void *ud);
LUA_API void (lua_close) (lua_State *L);
LUA_API lua_State *(lua_newthread) (lua_State *L);
加载/执行Lua
这几个是加载、执行Lua的指令或者文件的API,第一张图里的是加载脚本字符串的函数,这里还定义了一个宏,完成加载并执行脚本字符串。第二张图的函数也是类似的,这是加载和执行脚本文件的函数。
LUALIB_API int (luaL_loadbufferx) (lua_State *L, const char *buff, size_t sz, const char *name, const char *mode);
LUALIB_API int (luaL_loadstring) (lua_State *L, const char *s);
#define luaL_dostring(L, s)
(luaL_loadstring(L, s) || lua_pcall(L, 0, LUA_MULTRET, 0))
LUALIB_API int (luaL_loadfilex) (lua_State *L, const char *filename, const char *mode);
#define luaL_loadfile(L,f)luaL_loadfilex(L,f,NULL)
#define luaL_dofile(L, fn)
(luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))
堆栈的基本操作
说到Lua就不得不说Lua的堆栈,Lua与C的交互是通过堆栈来进行的,栈是一种先进后出的数据结构。在Lua的堆栈里,如果有N个元素,从栈底向上是用1 ~ N来进行索引,从栈顶向下是用-1 ~ -N来进行索引的。记住1指的是栈底,-1指的是栈顶就行。而堆栈里的每个元素可以是任意复杂的Lua数据类型,包括数字、布尔值、字符串、表和函数等。如果堆栈中没有元素即会包含一个空的数据类型。
如果有4个元素分别入栈,那第一个入栈的元素就是在栈底,索引为1或-4,最后一个入栈的在栈顶,索引为4或-1。
简单了解这几个函数的作用:
1. lua_absindex()是获取指定索引对应的绝对索引值,比如堆栈里有4个元素,传入的索引值是-3,那返回的是2。它们对应的是同一个元素。
2. lua_gettop()是获取栈顶的正数索引,也就是栈的长度。
3. lua_settop()是将栈顶的索引设为指定的索引值。如果新的栈顶比原来的大,则多出来那部分的元素将被设为nil,如果指定索引为0,即把所有元素清除。
4. lua_pushvalue()是复制指定索引对应的元素并压到栈顶。
5. lua_rotate()是把栈上的其中两部分元素进行互换。
6. lua_copy()是复制一个值到一个有效索引处,并覆盖原有的值。
7. lua_checkstack()是检查堆栈里是否有至少n个额外的空位,返回失败的原因一般有两种,一种是扩展后堆栈的比最大尺寸还要大,另一种是扩展堆栈失败。在对堆栈进行复杂操作之前一定要记得执行checkstack,以确保有足够的空位进行操作。
8. lua_xmove()从一个线程的栈弹出n个元素并压入另一个线程的栈上。
LUA_API int (lua_absindex) (lua_State *L, int idx);
LUA_API int (lua_gettop) (lua_State *L);
LUA_API void (lua_settop) (lua_State *L, int idx);
LUA_API void (lua_pushvalue) (lua_State *L, int idx);
LUA_API void (lua_rotate) (lua_State *L, int idx, int n);
LUA_API void (lua_copy) (lua_State *L, int fromidx, int toidx);
LUA_API int (lua_checkstack) (lua_State *L, int n);
LUA_API void (lua_xmove) (lua_State *from, lua_State *to, int n);
C和堆栈的数据交互
这部分的函数比较好理解,第一部分,这些都是将一些数据类型和线程压栈,其中包括数字、布尔值、字符串和c的函数闭包等。 第二部分的函数主要是用于判断指定索引对应元素的类型,以及将栈里的元素转化为对应的数据类型。
LUA_API void (lua_pushnil) (lua_State *L);
LUA_API void (lua_pushnumber) (lua_State *L, lua_Number n);
LUA_API void (lua_pushinteger) (lua_State *L, lua_Integer n);
LUA_API const char *(lua_pushlstring) (lua_State *L, const char *s, size_t len);
LUA_API const char *(lua_pushstring) (lua_State *L, const char *s);
LUA_API const char *(lua_pushvfstring) (lua_State *L, const char *fmt, va_list argp);
LUA_API const char *(lua_pushfstring) (lua_State *L, const char *fmt, ...);
LUA_API void (lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n);
LUA_API void (lua_pushboolean) (lua_State *L, int b);
LUA_API void (lua_pushlightuserdata) (lua_State *L, void *p);
LUA_API int (lua_pushthread) (lua_State *L);
LUA_API int (lua_isnumber) (lua_State *L, int idx);
LUA_API int (lua_isstring) (lua_State *L, int idx);
LUA_API int (lua_iscfunction) (lua_State *L, int idx);
LUA_API int (lua_isinteger) (lua_State *L, int idx);
LUA_API int (lua_isuserdata) (lua_State *L, int idx);
LUA_API int (lua_type) (lua_State *L, int idx);
LUA_API const char *(lua_typename) (lua_State *L, int tp);
LUA_API lua_Number (lua_tonumberx) (lua_State *L, int idx, int *isnum);
LUA_API lua_Integer (lua_tointegerx) (lua_State *L, int idx, int *isnum);
LUA_API int (lua_toboolean) (lua_State *L, int idx);
LUA_API const char *(lua_tolstring) (lua_State *L, int idx, size_t *len);
LUA_API size_t (lua_rawlen) (lua_State *L, int idx);
LUA_API lua_CFunction (lua_tocfunction) (lua_State *L, int idx);
LUA_API void *(lua_touserdata) (lua_State *L, int idx);
LUA_API lua_State *(lua_tothread) (lua_State *L, int idx);
LUA_API const void *(lua_topointer) (lua_State *L, int idx);
Lua和栈的数据交互
第一部分函数是从lua到堆栈,主要是一些取值函数。
1. lua_getglobal(),获取Lua的一个全局变量,并把它压栈。
2. lua_gettable(),这个一个根据key获取table的value的函数。索引指向一个table,栈顶的元素作为key,把key对应的value压栈。
3. lua_getfield(),索引指向的是一个table,k是作为key,把key对应的value压栈。跟gettable类似,只不过是取key的方法不一样。
4. lua_geti()跟getfield类似,这里的key是数字。
5. lua_createtable()创建一个table,narr是数组部分长度,nrec是哈希表部分长度。新建的table会压栈。 第二部分函数是从堆栈到lua,主要是一些赋值函数。
6. lua_setglobal(),这是一个设置全局变量的函数。从栈顶弹出一个值,将其作为全局变量name的新值。
7. lua_settable(),这是一个给table赋值的函数。索引指向一个table,栈顶的元素,即-1,指向的是value,栈顶之下的元素,即-2,指向的是key。这个函数执行完之后,键值两个元素都会出栈。
8. lua_setfield(),跟settable类似,索引指向一个table,k指向是key,栈顶的元素作为value,进行赋值操作。
9. lua_seti(),跟setfield类似。
LUA_API int (lua_getglobal) (lua_State *L, const char *name);
LUA_API int (lua_gettable) (lua_State *L, int idx);
LUA_API int (lua_getfield) (lua_State *L, int idx, const char *k);
LUA_API int (lua_geti) (lua_State *L, int idx, lua_Integer n);
LUA_API int (lua_rawget) (lua_State *L, int idx);
LUA_API int (lua_rawgeti) (lua_State *L, int idx, lua_Integer n);
LUA_API int (lua_rawgetp) (lua_State *L, int idx, const void *p);
LUA_API void (lua_createtable) (lua_State *L, int narr, int nrec);
LUA_API void *(lua_newuserdata) (lua_State *L, size_t sz);
LUA_API int (lua_getmetatable) (lua_State *L, int objindex);
LUA_API int (lua_getuservalue) (lua_State *L, int idx);
LUA_API void (lua_setglobal) (lua_State *L, const char *name);
LUA_API void (lua_settable) (lua_State *L, int idx);
LUA_API void (lua_setfield) (lua_State *L, int idx, const char *k);
LUA_API void (lua_seti) (lua_State *L, int idx, lua_Integer n);
LUA_API void (lua_rawset) (lua_State *L, int idx);
LUA_API void (lua_rawseti) (lua_State *L, int idx, lua_Integer n);
LUA_API void (lua_rawsetp) (lua_State *L, int idx, const void *p);
LUA_API int (lua_setmetatable) (lua_State *L, int objindex);
LUA_API void (lua_setuservalue) (lua_State *L, int idx);
函数调用
这几个函数是用于调用Lua函数,其中n是函数的参数个数,r是函数的返回值。调用的参数位于栈顶,Lua函数位于参数的下方,索引是-n -1。函数调用成功后,返回值将压入栈顶。
LUA_API void (lua_callk) (lua_State *L, int nargs, int nresults, lua_KContext ctx, lua_KFunction k);
#define lua_call(L,n,r)lua_callk(L, (n), (r), 0, NULL)
LUA_API int (lua_pcallk) (lua_State *L, int nargs, int nresults, int errfunc, lua_KContext ctx, lua_KFunction k);
#define lua_pcall(L,n,r,f)lua_pcallk(L, (n), (r), (f), 0, NULL)
LUA_API int (lua_load) (lua_State *L, lua_Reader reader, void *dt, const char *chunkname, const char *mode);
LUA_API int (lua_dump) (lua_State *L, lua_Writer writer, void *data, int strip);
函数调用分为受保护调用和不受保护调用,带p的是受保护调用。这里我们简单讲一下这个pcall的处理模式,它实际上是通过内部的一个luaD_pcall来实现的。这里我们可以看出pcall的处理模式:用C的堆栈保存状态,然后执行,如果执行出错的话就可以恢复原来的状态。
int luaD_pcall (lua_State *L, Pfunc func, void *u,
ptrdiff_t old_top, ptrdiff_t ef)
{
int status;
CallInfo *old_ci = L->ci;
lu_byte old_allowhooks = L->allowhook;
unsigned short old_nny = L->nny; ptrdiff_t old_errfunc = L->errfunc;
L->errfunc = ef;
status = luaD_rawrunprotected(L, func, u);
if (status != LUA_OK)
{
/* an error occurred? */
StkId oldtop = restorestack(L, old_top);
luaF_close(L, oldtop); /* close possible pending closures */
seterrorobj(L, status, oldtop);
L->ci = old_ci;
L->allowhook = old_allowhooks;
L->nny = old_nny;
luaD_shrinkstack(L);
}
L->errfunc = old_errfunc;
return status;
}
Demo
这是一个c和lua交互的一个demo,主要流程是先创建一个虚拟机,然后用openlibs打开内嵌库,然后在Lua虚拟机注册一个C函数,这样Lua就能使用这个testCFunc函数了。接着执行一个lua脚本,然后获取一个全局变量testLFunc,这是一个函数。此时这个函数位于栈顶。然后压入两个参数,再用pcall调用Lua。调用成功后,返回值位于栈顶。
--test.lua
function testLFunc(a, b)
print("testLFunc")
local c = testCFunc(a, b)
print("lua print: "..c)
return c
end
int testCFunc(lua_State *L)
{
printf("testCFunc ");
lua_Number a = luaL_checknumber(L, 1);
lua_Number b = luaL_checknumber(L, 2);
char str[20];
sprintf(str, "recv %.0f %.0f", a, b);
lua_pushstring(L, str);
return 1;
}
int main()
{
lua_State *L = luaL_newstate();
luaL_openlibs(L);
lua_register(L, "testCFunc", testCFunc);
luaL_dofile(L, "test.lua");
lua_getglobal(L, "testLFunc");
lua_pushnumber(L, 5);
lua_pushnumber(L, 10);
if (lua_pcall(L, 2, 1, 0))
{
printf("pcall error!
");
}
printf("C print: %s ", lua_tostring(L, -1));
return 0;
}
输出结果:
testLFunc
testCFunc
lua print: recv 5 10
C print: recv 5 10
看完以上的内容,对Lua语言是否了解得更全面了?如果对本期内容有疑问或者建议~可以在评论区留言讨论哦~
敬请期待下一期: 源码赏析:Lua VM
西山居质量Test+团队是金山西山居工作室的专业质量团队,对于游戏领域有超过18年的测试经验积累,专注于为手游、端游提供各类自动化测试等全方位质量保障。
以上是关于源码赏析带你零基础掌握Lua!的主要内容,如果未能解决你的问题,请参考以下文章