以前游戏里用到过lua,主要是做配置,所以专门看过《lua程序设计》第二版。后面用lua实现了一个功能,大概几千行代码,当时感觉到写起来方便,调试维护确实不易。听说lua只有2万行代码,便实现了主流动态语言的大部分功能,于是想学习一下。
最近开始看了一点lua(5.1.4版本)的源代码,主要是lua解释器和内嵌库(不含debug库和string库的模式匹配)。
文件说明
源文件 | 说明 |
lua.c | lua解释器 |
lualib.c | 库管理 |
lbaselib.c | 基础库 |
lcolib.c | 协程库 |
ldblib.c | debug库 |
liolib.c | io库 |
lmathlib.c | 数学库 |
loadlib.c | 加载库 |
loslib.c | os库 |
lstrlib.c | 字符串库 |
ltablib.c | 表处理库 |
lua解释器
解释器主要是将代码段用 luaL_loadbuffer() 加载,或将文件用 luaL_loadfile() 加载,然后用 lua_pcall() 去执行。交互模式下执行的语句如果有返回值,会用 print() 来输出。
在调用 lua_pcall() 前先执行了 signal(SIGINT, laction);
:
static void lstop (lua_State *L, lua_Debug *ar) {
lua_sethook(L, NULL, 0, 0);
luaL_error(L, "interrupted!");
}
static void laction (int i) {
/* if another SIGINT happens before lstop, terminate process (default action) */
signal(i, SIG_DFL);
lua_sethook(globalL, lstop, LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT, 1);
}
signal(SIGINT, laction);
lua_pcall(L, narg, (clear ? 0 : LUA_MULTRET), base);
signal(SIGINT, SIG_DFL);
这样在执行语句或文件的过程中,如果收到 SIGINT,会调用 lstop(),结束语句或文件的执行。然后再恢复 SIGINT 的默认处理行为,这时如果再按 ctrl c,解释器便会退出。
用 luaL_openlibs(L); 打开所有库后,是可以清空栈的,因为栈上的table,都有变量引用着,不会被回收。
我用libreadline库为解释器添加了库成员的table补全功能。
数学库
数学库的实现比较简单,大部分函数基本是对c函数的转调,可以从这里入手。
所有注册到lua中的函数都具有相同的原型 typedef int (*lua_CFunction)(lua_State *L);
。
当lua调用c函数时,c函数从栈中获取函数参数,并将结果压入栈中。每个函数都有自己局部的栈,第一个参数总是这个局部栈的索引1。c函数返回一个整数,表示其压入栈中返回值数量,第一个返回值先压入栈。c函数无须在压入结果前清空栈,函数返回后,lua会自动删除栈中返回结果之下的内容。
多个c函数用 luaL_register() 注册为lua的库。
luaopen_math() 返回值表示压入栈的项目数量,但这个返回值并没有实际用到,只是为了遵循惯例而已。
可以注意一下 math_random() 是如何处理参数数目不同的情况。
也能了解直接设置表成员值的方法:
luaL_register(L, LUA_MATHLIBNAME, mathlib);
lua_pushnumber(L, PI);
lua_setfield(L, -2, "pi");
os库
os_date() 那里注意了解一下 luaL_Buffer 的使用,luaL_Buffer 是往栈里压入不定数量字符或字符串的一个辅助结构。
在往buffer里添加字符或字符串时,luaL_Buffer 可能会往栈里压入多层,但最后调用 luaL_pushresult() 时,luaL_Buffer 会将所有已压入栈的以及未压入栈的字符和字符串组合成一个字符串,最终只往栈里压入一个字符串。
os_date() 对每个转换说明符都调用了一次 strftime(),通过 luaL_Buffer 将所有结果组合起来,是因为没法确定最后结果字符串的长度,所以只好分开转换,最后再组合在一起。
table库
sort() 用的就是递归的快排。排序时table和数组颇为类似,但是代码行数却很多。因为lua里大部分操作只能与栈进行,比如交换table中两个成员的值,要先将两个成员入栈再出栈,实现同样的算法,由于额外多出的操作,lua的效率肯定是不如c的。
string库
createmetatable() 为字符串类型创建了一个元表,元表的 __index 指向string。
设s为一个字符串 s=‘123’,s:len() 等价于 s.len(s),s没有len成员,就去找s元表的 __index 也就是string,最后是调用 string.len(s)。可参考《Lua的面向对象》进行理解。
load库
主要是引入了两个全局函数:require() 和 module()。
module() 比较有意思,lua文件中在执行 module(“m”) 之后,后面的非local变量都是从表m中去引用了。如果在这个模块里想调用 print 输出调试信息怎么办呢?一个简单的方法是:
local print=print
module("m")
或者可以用:
local _G=_G
module("m")
那么 _G.print 也是可以用的。
在lua中,每个函数都有一个环境,它是一个lua表,用于存储函数中使用的全局变量和外部变量。在函数的定义体内,默认使用其定义处的环境。
module(“m”) 创建了一个全局的表m,然后将当前调用 module() 的函数(当前lua文件,或交互模式中的当前代码段)的环境变量设置为m,所以就实现了上面的效果。
新创建的函数会使用当前环境(c函数也是这样),lua文件的环境则是 _G,所以不单独设置环境的lua函数能直接使用全局变量,新创建的变量也是全局的。
io库
文件指针 FILE* 是lua的一个userdata,将所有该类型的userdata的元表都设置为一个共同的表,这里暂且称为mt。令mt.__index = mt,并将这些函数注册到mt中:
static const luaL_Reg flib[] = {
{"close", io_close},
{"flush", f_flush},
{"lines", f_lines},
{"read", f_read},
{"seek", f_seek},
{"setvbuf", f_setvbuf},
{"write", f_write},
{"__gc", f_gc},
{"__tostring", f_tostring},
{NULL, NULL}
};
这样关闭一个文件变量f,f:close(),就变成了 mt.close(f) 了。
文件有3种类型,普通文件,标准输入输出错误,popen() 打开的文件。它们的区别是关闭方法不同,第一种用 fclose() 关闭,第二种不需要关闭,第三种用 pclose() 关闭。
这是通过让文件类型的userdata使用不同环境实现的,关闭文件时寻找userdata的环境的__close指向的函数,用它来关闭文件。
基础库
这里是一些全局函数的实现。
luaopen_base() 命名了 _G:
lua_pushvalue(L, LUA_GLOBALSINDEX);
lua_setglobal(L, "_G");
开始global表没有名 字,现在将其命名为 _G,而 lua_setglobal() 又会将 _G 设为global表的成员,所以 _G._G 就指向了 _G 自己。
协程库
基本只是转调lua api而已,只能看个热闹。
本来是包含在 lbaselib.c 里的,我给单独拿出来了。