1.2.1 虚拟机简介
前文介绍了Lua解释器的整体架构和运行机制,本节将会对其内部的一些数据结构进行简要的介绍。在后续的章节中,会逐渐丰富虚拟机的内容。回顾一下图1-7,到目前为止,Lua解释器的虚拟机被看作是一个黑盒子,那么这个黑盒子里有什么东西呢?由于Lua解释器是用C语言开发的,并没有什么面向对象的概念,因此也没有一个虚拟机类对象。但是Lua虚拟机有一个很重要的数据结构,这个结构被称为global_State。这个结构包含了为虚拟机开辟和释放内存所需的内存分配函数,保存GC对象和状态的成员变量,以及一个主线程结构实例、全局注册表等。
要理解Lua虚拟机,有两个特别重要的结构要弄清楚,一个是前面说的global_State结构,还有一个是Lua虚拟机自定义的“线程”结构,也被称为lua_State结构。Lua虚拟机里的“线程”和操作系统的线程是有区别的。操作系统中多条线程之间可以并发(分时间片交替运行)或并行(在同一时刻、不同CPU核心内)执行,而Lua虚拟机的“线程”则不行。Lua虚拟机的“线程”切换,必须等正在运行的“线程”先执行完或者主动调用挂起函数,否则其他“线程”不会被执行。Lua虚拟机的“线程”实际上是运行在操作系统的线程内。在实践中,一条操作系统线程,在同一时刻往往只会运行Lua虚拟机里其中的一条“线程”。当Lua虚拟机内部存在多个“线程”实例时,除了“主线程”,其他“线程”实际上是协程。
现在先来看一下global_State的整体结构。global_State结构里的成员可以先从大体概念去划分,而不是过早地关注内部的细节,这样有利于先建立整体的概念,然后顺着概念逐个击破。图1-11展示了整个global_State大体的结构,这是Lua虚拟机最核心的数据结构。现在对global_State结构的几个部分分别进行解释和说明。
· Allocator:这是Lua虚拟机的内存分配器,本质上是一个内存分配函数。虚拟机开辟内存和释放内存均需要通过这个函数。用户可以自定义内存分配器,也可以使用官方默认的。官方默认的最终会调用realloc和free函数。
·图1-11
· GC fields:这是包含一系列和GC相关的成员,将在第2章详细讨论它们。
· String Table:这是短字符串的全局缓存。同样地,对于字符串,将在第2章讨论它的设计和实现。
· Registry:这是Lua虚拟机的全局注册表。它本质上是一个Lua表对象,在全局注册表Registry中只有数组被用到,并且第一个值是指向“主线程”的指针,而第二个值则是指向全局表(也就是_G)的指针。
· Mainthread:这是Lua虚拟机,指向“主线程”结构的指针。在Lua C层代码中,可以很轻易地拿到global_State指针。而这个Mainthread指针,可以方便地获取Mainthread对象。
剩下的部分是和元表、弱表等相关的内容,在第5章会介绍它们。
接下来要来介绍的内容则是Lua虚拟机“线程”结构。前文也已经提到过,Lua虚拟机的“线程”结构实际上是lua_State结构。lua_State结构的内容较多,这里将其抽象成若干个大的模块,如图1-12所示。下面对这些模块分类进行简要说明。
· GC相关:所谓GC即是Garbage Collection,也就是垃圾回收。所有垃圾回收相关的成员都归为这类,第2章将详细讨论GC机制。
· Stack相关:每个Lua“线程”实例,都会有自己独立的栈空间、信息等。这些部分包含在stack相关的域内,Lua的函数会在栈上执行,临时变量也会暂存在栈上,同时栈的起始地址、大小信息也包含在这里面。此外,Lua虚拟机的虚拟寄存器也是直接使用栈上的空间,第4章将讨论编译器相关的内容。
· status:代表了Lua“线程”实例的状态,Lua“线程”在初始化阶段会被设置为LUA_OK。
·图1-12
· global_State指针:指向Lua虚拟机中global_State结构的指针。
· CallInfo相关:这是函数调用相关的信息。前文提到过,函数(Lua函数和C函数)要被执行,首先函数实体要被压入lua_State结构的栈中,然后再进行调用。CallInfo相关的信息则会记录被调用的函数在栈中的位置。每个被调用的函数都有自己独立的虚拟栈(lua_State栈中的某个片段),CallInfo信息会记录独立虚拟栈的栈顶信息。此外,它还记录了被调用的函数有几个返回值、调用的状态以及当前执行的指令地址等,是和Lua栈一样具有同等重要性的数据结构。
· 异常处理相关:当Lua栈内函数调用发生异常时,需要这些异常相关的变量协助进行错误处理。
到这里就已经完成Lua虚拟机中最重要的两个数据结构的介绍了。实际上,需要介绍的内容还有很多,包括字符串和表等,这些将在后续章节详细介绍。此外,相关资料对于虚拟机的定义,语言级别的虚拟机是用来运行独立于平台的程序(比如字节码)。也就是说,Lua虚拟机也需要有解析并运行Lua字节码的能力。
Lua虚拟机的运行主要是调用函数。在Lua虚拟机中被调用的函数类型主要有两大类:一类是C函数,另一类是Lua函数。C函数又细分为Light C Function和C闭包(C Closure),Lua函数主要是指Lua闭包。本书将在后续章节介绍闭包的概念,这里仅举两个简单的例子。先来看第一个例子,假设有一个C函数,如下所示。
在调用test_main04函数之前,首先要调用luaL_newstate函数创建一个Lua虚拟机实例,内存中将会得到一个global_State和lua_State实例。此时,虚拟机将test_main04函数压入虚拟机“主线程”的栈中,然后压入两个整型参数:1和2,得到图1-13所示的结果。
现在可以清晰地看到栈顶函数和参数的位置。那么此时,要在Lua虚拟机中运行test_main04函数,需要调用随书代码中的lua_pcall函数,其声明如下所示。
·图1-13
函数的第一个参数L是表示Lua“线程”;第二个参数narg表示要在Lua“线程”里执行的函数有多少个参数;第三个参数nresult表示在Lua“线程”里执行的函数有多少个返回值。
可以看到,test_main04函数是将两个输入参数相加再返回,那么要在虚拟机调用这个函数需要调用如下代码。
这里需要说明的是,lua_pcall函数是随书工程自定义的一个函数,为了方便叙述,它省略了官方同名函数中的最后一个参数。上面这行代码代表的含义是,调用图1-13中Lua“主线程”栈上位于top-(2+1)位置上的test_main04函数,其中参数有两个,返回值是1个。接下来,就会执行test_main04函数,于是可以得到图1-14所示的结果。
从图1-14中,可以看到原来函数的位置被计算结果覆盖了。而top指针则指向了计算结果的上方。
以上就是Lua虚拟机调用C函数的一个简单例子。既然Lua虚拟机的作用是运行Lua脚本编译出来的指令,为什么还要保留调用C函数的功能呢?这样会不会多此一举?答案是不会的。因为在Lua脚本中调用C函数,C库可以作为Lua语言的高性能拓展,这也是为什么Lua能作为胶水语言的原因。
·图1-14
现在来看第二个例子。假设有一个Lua脚本test.lua,脚本里的代码只是一行“print("hello world")”的代码,那么加载并运行这段脚本的流程又是怎样的呢?首先,通过luaL_loadfile函数加载test.lua脚本,并进行编译,此时会生成一个LClosure类型的实例(脚本被编译之后的结果,被虚拟机运行前,要创建这样一个实例来存放这些编译结果和执行状态)。它包含了一个Proto结构,Proto结构里包含了编译好的指令和其他一些信息,这种状态可以通过图1-15来表示。在图中读者可以看到,编译后的结果会被存放在Proto结构中,其中指令存放在code列表中,而脚本中的常量则被存放到常量表k中。图中的Proto结构是被压入栈中LClosure实例的一个成员。目前图1-15所展示的是经过luaL_loadfile编译后的状态,接下来就是要让虚拟机去运行LClosure实例中的代码了。
调用lua_pcall(L,0,0)就能够让虚拟机找到LClosure函数,并且去执行它。虚拟机的执行函数,会逐个运行Proto结构中code列表的指令。
首先执行的是“OP_GETTABUP 0 0 256”指令。这个指令的等式表达为R(A)=_ENV[RK(C)](R表示寄存器,K表示常量表)。指令的第一个0表示目标寄存器的位置,在本例中就是stack上被标记为0的位置。第二个0表示从LClosure实例的第0个上值(upvalue)找RK(256)的变量。在Lua-5.3中,每个函数实例的第一个上值都是_ENV,而它默认指向_G。RK(C)的含义是:当C的值<256时,就去栈中找变量;当C≥256时,就到常量表k[C-256]中找值。结合起来,就是
R(A)=_ENV[RK(C)]==>R(0)=_ENV[k[C-256]]==>R(0)=_ENV[k[0]]==>R(0)=_G[“print”]
含义就是,从全局表_G中找到名为print的值,并且将其设置到stack上被标记为0的位置上。
接下来要执行的指令则是OP_LOADK,它的等式表达为R(A)=k[B]。结合本例的例子,可以得到如下推导:
·图1-15
R(A)=k[B]==>R(1)=k[1]==>R(1)=“hello world”
往后就是执行OP_CALL指令(见附录A),它可以用公式“Call R(A)B-1 C-1”表示。将当前情景代入,则是Call R(0)1 0,表示调用位于栈0的函数——print函数,输入一个参数“hello world”,并返回0个值。完成调用后,屏幕输出“hello world”,同时栈中0和1标记的位置就相当于清空状态了。
对Lua虚拟机的介绍就到此为止了。本节首先对虚拟机两个重要的数据结构global_State和lua_State进行简要说明,然后对虚拟机运行C函数和Lua函数的流程也进行了简要的说明,后面将介绍Lua虚拟机的指令编码方式和指令集。