目录

一、虚拟机 - 从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函数。接下去,我们先看一下主流程:

luacom 读取excel lua读取文件_Lua

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);
}