共计 25841 个字符,预计需要花费 65 分钟才能阅读完成。
前言
在本篇,咱们正式进入到 Lua 解释器的开发阶段(这是一个遵循 Lua 5.3 规范的我的项目)。本篇并不间接接入到设计和实现语法分析器和词法分析器的阶段,而是先设计和实现 Lua 虚拟机的根底数据结构(包含 Lua 最根本的数据结构,如根本数据类型、示意虚拟机状态的 global_State 和 lua_State 构造、在函数调用中表演重要角色的 CallInfo 构造等)以及设计和实现基于栈的 C 函数调用流程。这些都是了解前面虚拟机运作的根底。因为这是一个仿造我的项目,为了和官网版本做辨别,就称之为 dummylua,前面要称说本我的项目时,一律用 dummylua 来示意。
本篇将分为几个局部:首先介绍工程目录构造的组织,以及为什么要这样组织,每个目录别离蕴含哪些文件,这些文件别离蕴含哪些内容;其次是着手进行 Lua 根本类型的设计与实现,实现示意 Lua 虚拟机状态的 global_State 和 lua_State 构造,以及用于函数调用的 CallInfo 构造;接着是设计咱们在应用层,要实现 C 函数,在栈中调用流程的 API,并且构建从虚拟机状态初始化到函数实现调用的逻辑图,论述这个过程;最初通过编写代码,将所有的流程实现。第一局部的性能曾经实现开发和测试(Ubuntu、Mac 和 Windows 平台)。
获取源码能够查看:
https://github.com/Manistein/…
目录构造
在开始介绍我的项目的目录构造之前,咱们无妨先回顾一下 Lua 运作的两种基本模式。
一种是创立 Lua 虚拟机,间接加载脚本并且间接运行,其遵循如下流程:
- 创立 Lua 虚拟机状态实例
- 加载规范库
- 加载脚本,通过词法分析器 Lexer 和语法分析器 Parser 将脚本编译成 Lua 虚拟机可能辨认的 Opcodes,并存储在虚拟机状态实例中
- 运行虚拟机,对虚拟机状态实例中的 Opcodes 进行执行
还有一种则是,事后将脚本编译,而后将内存中的指令信息,Dump 到文件中,以 Bytecode 的模式存在,当前要运行的时候,间接加载 Dump 文件的 Bytecode 并且间接运行:
- 创立 Lua 虚拟机状态实例
- 加载规范库
- 加载脚本,通过词法分析器 Lexer 和语法分析器 Parser 将脚本编译成 Lua 虚拟机可能辨认的 Opcodes,并存储在虚拟机状态实例中
- 将虚拟机指令 Dump 成二进制文件,以 Bytecode 的模式保留
- 在未来某个时刻,运行 Lua 虚拟机,并加载 Dump 后的文件,间接通过 Dump 数据,将指令构造还原回来,保留在虚拟机状态实例中
- 运行虚拟机,对虚拟机状态实例中的 Opcodes 进行执行
这两种形式,前者从虚拟机创立到加载脚本,再到运行零打碎敲。后者须要事后将 Lua 脚本编译成 Bytecode,而后要应用的时候再加载运行,运行时省去了编译流程,比前者更快。不过当初 Lua 间接加载脚本并运行曾经足够快了,除非对性能有极其刻薄的要求,否则前者曾经可能满足咱们的日常须要了。上面援用一张《The Lua Architecture》(Reference 1)中的一张图,来展现一下前面一种形式的流程。
从上图,咱们能够理解到一个流程,就是咱们要运行 Lua 脚本,首先要创立 Lua 解释器(因为 Lua 是采纳纯 C 来写的工程,因而函数和数据是拆散的,这里其实也只是创立一个 Lua 虚拟机状态实例,也就是前面咱们要介绍的 lua_State 构造和 global_State 构造),而后通过编译器(Lexer 和 Parser)将脚本编译成虚拟机可能辨认的指令(Opcodes),再交给虚拟机执行。因而,咱们能够将编译和运行宰割开来,他们独特应用的局部也独自抽离进去,于是咱们的目录构造能够按如下所示的形式组织:
+ 3rd/ #援用的第三方库均搁置在这里
+ bin/ #编译生成的二进制文件搁置在这里
+ clib/ #内部要在 c 层应用 Lua 的 C API,那么只能调用 clib 里提供的接口,而不能调用其余外部接口
+ common/ #vm 和 compiler 独特应用的构造、接口均搁置在这里
+ compiler/ #编译器相干的局部搁置在这里
+ test/ #测试用例全副搁置在这里
+ vm/ #虚拟机相干的局部搁置在这里
main.c
makefile
咱们有理由置信,目录组织也是架构的一部分,下面附上了目录阐明,可能清晰阐明他们的分类和作用。我想构建的逻辑档次图如下所示:
3rd 和 common 作为 vm 和 compiler 的根底模块而存在,内部在应用 c 接口的时候,只能通过 clib 里的辅助库来进行,以暗藏不必要裸露的细节。在定下目录构造当前,接下来将展现不同的文件别离有哪些文件。
目录组织实现当前,接下来确定有哪些文件了,我将文件内容展现到上面局部:
+ 3rd/ #援用的第三方库均搁置在这里
+ bin/ #编译生成的二进制文件搁置在这里
~ clib/ #内部要在 c 层应用 Lua 的 C API,那么只能调用 clib 里提供的接口,而不能调用其余外部接口
luaaux.h #供内部应用的辅助库
luaaux.c
~ common/ #vm 和 compiler 独特应用的构造、接口均搁置在这里
lua.h #提供 lua 根本类型的定义,错误码定义,全我的项目都可能用到的宏均会搁置在这里
luamem.h #lua 内存分配器
luamem.c
luaobject.h #lua 根本类型
luaobject.c
luastate.h #虚拟机状态构造,以及对其相干操作的接口均搁置于此
luastate.c
+ compiler/ #编译器相干的局部搁置在这里
+ test/ #测试用例全副搁置在这里
~ vm/ #虚拟机相干的局部搁置在这里
luado.h #函数调用相干的接口均搁置于此
luado.c
main.c
makefile
下面展现了咱们本局部要实现的局部,后续开发会陆续增加新的文件,前面章节也会陆续援用这个片段。到当初为止,咱们的目录构造就介绍完了,前面将介绍根本数据结构。
根本数据结构
根本类型
Lua 的根本类型,包含 lua_Integer、lua_Number、lu_byte、lua_CFunction 等,当然最典型的则是其可能代表任何根本类型的 TValue 构造。当初咱们将逐个实现这些类型。
首先咱们要实现两个宏,LUA_INTEGER 和 LUA_NUMBER 在 common/lua.h 里:
// common/lua.h
#ifndef lua_h
#define lua_h
static int POINTER_SIZE = sizeof(void*);
#if POINTER_SIZE >= 8
#define LUA_INTEGER long
#define LUA_NUMBER double
#else
#define LUA_INTEGER int
#define LUA_NUMBER float
#endif
#endif
而后在 common/luaobject.h 中退出上面几行代码:
// common/luaobject.h
#ifndef luaobject_h
#define luaobject_h
#include "lua.h"
typedef LUA_INTEGER lua_Integer;
typedef LUA_NUMBER lua_Number;
#endif
此时咱们的 lua_Integer 和 lua_Number 类型就定义完了,这里要先在 common/lua.h 中定义 LUA_INTEGER 的目标是让 LUA_INTEGER 适配 32bit 和 64bit 两种编译环境的逻辑保留在 lua.h 中。适配的目标也是为了前面的 Value 构造的各个字段可能完满对齐。官网实现版本,甚至对编译器类型做了适配,这里咱们只思考 32bit 和 64bit 的状况。
接下来咱们要定义 lu_byte 和 lua_CFunction 两种根本类型,他们的定义如下所示:
// common/luaobject.h
#ifndef luaobject_h
#define luaobject_h
#include "lua.h"
typedef struct lua_State lua_State;
typedef LUA_INTEGER lua_Integer;
typedef LUA_NUMBER lua_Number;
typedef unsigned char lu_byte;
typedef int (*lua_CFunction)(lua_State* L);
#endif
lu_byte 是个 unsigned char 类型,官网将其取名作 lu_byte 兴许是示意 lua unsigned byte 的意思。而 lua_CFunction 基本上就是 Lua 栈中,能被调用的 light c function 的模式了。
在实现了最根本类型的定义后,当初要来定义 Lua 的通用数据类型 Value 和 TValue 了,咱们将这两个数据结构定义在 common/luaobject.h 中:
// common/luaobject.h
#ifndef luaobject_h
#define luaobject_h
#include "lua.h"
...
typedef union lua_Value {
void* p; // light userdata
int b; // boolean: 1 = true, 0 = false
lua_Integer i;
lua_Number n;
lua_CFunction f;
} Value;
typedef struct lua_TValue {
Value value_;
int tt_;
} TValue;
#endif
Value 是一个 union 类型,以上 5 种类型在 32bit 环境下,共用 4 个字节的内存,在 64bit 环境下共用 8 个字节的内存,这样做的目标是为了节约内存,p 指针是用来寄存 light userdata 的,这种值在官网 Lua 中,须要咱们自行治理内存,因为目前咱们没有实现 GC,因而所有的自定义类型被创立进去后,均放在 p 中。TValue 蕴含了 Value 类型的值 value_,以及应用一个 int 变量 tt_示意其类型。咱们能够应用 TValue 来示意任何 Lua 对象。此外 TValue 的类型定义在 common/lua.h 中,如下所示:
// common/lua.h
// basic object type
#define LUA_TNUMBER 1
#define LUA_TLIGHTUSERDATA 2
#define LUA_TBOOLEAN 3
#define LUA_TSTRING 4
#define LUA_TNIL 5
#define LUA_TTABLE 6
#define LUA_TFUNCTION 7
#define LUA_TTHREAD 8
#define LUA_TNONE 9
因为名称都很直观,这里就不加正文了。下面论述的还是个别类型,其中 LUA_TNUMBER、LUA_TSTRING 和 LUA_TFUNCTION 还能够细分,我将细分的类型在 common/luaobject.h 里定义,放在这里更加贴近应用它的接口:
// common/luaobject.h
// lua number type
#define LUA_NUMINT (LUA_TNUMBER | (0 << 4))
#define LUA_NUMFLT (LUA_TNUMBER | (1 << 4))
// lua function type
#define LUA_TLCL (LUA_TFUNCTION | (0 << 4))
#define LUA_TLCF (LUA_TFUNCTION | (1 << 4))
#define LUA_TCCL (LUA_TFUNCTION | (2 << 4))
// string type
#define LUA_LNGSTR (LUA_TSTRING | (0 << 4))
#define LUA_SHRSTR (LUA_TSTRING | (1 << 4))
能够察看到,Lua 数值的大类型定义的值在 1~8 之间,也就是 00012~10002 之间,那么小类型不能占用低四位,只能往高位作文章,因而他们别离的含意为:
- LUA_NUMINT <=> LUA_TNUMBER | (0 << 4) <=> 00012 | 00002 <=> 00012 <=> 1
- LUA_NUMFLT <=> LUA_TNUMBER | (1 << 4) <=> 00012 | 100002 <=> 100012 <=> 17
- LUA_TLCL <=> LUA_TFUNCTION | (0 << 4) <=> 01112 | 00002 <=> 01112 <=> 7
- LUA_TLCF <=> LUA_TFUNCTION | (1 << 4) <=> 01112 | 100002 <=> 101112 <=> 23
- LUA_TCCL <=> LUA_TFUNCTION | (2 << 4) <=> 01112 | 1000002 <=> 1001112 <=> 39
- LUA_LNGSTR <=> LUA_TSTRING | (0 << 4) <=> 01002 | 00002 <=> 01002 <=> 4
- LUA_SHRSTR <=> LUA_TSTRING | (1 << 4) <=> 01002 | 100002 <=> 101002 <=> 20
这些类型值,将被存储在 TValue 的 tt_变量中。到目前为止咱们曾经介绍完了 Lua 根本数据类型。接下来咱们将定义 lua_State 和 global_State。
lua_State、CallInfo 和 global_State
在介绍完 TValue 数据结构当前,接下来要介绍和虚拟机非亲非故的虚拟机状态数据结构 lua_State 和 global_State。如果说,虚拟机指令要在栈上运行,那么这个栈就是保留在 lua_State 这个构造体中。同时咱们的 CallInfo 构造,用于标记函数在栈中的地位,标记调用函数时,它的栈顶位于 lua_State 栈中的哪个地位,同时它还保留要返回多少个返回值的标记。而 global_State 则是蕴含了 lua_State 和一个内存分配器等,透过 global_State 来治理内存和 lua_State 是十分不便的事件,在官网的 Lua 版本中,咱们还须要透过它来治理 GC。当初咱们来定义这些个构造体,将其定义在 common/luastate.h 中:
// common/luastate.h
typedef TValue* StkId;
struct CallInfo {
StkId func; // 被调用函数在栈中的地位
StkId top; // 被调用函数的栈顶地位
int nresult; // 有多少个返回值
int callstatus; // 调用状态
struct CallInfo* next; // 下一个调用
struct CallInfo* previous; // 上一个调用
};
typedef struct lua_State {
StkId stack; // 栈
StkId stack_last; // 从这里开始,栈不能被应用
StkId top; // 栈顶,调用函数时动静扭转
int stack_size; // 栈的整体大小
struct lua_longjmp* errorjmp; // 保护模式中,要用到的构造,当异样抛出时,跳出逻辑
int status; // lua_State 的状态
struct lua_State* next; // 下一个 lua_State,通常创立协程时会产生
struct lua_State* previous;
struct CallInfo base_ci; // 和 lua_State 生命周期统一的函数调用信息
struct CallInfo* ci; // 以后运作的 CallInfo
struct global_State* l_G; // global_State 指针
ptrdiff_t errorfunc; // 谬误函数位于栈的哪个地位
int ncalls; // 进行多少次函数调用
} lua_State;
typedef struct global_State {
struct lua_State* mainthread; // 咱们的 lua_State 其实是 lua thread,某种程度上来说,它也是协程
lua_Alloc frealloc; // 一个能够自定义的内存调配函数
void* ud; // 当咱们自定义内存分配器时,可能要用到这个构造,然而咱们用官网默认的版本
// 因而它始终是 NULL
lua_CFunction panic; // 当调用 LUA_THROW 接口时,如果以后不处于保护模式,那么会间接调用 panic 函数
// panic 函数通常是输入一些要害日志
} global_State;
我将构造的阐明,间接写到了正文里,这些正文可能对构造进行很好的解释和阐明。当初咱们也实现了 CallInfo、lua_State 和 global_State 的定义,接下来进入下一个阶段。
函数调用流程
到目前为止,咱们曾经实现了 Lua 根本数据结构的定义,本篇的指标除了定义这些数据结构,对其中的字段加以阐明之外,还将设计和实现 C 函数在 lua_State 栈中的调用。在开始实现具体细节之前,咱们无妨从应用程序调用的视角来察看这个流程。咱们以如下的代码为例子:
// main.c
#include "clib/luaaux.h"
static int add_op(struct lua_State* L) {int left = luaL_tointeger(L, -2);
int right = luaL_tointeger(L, -1);
luaL_pushinteger(L, left + right);
return 1;
}
int main(int argc, char** argv) {struct lua_State* L = luaL_newstate(); // 创立虚拟机状态实例
luaL_pushcfunction(L, &add_op); // 将要被调用的函数 add_op 入栈
luaL_pushinteger(L, 1); // 参数入栈
luaL_pushinteger(L, 1);
luaL_pcall(L, 2, 1); // 调用 add_op 函数,并将后果 push 到栈中
int result = luaL_tointeger(L, -1); // 实现函数调用,栈顶就是 add_op 放入的后果
printf("result is %d\n", result);
luaL_pop(L); // 后果出栈,保障栈的正确性
printf("final stack size %d\n", luaL_stacksize(L));
luaL_close(L); // 销毁虚拟机状态实例
system("pause");
return 0;
}
正如咱们之前设计的那样,要应用 dummylua 的接口,只能应用 clib 库里的 luaaux.h 里定义的接口,这些接口的结尾一律以 luaL* 示意,L 指代 Lib(官网能调用的接口并非均是以 luaL* 结尾的,这点须要强调一下)。下面一段代码,咱们先创立了一个 lua_State 实例,而后 Push 了要调用的函数和参数,最终调用了这个函数,这个函数最终将后果入栈,并返回给调用者。在摸索代码细节之前,我先绘制几张逻辑图,来形容这个过程,以更好地让大家了解这个逻辑调用过程。
首先咱们调用了 luaL_newstate 接口,用于创立和初始化 lua_State 实例,这个接口不仅仅会创立 lua_State 实例,也会创立一个 global_State 实例,并且初始化它们,global_State 对外部是不可见的,它是 Lua 虚拟机外部应用的一个构造,在实现函数调用当前,咱们能够失去下图的构造:
咱们能够看到,global_State 实例也被一起创立了,并且将 mainthread 指针指向了 lua_State 实例,lua_State 默认就有一个 CallInfo 类型的变量 base_ci,这个 CallInfo 的 func 和 top 指针,指明了一个被调用的函数所能应用的栈的范畴,每当咱们调用一个新的函数时,会从 lua_State 的 stack 中,取出一段作为该函数的栈空间,开始地位是 CallInfo 类型变量 func 指针的下一个地位,而能被应用的栈空间限度个别是 20 个(官网版本由 LUA_MINSTACK 这个宏指定)。此外 lua_State 的 stack 指针,指向了栈起始的地位,而从 stack_last 开始,包含前面的 EXTRA_SPACE 则是不能被应用的空间。lua_State 中的 top 指针,则是在函数调用的过程中,动静扭转的,它标记着正在被调用函数的栈顶地位。另外还要强调的一点,则是 CallInfo 的 func 指针,本质上是指向一个函数实例(base_ci 除外),而这个被 func 指针指向的地位,是不能作为栈顶存在的,也就是说,以后函数在被调用时,如果它是空栈,那么 lua_State 的 top 指针将落在 L ->ci->func 的上一格地位。正如上图所示,当初它是一个空栈。
在实现虚拟机状态实例创立当前,咱们须要往 lua_State 的栈中,Push 咱们心愿被调用的函数,这个函数在咱们应用的例子中,就是 add_op,咱们通过一张图展现,在 luaL_pushcfunction 执行完当前的状态:
目前看起来平平无奇,接下来将两个参数入栈,于是失去函数调用前的栈状态:
在实现参数入栈当前,咱们就须要开始调用 add_op 这个函数了,调用它的形式是执行 luaL_pcall(L, 2, 1)这行代码,这两个参数别离示意,咱们传入两个参数,并且期待一个返回值。函数调用的过程略微有点简单,然而绝对于前面波及到的虚拟机运作,还算是小巫见大巫。要开始函数调用,首先要构建新的 CallInfo 实例,它会指定 add_op 运行时,该函数栈能够应用的范畴同时也蕴含,这个函数被期待返回多少个函数,于是咱们能够失去如下图所示的状态:
从图中咱们能够看到,新创建的 CallInfo 对象归属于 add_op 这个函数,CallInfo 的 func 指针指向了 add_op 这个函数的地位,同时新创建的 CallInfo 对象的 func 和 top 指针限定了 add_op 可能应用的栈空间的范畴。同时咱们也能够察看到 lua_State 数据实例的 ci 指针指向了新创建的 CallInfo 实例,函数调用完结后,它会指向上一个 CallInfo 数据实例,ci 指针实质就是用来标记以后调用的是哪个函数。在实现 CallInfo 创立当前,就能够开始调用 add_op 这个函数了。在开始调用之前,咱们先来看看栈的索引,咱们能够察看到如下图所示,当索引为正时,add_op 函数栈的栈底从 1 开始,并且朝栈顶递增,如果索引为正数时,add_op 函数栈的栈顶从 - 1 开始,并且朝栈底递加。
在理解了栈索引的操作当前,add_op 外部的操作也就清晰明了了,就是把两个参数取出相加当前入栈(add_op 最初的 return 1,则示意 add_op 实际上有一个返回值),于是失去如下图所示的状态:
当初进入到 add_op 函数的最初调用阶段,就是销毁以后的 CallInfo 实例,并且将返回值移到 add_op 的地位(大家能够尝试推导一下多个返回值的状况),于是失去下图的状态
当初控制权又回到了 main 函数了,main 函数间接将栈顶变量取出打印,而后是把 add_op 的返回值出栈,于是咱们就实现了残缺的函数调用。
函数调用实现
创立虚拟机实例
在实现展示逻辑图当前,咱们当初开始着手实现具体的逻辑。本节将逐渐设计和实现上节援用例子的各个步骤,首先咱们要实现的是 luaL_newstate 接口,这个接口我定义在 clib/luaaux.h clib/luaaux.c 中,它的定义和实现如下所示:
// clib/luaaux.h
struct lua_State* luaL_newstate();
// clib/luaaux.c
static void* l_alloc(void* ud, void* ptr, size_t osize, size_t nsize) {(void)ud;
(void)osize;
// printf("l_alloc nsize:%ld\n", nsize);
if (nsize == 0) {free(ptr);
return NULL;
}
return realloc(ptr, nsize);
}
struct lua_State* luaL_newstate() {struct lua_State* L = lua_newstate(&l_alloc, NULL);
return L;
}
如上所示,luaL_newstate 实际上是转调了另一个库的接口 lua_newstate,luaL_newstate 为其指定了一个内存调配函数 l_alloc。而咱们的 lua_newstate 函数则定义在 common/luastate.h common/luastate.c 中,它们的定义是:
// common/luastate.h
#define LUA_EXTRASPACE sizeof(void*)
#define G(L) ((L)->l_G)
struct lua_State* lua_newstate(lua_Alloc alloc, void* ud);
// common/luastate.c
struct lua_State* lua_newstate(lua_Alloc alloc, void* ud) {
struct global_State* g;
struct lua_State* L;
struct LG* lg = (struct LG*)(*alloc)(ud, NULL, LUA_TTHREAD, sizeof(struct LG));
if (!lg) {return NULL;}
g = &lg->g;
g->ud = ud;
g->frealloc = alloc;
g->panic = NULL;
L = &lg->l.l;
G(L) = g;
g->mainthread = L;
stack_init(L);
return L;
}
lua_newstate 实际上是为 global_State 和 lua_State 开拓内存,并实现初始化。这里 lua_newstate 应用了从 luaL_newstate 传入的内存调配函数,这个函数的作用和 C 语言中的 realloc 相似,然而它规定,当 nsize(意为 new size)为 0 时,要将内存开释掉。对于 realloc(Reference 2)这个函数,应用它十分不便,它的作用是先开拓 nsize 的内存,将旧的内容拷贝到新的内存块后再开释原来的内存,省去了咱们手工解决的逻辑。而开拓内存自身,咱们能够留神到,它并不是独自为 global_State 和 lua_State 开拓内存,而是应用了一个叫做 LG 的数据结构:
// common/luastate.c
typedef struct LX {lu_byte extra_[LUA_EXTRASPACE];
lua_State l;
} LX;
typedef struct LG {
LX l;
global_State g;
} LG;
这是参照官网版本做的设计,咱们能够看到这里整合了一个 global_State 变量和一个 LX 变量,LX 构造蕴含了一个 lua_State 类型的变量和一个 LUA_EXTRASPACE 大小的 lu_byte 数组变量 extra_,这个变量我在官网版本中没有搜到应用到他的中央,兴许是个历史遗留问题。至于为什么要通过一个 LG 构造,将 global_State 和 lua_State 绑定在一起?这里我在云风的《lua 源码观赏》找到这样的解释(Reference 3):
Lua 的实现尽可能的防止内存碎片,同时也缩小内存调配和开释的次数。它采纳了一个小技巧,利用一个 LG 构造,把主线程 lua_State 和 global_State 调配在一起。
余下的逻辑就是 global_State 和 lua_State 的初始化操作,对于 lua_State 而言,它还须要进行栈相干的初始化 stack_init:
// luastate.c
static void stack_init(struct lua_State* L) {L->stack = (StkId)luaM_realloc(L, NULL, 0, LUA_STACKSIZE * sizeof(TValue));
L->stack_size = LUA_STACKSIZE;
L->stack_last = L->stack + LUA_STACKSIZE - LUA_EXTRASTACK;
L->next = L->previous = NULL;
L->status = LUA_OK;
L->errorjmp = NULL;
L->top = L->stack;
L->errorfunc = 0;
int i;
for (i = 0; i < L->stack_size; i++) {setnilvalue(L->stack + i);
}
L->top++;
L->ci = &L->base_ci;
L->ci->func = L->stack;
L->ci->top = L->stack + LUA_MINSTACK;
L->ci->previous = L->ci->next = NULL;
}
这里他为 lua_State 开拓 LUA_STACKSIZE(官网定义 40)大小的栈,并且设定了 L ->top 不能拜访的区域,即 stack_last 以及往后的 EXTRA_SPACE 局部。至于 EXTRA_SPACE 的作用,我集体的观点是防止爆栈时(L->top 指针超过 L ->stack_last),拜访了其余内存区域毁坏其余内存块,这里被作为一个容错的缓冲区。其余局部则是各个变量的初始化,包含为 base_ci 赋初值(限定栈范畴等)。间接通过调用 global_State 的 frealloc 函数来开拓内存,是十分繁琐的,而且还可能遇到内存不足的状况,遇到这种状况为每一个调用内存的中央做 NULL 指针断定是相当冗余的,于是我在 common/luamem.h common/luamem.c 中定义了一个 luaM_realloc 接口,M 代表 memory:
// luamem.h
void* luaM_realloc(struct lua_State* L, void* ptr, size_t osize, size_t nsize);
// luamem.c
void* luaM_realloc(struct lua_State* L, void* ptr, size_t osize, size_t nsize) {struct global_State* g = G(L);
int oldsize = ptr ? osize : 0;
void* ret = (*g->frealloc)(g->ud, ptr, oldsize, nsize);
if (ret == NULL) {luaD_throw(L, LUA_ERRMEM);
}
return ret;
}
这个接口只是多做了一个 NULL 指针断定,当内存申请失败时,调用 luaD_throw 抛出异样,这个 luaD_throw 前面介绍以保护模式调用函数时会进行阐明。走到这一步咱们的 luaL_newstate 流程也实现了,它的状态就是下图所示的那样。
销毁虚拟机实例
在介绍完创立虚拟机状态后,就须要相应地介绍销毁虚拟机状态的接口,这个就是咱们要实现的 luaL_close 函数,和 luaL_newstate 一样,它定义在 clib/luaaux.h clib/luaaux.c 中:
// clib/luaaux.h
void luaL_close(struct lua_State* L);
// clib/luaaux.c
void luaL_close(struct lua_State* L) {lua_close(L);
}
它同样是转调 common/luastate.h 里的接口:
// common/luastate.h
void lua_close(struct lua_State* L);
// common/luastate.c
#define fromstate(L) (cast(LX*, cast(lu_byte*, (L)) - offsetof(LX, l)))
static void free_stack(struct lua_State* L) {global_State* g = G(L);
(*g->frealloc)(g->ud, L->stack, sizeof(TValue), 0);
L->stack = L->stack_last = L->top = NULL;
L->stack_size = 0;
}
void lua_close(struct lua_State* L) {struct global_State* g = G(L);
struct lua_State* L1 = g->mainthread; // only mainthread can be close
// because I have not implement gc, so we should free ci manual
struct CallInfo* ci = &L1->base_ci;
while(ci->next) {
struct CallInfo* next = ci->next->next;
struct CallInfo* free_ci = ci->next;
(*g->frealloc)(g->ud, free_ci, sizeof(struct CallInfo), 0);
ci = next;
}
free_stack(L1);
(*g->frealloc)(g->ud, fromstate(L1), sizeof(LG), 0);
}
整个逻辑很简略,先把 CallInfo 实例开释掉,而后再把 lua_State 的 stack 开释掉,最初把整个 LG 开释掉(下面曾经解释过他是蕴含 global_State 和 lua_State 类型的构造)。这里须要留神的一点,则是宏 fromstate,他实际上是通过 luaState 指针 L,通过 offsetof 函数,找到 LG 的起始地位,最终将整个 LG 开释掉,而 offsetof 须要咱们指定类或构造体中成员的名称,offsetof 函数承受两个参数,第一个是类或构造体的名称,第二个是成员变量名,将第一个成员变量名传入,咱们会取得 0。因为 fromstate 这个宏中的 offsetof 调用,是将 LX 的成员 l 传入,因而 offsetof 取得的值就是 sizeof(extra),对于 lua_State L 而言,他正好能找到 LG 的起始地址,因而能够通过这种形式一并将 lua_State 和 global_State 开释。
参数入栈
参数入栈其实就是将指定类型的值 Push 到栈中,为此我实现了 luaL_pushinteger、luaL_pushnumber、luaL_pushlightuserdata、luaL_pushnil、luaL_pushcfunction 和 luaL_pushboolean,这些函数都在 clib/luaaux.h clib/luaaux.c 中,他们同样是转调 common/luastate.h common/luastate.c 的接口,这里就不一一列举他们的实现了。Push 的操作也很简略,其实就是对 L ->top 指向的地位赋值,而后 L ->top++,因而咱们首先要对每种类型都实现一个 set 函数:
// common/luastate.h
void setivalue(StkId target, int integer);
void setfvalue(StkId target, lua_CFunction f);
void setfltvalue(StkId target, float number);
void setbvalue(StkId target, bool b);
void setnilvalue(StkId target);
void setpvalue(StkId target, void* p);
void setobj(StkId target, StkId value);
// common/luastate.c
void setivalue(StkId target, int integer) {
target->value_.i = integer;
target->tt_ = LUA_NUMINT;
}
void setfvalue(StkId target, lua_CFunction f) {
target->value_.f = f;
target->tt_ = LUA_TLCF;
}
void setfltvalue(StkId target, float number) {
target->value_.n = number;
target->tt_ = LUA_NUMFLT;
}
void setbvalue(StkId target, bool b) {
target->value_.b = b ? 1 : 0;
target->tt_ = LUA_TBOOLEAN;
}
void setnilvalue(StkId target) {target->tt_ = LUA_TNIL;}
void setpvalue(StkId target, void* p) {
target->value_.p = p;
target->tt_ = LUA_TLIGHTUSERDATA;
}
void setobj(StkId target, StkId value) {
target->value_ = value->value_;
target->tt_ = value->tt_;
}
之前咱们介绍过 TValue,这里其实就是为每一种类型的域赋值,并且为其加上类型,性能简略没有什么要阐明的,对于 push 函数,这里仅举一例 lua_pushinteger,其余的实现办法相似,大家能够间接去 dummylua 的工程里找。
// common/luastate.h
void increase_top(struct lua_State* L);
void lua_pushinteger(struct lua_State* L, int integer);
// common/luastate.c
void increase_top(struct lua_State* L) {
L->top++;
assert(L->top <= L->ci->top);
}
void lua_pushinteger(struct lua_State* L, int integer) {setivalue(L->top, integer);
increase_top(L);
}
出栈
出栈操作非常简单,只须要让 L ->top–,同时要留神 top 指针不要 <=L->ci->func:
// common/luastate.h
void lua_settop(struct lua_State* L, int idx);
int lua_gettop(struct lua_State* L);
void lua_pop(struct lua_State* L);
// common/luastate.c
int lua_gettop(struct lua_State* L) {return cast(int, L->top - (L->ci->func + 1));
}
void lua_settop(struct lua_State* L, int idx) {
StkId func = L->ci->func;
if (idx >=0) {assert(idx <= L->stack_last - (func + 1));
while(L->top < (func + 1) + idx) {setnilvalue(L->top++);
}
L->top = func + 1 +idx;
}
else {assert(L->top + idx > func);
L->top = L->top + idx;
}
}
void lua_pop(struct lua_State* L) {lua_settop(L, -1);
}
这里引入了设置栈顶指针的函数 settop,逻辑也十分直观,这里不作解释。
获取栈上的值
获取栈上的值,实际上就是要传入栈的索引,而后获取他的值,通常咱们须要该地位的值是什么类型,因而通常是一个尝试性的操作,如 lua_tointeger(L, -1)是尝试将栈顶 (L->top – 1) 的值转成 integer 类型,期间可能胜利,也可能失败,这里仅举 lua_tointeger 的例子,其余局部能够到 dummylua 工程里查阅。
// clib/luaaux.h
lua_Integer luaL_tointeger(struct lua_State* L, int idx);
// clib/luaaux.c
lua_Integer luaL_tointeger(struct lua_State* L, int idx) {
int isnum = 0;
lua_Integer ret = lua_tointegerx(L, idx, &isnum);
return ret;
}
// common/luastate.h
lua_Integer lua_tointegerx(struct lua_State* L, int idx, int* isnum);
// common/luastate.c
static TValue* index2addr(struct lua_State* L, int idx) {if (idx >= 0) {assert(L->ci->func + idx < L->ci->top);
return L->ci->func + idx;
}
else {assert(L->top + idx > L->ci->func);
return L->top + idx;
}
}
lua_Integer lua_tointegerx(struct lua_State* L, int idx, int* isnum) {
lua_Integer ret = 0;
TValue* addr = index2addr(L, idx);
if (addr->tt_ == LUA_NUMINT) {
ret = addr->value_.i;
*isnum = 1;
}
else {
*isnum = 0;
LUA_ERROR(L, "can not convert to integer!\n");
}
return ret;
}
luaL_pcall 实现
在开始介绍 pcall 之前,咱们先来看看不在保护模式下的函数调用逻辑是怎么实现的,这一些列操作蕴含在 luaD_call 函数内,它被定义在 vm/luado.h vm/luado.c 上:
// vm/luado.h
int luaD_call(struct lua_State* L, StkId func, int nresult);
// vm/luado.c
int luaD_call(struct lua_State* L, StkId func, int nresult) {if (++L->ncalls > LUA_MAXCALLS) {luaD_throw(L, 0);
}
if (!luaD_precall(L, func, nresult)) {// TODO luaV_execute(L);
}
L->ncalls--;
return LUA_OK;
}
其中的 func 参数,指定了要被调用函数的栈地址,而 nresult 则指定了这个函数被冀望返回多少个返回值。这里并没有间接执行 func,而是调用了一个 luaD_precall 为函数调用做筹备,这个函数次要预处理 Lua 函数调用的状况,如果调用的是 light c function,那么就是间接执行,而不必进入到后续章节咱们将实现的 luaV_execute 中去执行虚拟机指令。这个函数同样定义在 vm/luado.h 和 vm/luado.c 中:
// vm/luado.h
int luaD_precall(struct lua_State* L, StkId func, int nresult);
// vm/luado.c
// prepare for function call.
// if we call a c function, just directly call it
// if we call a lua function, just prepare for call it
int luaD_precall(struct lua_State* L, StkId func, int nresult) {switch(func->tt_) {
case LUA_TLCF: {
lua_CFunction f = func->value_.f;
ptrdiff_t func_diff = savestack(L, func);
luaD_checkstack(L, LUA_MINSTACK); // 查看 lua_State 的空间是否短缺,如果不短缺,则须要扩大
func = restorestack(L, func_diff);
next_ci(L, func, nresult);
int n = (*f)(L); // 对 func 指向的函数进行调用,并获取理论返回值的数量
assert(L->ci->func + n <= L->ci->top);
luaD_poscall(L, L->top - n, n); // 解决返回值
return 1;
} break;
default:break;
}
return 0;
}
目前我只解决了 LUA_TLCF 也就是 light c function 这一种状况。调用 func 之前,首先会检查一下 lua_State 的栈是否空间短缺,如果新创建的 CallInfo 的 top 指针,不能在 lua_State 栈的无效范畴之内,那么栈就要裁减,通常是裁减为原来的两倍,这一段逻辑写在 luaD_checkstack 内,大家能够间接到 dummylua 工程里查看,这里就不列举了。这里须要留神的是,拓展栈须要申请一块新的内存空间,因而 L ->stack 的地址也会扭转,在裁减之后,须要修改 func 所指向的地址。实现这一系列的操作之后,就是为调用 func 创立一个 CallInfo 实例,大家能够将上面这段代码和之前展现的逻辑图分割起来,就能很好得了解了。
static struct CallInfo* next_ci(struct lua_State* L, StkId func, int nresult) {struct global_State* g = G(L);
struct CallInfo* ci;
ci = luaM_realloc(L, NULL, 0, sizeof(struct CallInfo));
ci->next = NULL;
ci->previous = L->ci;
L->ci->next = ci;
ci->nresult = nresult;
ci->callstatus = LUA_OK;
ci->func = func;
ci->top = L->top + LUA_MINSTACK;
L->ci = ci;
return ci;
}
接下来就是对 func 函数进行调用,调用实现后,他会返回一个值 n,通知咱们他理论有多少个返回值,而后须要依据这个 n 值对返回值进行解决,这些解决逻辑实现在 vm/luado.h vm/luado.c 中的 luaD_poscall 函数中:
// vm/luado.h
int luaD_poscall(struct lua_State* L, StkId first_result, int nresult);
// vm/luado.c
int luaD_poscall(struct lua_State* L, StkId first_result, int nresult) {
StkId func = L->ci->func;
int nwant = L->ci->nresult;
switch(nwant) {
case 0: {L->top = L->ci->func;} break;
case 1: {if (nresult == 0) {
first_result->value_.p = NULL;
first_result->tt_ = LUA_TNIL;
}
setobj(func, first_result);
first_result->value_.p = NULL;
first_result->tt_ = LUA_TNIL;
L->top = func + nwant;
} break;
case LUA_MULRET: {int nres = cast(int, L->top - first_result);
int i;
for (i = 0; i < nres; i++) {
StkId current = first_result + i;
setobj(func + i, current);
current->value_.p = NULL;
current->tt_ = LUA_TNIL;
}
L->top = func + nres;
} break;
default: {if (nwant > nresult) {
int i;
for (i = 0; i < nwant; i++) {if (i < nresult) {
StkId current = first_result + i;
setobj(func + i, current);
current->value_.p = NULL;
current->tt_ = LUA_TNIL;
}
else {
StkId stack = func + i;
stack->tt_ = LUA_TNIL;
}
}
L->top = func + nwant;
}
else {
int i;
for (i = 0; i < nresult; i++) {if (i < nwant) {
StkId current = first_result + i;
setobj(func + i, current);
current->value_.p = NULL;
current->tt_ = LUA_TNIL;
}
else {
StkId stack = func + i;
stack->value_.p = NULL;
stack->tt_ = LUA_TNIL;
}
}
L->top = func + nresult;
}
} break;
}
struct CallInfo* ci = L->ci;
L->ci = ci->previous;
L->ci->next = NULL;
// because we have not implement gc, so we should free ci manually
struct global_State* g = G(L);
(*g->frealloc)(g->ud, ci, sizeof(struct CallInfo), 0);
return LUA_OK;
}
函数第二个参数 first_result 记录了第一个返回值在栈中的地位,而第三个参数 nresult 则指明了理论的返回值数量。很多状况下,咱们调用函数时,期待的返回值数量 nwant 和理论的返回值数量 nresult 是不一样的,因而在 luaD_poscall 函数里须要对各种状况进行解决。如果咱们不期待有返回值,也就是 nwant 为 0,那么理论的返回值不论有多少个都会被抛弃,此时 L ->top 指针也会指向 L ->ci->func 的地位,函数以及之前 Push 进来的参数都会被出栈。当咱们期待只有一个返回值时,返回值后果会被挪动到 L ->ci->func 的地位,如果理论没有返回值,他就会被设置为 NIL。当咱们期待的返回值比理论的返回值多时,短少的局部会被 NIL 值补足。当咱们期待的返回值比理论少时,多进去的会被抛弃。这里还有一个非凡的状况,就是 nwant 为 LUA_MULRET 的状况,这种状况就是理论有多少个返回值,他就返回多少个,不会抛弃,事实上 LUA_MULRET 的值是 -1。通过一系列操作,对于被调用者而言当初栈上存在的不再是被调用函数本人的参数,而是被调用函数留下的返回值。
形容 C 函数在栈上的调用流程,是十分难写的,这也是为什么在探讨具体实现之前,先通过图的形式在概念上展现这个流程,目标是为了让大家有宏观上的感知,如果能了解上节的内容,那么这章节的内容也不难理解,尽管他可能很干燥。在实现函数调用的实现当前,咱们还须要探讨的一个货色,就是在保护模式下进行函数调用。事实上提供给内部应用的函数调用接口是 luaL_pcall,也就是默认所有的 C 函数调用都应该在保护模式下进行,他的定义如下所示:
// clib/luaaux.h
int luaL_pcall(struct lua_State* L, int narg, int nresult);
// clib/luaaux.c
// function call
typedef struct CallS {
StkId func;
int nresult;
} CallS;
static int f_call(lua_State* L, void* ud) {CallS* c = cast(CallS*, ud);
luaD_call(L, c->func, c->nresult);
return LUA_OK;
}
int luaL_pcall(struct lua_State* L, int narg, int nresult) {
int status = LUA_OK;
CallS c;
c.func = L->top - (narg + 1);
c.nresult = nresult;
status = luaD_pcall(L, &f_call, &c, savestack(L, L->top), 0);
return status;
}
luaL_pcall 接口只是让使用者填入,被调用的函数有多少个参数,以及冀望有多少个返回值,luaL_pcall 是通过 narg 这个示意参数个数的值,推断出被调用函数在栈中的地位,并且调用另一个函数 luaD_pcall,咱们能够察看到,这里一并将 f_call 函数传入到 luaD_pcall 中,在保护模式下执行 f_call,实际上就是执行咱们刚刚探讨的 luaD_call。调用 luaD_pcall,除了须要传入理论执行栈函数调用的 f_call 以外,咱们还须要传入一个 CallS 类型的变量 c,这个变量会在 f_call 中应用到,其实是保留调用信息长期数据的一个构造。此外咱们还须要传入栈顶在函数调用前的地位,用于异样后复原栈之用。上面咱们来看看 luaD_pcall 的定义:
// vm/luado.h
int luaD_pcall(struct lua_State* L, Pfunc f, void* ud, ptrdiff_t oldtop, ptrdiff_t ef);
// vm/luado.c
int luaD_pcall(struct lua_State* L, Pfunc f, void* ud, ptrdiff_t oldtop, ptrdiff_t ef) {
int status;
struct CallInfo* old_ci = L->ci;
ptrdiff_t old_errorfunc = L->errorfunc;
status = luaD_rawrunprotected(L, f, ud);
if (status != LUA_OK) {
// because we have not implement gc, so we should free ci manually
struct global_State* g = G(L);
struct CallInfo* free_ci = L->ci;
while(free_ci) {if (free_ci == old_ci) {
free_ci = free_ci->next;
continue;
}
struct CallInfo* previous = free_ci->previous;
previous->next = NULL;
struct CallInfo* next = free_ci->next;
(*g->frealloc)(g->ud, free_ci, sizeof(struct CallInfo), 0);
free_ci = next;
}
L->ci = old_ci;
L->top = restorestack(L, oldtop);
seterrobj(L, status);
}
L->errorfunc = old_errorfunc;
return status;
}
咱们的 f_call 最终会在 luaD_rawrunprotected 中被调用,而在调用之前,咱们须要对栈的一些信息进行保留,包含栈以后调用的是哪个函数(保留 ci),栈以后的谬误处理函数处于哪个地位。而一旦调用失败,咱们就须要复原原来栈的状态。
当初咱们来看看 luaD_rawrunprotected 的定义:
// vm/luado.h
int luaD_rawrunprotected(struct lua_State* L, Pfunc f, void* ud);
// vm/luado.c
#define LUA_TRY(L, c, a) if (_setjmp((c)->b) == 0) {a}
#ifdef _WINDOWS_PLATFORM_
#define LUA_THROW(c) longjmp((c)->b, 1)
#else
#define LUA_THROW(c) _longjmp((c)->b, 1)
#endif
struct lua_longjmp {
struct lua_longjmp* previous;
jmp_buf b;
int status;
};
int luaD_rawrunprotected(struct lua_State* L, Pfunc f, void* ud) {
int old_ncalls = L->ncalls;
struct lua_longjmp lj;
lj.previous = L->errorjmp;
lj.status = LUA_OK;
L->errorjmp = &lj;
LUA_TRY(
L,
L->errorjmp,
(*f)(L, ud);
)
L->errorjmp = lj.previous;
L->ncalls = old_ncalls;
return lj.status;
}
这里咱们定义了一个 lua_longjmp 的构造,这是用来辅助咱们解决异常情况用的。在初始化 lua_State 的时候,咱们的 errorjmp 的值为 NULL,errorjmp 是否为 NULL 是判断以后函数是否处于保护模式的重要指标,这个咱们前面会提到。咱们的 f_call 函数,是在 LUA_TRY 这个宏里被调用的,而这个宏的作用是什么呢?因为 / C 语言没有 try catch 机制,因而咱们须要通过一种形式来模仿 try catch 机制来捕获异样,这里就须要用到咱们的 setjmp 和 longjmp 函数了。setjmp 和 longjmp 是什么接口?咱们来援用 Linux man page longjmp 的一段阐明:
Description
longjmp() and setjmp(3) are useful for dealing with errors and interrupts encountered in a low-level subroutine of a program. longjmp() restores the environment saved by the last call of setjmp(3) with the corresponding env argument. After longjmp() is completed, program execution continues as if the corresponding call of setjmp(3) had just returned the value val. longjmp() cannot cause 0 to be returned. If longjmp() is invoked with a second argument of 0, 1 will be returned instead.siglongjmp() is similar to longjmp() except for the type of its env argument. If, and only if, the sigsetjmp(3) call that set this env used a nonzero savesigs flag, siglongjmp() also restores the signal mask that was saved by sigsetjmp(3).
是不是看着很晕?那咱们就通过一个例子来加以解释和阐明:
#include <stdio.h>
#include <assert.h>
#include <setjmp.h>
jmp_buf b;
int main(void) {int ret = setjmp(b);
printf("setjmp result = %d\n", ret);
if (ret == 0)
longjmp(b, 1);
return 0;
}
此时的输入是:
setjmp result = 0
setjmp result = 1
为什么 printf 函数会被调用两次?答案是咱们第一次调用 setjmp 的时候,将以后的栈信息保留在了 jmp_buf 变量 b 中,并且返回 0 值,此时程序继续执行,这时候因为返回值 ret 为 0,因而咱们会调用 longjmp 函数。这个 longjmp 函数会复原 jmp_buf 变量 b 中记录的环境,因而程序会跳回到 setjmp 调用的中央,并且 longjmp 还将第二个参数 1,带给了 setjmp,并作为其第二次调用的返回值。利用这种个性,咱们能够模仿 C ++ 的 try catch 机制,上面再举个例子:
#include <stdio.h>
#include <assert.h>
#include <setjmp.h>
int except = 0;
jmp_buf b;
void foo() {printf("foo\n");
longjmp(b, 1);
}
int main(void) {if ((except = setjmp(b)) == 0) {foo();
}
else {printf("except type %d\n", except);
}
return 0;
}
// 输入
foo
except type 1
把 setjmp(b)不为 0 时,执行的局部,作为捕获异样的逻辑,把 longjmp 作为 throw 函数应用,回顾一下咱们的 LUA_TRY 和 LUA_THROW 宏,是不是有一种殊途同归之妙?当然,咱们不会间接在被调用的函数里,产生异样时间接调用 LUA_THROW 宏,而是应用另一个接口 luaD_throw:
// vm/luado.h
void luaD_throw(struct lua_State* L, int error);
// vm/luado.c
void luaD_throw(struct lua_State* L, int error) {struct global_State* g = G(L);
if (L->errorjmp) {
L->errorjmp->status = error;
LUA_THROW(L->errorjmp);
}
else {if (g->panic) {(*g->panic)(L);
}
abort();}
}
这个函数,依据 L ->errorjmp 的值,判断以后函数是否处于保护模式中,处于与不处于解决的形式是不雷同的,如果 L ->errorjmp 为非 NULL 值,那阐明函数是在 LUA_TRY 的包裹下执行的,因而能够调用 LUA_THROW 跳出以后的调用,如果不是,那么则调用之前设置好的 panic 函数(如果有),而后退出过程。
到目前为止,函数调用和保护模式的代码咱们均已实现。
结束语
这篇文章篇幅较长,然而咱们的指标是清晰的,设计和实现 Lua 根本数据结构、栈和基于栈的 C 函数调用的设计与实现,并对这些内容进行了不同水平的解析。只管咱们当初并未波及到任何编译器和虚拟机指令相干的内容,然而以后的内容是了解后续内容的根底,也是关键所在,心愿后续内容可能给大家带来更精彩的内容。
Reference
- The Lua Architecture 图片位于 Lua: An Embedded Script Language 这个章节中:Figure 1: process of initializing Lua and loading a script file
- realloc
a) expanding or contracting the existing area pointed to by ptr, if possible. The contents of the area remain unchanged up to the lesser of the new and old sizes. If the area is expanded, the contents of the new part of the array are undefined. b) allocating a new memory block of size new_size bytes, copying memory area with size equal the lesser of the new and the old sizes, and freeing the old block. - Lua 源码观赏 2.2 全局状态机
这是侑虎科技第 1300 篇文章,感激作者 Manistein 供稿。欢送转发分享,未经作者受权请勿转载。如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ 群:465082844)
作者主页:https://manistein.github.io/b…
再次感激 Manistein 的分享,如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ 群:465082844)