目录
一、虚拟机 - 从Lua的例子入手
二、虚拟机 - 文件读取ZIO
三、虚拟机 - 文件读取主流程
四、虚拟机 - loadfile、dofile、load和require等函数实现
前几章主要讲解了Lua的主流程和Lua的扩展库实现机制。本章开始讲解Lua虚拟机部分的实现机制。
一、虚拟机 - 从Lua的例子入手
我们通过一个Lua的例子,来看一下Lua脚本的执行。
我们首先顶一个一个lua文件,test.lua,里面是一段协程的简单示例脚本。
-- 定义一个协程回调函数
function f ()
print('--启动程序--');
print('--中断程序--');
coroutine.yield();
print('--恢复程序--');
end
-- 为这个函数新建一个协程
co = coroutine.create(f);
print('--准备启动--');
coroutine.resume(co);
print('--准备恢复--');
然后我们通过Lua命令,执行一条命令行语句。
11111111:Togo zhuli$ lua test.lua
--准备启动--
--启动程序--
--中断程序--
--准备恢复--
--恢复程序--
从上面的例子,我们大概就能看出,Lua语言要执行一个Lua的脚本文件,则核心要做四件事情:文件读取、解析成语法Token、编译成二进制操作码、执行二进制操作码
Lua是解释型语言,通过对Lua的语言进行语法解析,然后生成二进制字节码,然后转由C语言进行执行操作。编译型语言,则会进行编译后生成机器码,直接由机器进行执行即可,执行效率会比较高。
二、虚拟机 - 文件读取ZIO
Lua文件读取的操作主要在lzio.c文件中。Zio结构主要存储文件流读取的状态信息。
- n:多少未读取的字符
- p:buf的读取指针地址
- reader:文件读取方法
- data:buf指针地址
- L:当前线程栈地址
struct Zio {
size_t n; /* 多少未读取 bytes still unread */
const char *p; /* buf的读取指针地址 current position in buffer */
lua_Reader reader; /* 文件读取方法 reader function */
void *data; /* buf指针地址 buf additional data */
lua_State *L; /* 当前线程栈地址 Lua state (for reader) */
};
1. 我们通过luaZ_init函数进行ZIO数据结构的初始化,luaZ_fill函数进行文件内容的读取和buf填充
/**
* 读取文件
*/
int luaZ_fill (ZIO *z) {
size_t size;
lua_State *L = z->L;
const char *buff;
lua_unlock(L);
buff = z->reader(L, z->data, &size); //文件读取,返回size 大小 getF方法
lua_lock(L);
if (buff == NULL || size == 0)
return EOZ;
z->n = size - 1; /* discount char being returned */
z->p = buff; //起始地址指向buf开始地址
return cast_uchar(*(z->p++));
}
/**
* ZIO初始化
*/
void luaZ_init (lua_State *L, ZIO *z, lua_Reader reader, void *data) {
z->L = L;
z->reader = reader;
z->data = data;
z->n = 0;
z->p = NULL;
}
2. 具体的reader方法,在lauxlib.c文件中getF函数。主要通过fread函数直接读取lua文件数据到buf区域。
/**
* 文件读取方法
*/
static const char *getF (lua_State *L, void *ud, size_t *size) {
LoadF *lf = (LoadF *)ud;
(void)L; /* not used */
if (lf->n > 0) { /* are there pre-read characters to be read? */
*size = lf->n; /* return them (chars already in buffer) */
lf->n = 0; /* no more pre-read characters */
}
else { /* read a block from file */
/* 'fread' can return > 0 *and* set the EOF flag. If next call to
'getF' called 'fread', it might still wait for user input.
The next check avoids this problem. */
if (feof(lf->f)) return NULL;
*size = fread(lf->buff, 1, sizeof(lf->buff), lf->f); /* read block */
}
return lf->buff;
}
3. 在lzio.h文件中,我们定义了zgetc。从上面getF函数我们知道,Lua首先将文件流读取到内容buf中,然后通过zgetc函数,逐个读取字符。
该函数在语法树解析的时候会用得比较频繁。语法解析原理:通过逐个读取文件流数据,通过关键字TOKEN,然后分割不同的代码语句statement。
#define zgetc(z) (((z)->n--)>0 ? cast_uchar(*(z)->p++) : luaZ_fill(z))
三、虚拟机 - 文件读取主流程
Lua文件的读取执行流程,从pmain方法的dofile函数开始。整体来说,文件的加载,底下主要调用的是lapi.c文件中的lua_load函数。接下去,我们先看一下主流程:
1. pmain函数中,执行完openlibs,就开始执行dofile操作,针对文件进行加载、解析和执行操作
static int pmain (lua_State *L) {
//.....打开扩展库
luaL_openlibs(L); /* open standard libraries */
//...加载文件
else dofile(L, NULL); /* executes stdin as a file */
}
static int dofile (lua_State *L, const char *name) {
return dochunk(L, luaL_loadfile(L, name));
}
static int dochunk (lua_State *L, int status) {
if (status == LUA_OK) status = docall(L, 0, 0); //docall执行文件lua源码
return report(L, status);
}
2. 然后在luaL_loadfilex方法中调用lua_load,所以lua_load函数为文件加载的核心函数。getF为文件读取z->reader的核心函数
/**
* 加载Lua文件
*/
LUALIB_API int luaL_loadfilex (lua_State *L, const char *filename, const char *mode) {
//.....
/* 加载文件,文件和解析文件;如果多个文件嵌套,则嵌套加载 */
status = lua_load(L, getF, &lf, lua_tostring(L, -1), mode);
//.....
}
3. lua_load函数通过luaD_protectedparser保护方式来进行文件读取和语法树解析。luaD_protectedparser内部调用的是luaD_pcall方法。我们知道luaD_pcall方法有异常保护功能,pmain方法就是通过luaD_pcall来调用的。
/**
* 文件解析函数(保护方式调用)
* 调用:luaD_pcall方法
*/
int luaD_protectedparser (lua_State *L, ZIO *z, const char *name, const char *mode) {
status = luaD_pcall(L, f_parser, &p, savestack(L, L->top), L->errfunc);
}
4. luaD_pcall执行的时候回调了f_parser函数。
static void f_parser (lua_State *L, void *ud) {
//文本类型,使用luaY_parser调用
checkmode(L, p->mode, "text");
cl = luaY_parser(L, p->z, &p->buff, &p->dyd, p->name, c);
}
5. 真正执行语法树解析的是luaY_parser函数。该函数内部主要用于组装:语法状态结构:LexState和方法状态结构:FuncState(下一节讲解语法树解析原理)。该函数最后执行mainfunc方法,用于执行语法树的解析工作。
LClosure *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, Dyndata *dyd, const char *name, int firstchar) {
LexState lexstate;
FuncState funcstate;
mainfunc(&lexstate, &funcstate);
}
6. mainfunc函数中,有两个函数比较关键。luaX_next:主要用于语法TOKEN的分割,是语法分割器;statlist:主要根据luaX_next分割器分割出来的TOKEN,组装成语法块语句statement,最后将语句逐个组装成语法树。
static void mainfunc (LexState *ls, FuncState *fs) {
luaX_next(ls); /* 读取第一个token read first token */
statlist(ls); /* 语法树遍历解析 parse main body */
}
7. 这里我们会有疑问,Lua是如何读取文件流上的内容的。luaX_next函数主要作用是Token分割,真正执行分割的函数是llex,该函数是一个for循环,循环从文件buf中去读取数据。for循环中,会调用next函数,会逐个读取字符。
#define next(ls) (ls->current = zgetc(ls->z))
四、虚拟机 - loadfile、dofile、load和require等函数实现
我们在lbaselib.c Lua的基础库文件中,有几个常用的基础函数,loadfile、dofile、load和require等。这里讲解一下这几个函数的区别。
- loadfile:只会加载文件,编译代码,不会运行文件里的代码
- dofile:会加载文件并执行文件,对于相同的文件每次都会执行
- load:从字符串中读取代码
- require:加载文件,如果已经加载过了,则不加载
/**
* 加载文件并执行代码
* lua_callk 通过该方法执行Lua文件源码
*/
static int luaB_dofile (lua_State *L) {
const char *fname = luaL_optstring(L, 1, NULL);
lua_settop(L, 1);
if (luaL_loadfile(L, fname) != LUA_OK)
return lua_error(L);
lua_callk(L, 0, LUA_MULTRET, 0, dofilecont);
return dofilecont(L, 0, 0);
}
/**
* luaL_loadfilex函数底层调用lua_load方法
* 只会加载文件,编译代码,不会运行文件里的代码
*/
static int luaB_loadfile (lua_State *L) {
const char *fname = luaL_optstring(L, 1, NULL);
const char *mode = luaL_optstring(L, 2, NULL);
int env = (!lua_isnone(L, 3) ? 3 : 0); /* 'env' index or 0 if no 'env' */
int status = luaL_loadfilex(L, fname, mode);
return load_aux(L, status, env);
}