risc-v中文社区

 找回密码
 立即注册
查看: 2189|回复: 0

Lua源码分析 - 虚拟机篇 - 语义解析之loadfile文件读取(14)

[复制链接]

347

主题

564

帖子

2237

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
2237
发表于 2022-6-20 15:56:09 | 显示全部楼层 |阅读模式
目录

一、虚拟机 - 从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);
}

————————————————
版权声明:本文为CSDN博主「老码农zhuli」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/initphp/article/details/104428729

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



Archiver|手机版|小黑屋|risc-v中文社区

GMT+8, 2024-4-24 01:53 , Processed in 0.014145 second(s), 17 queries .

risc-v中文社区论坛 官方网站

Copyright © 2018-2021, risc-v open source

快速回复 返回顶部 返回列表