1.1 Lua解释器
1.1.1 Lua解释器的整体架构
Lua是一门脚本语言,Lua脚本能够边编译边执行,符合解释器的所有特征。编译和运行Lua脚本的程序称为Lua解释器。
本节将对Lua的整体架构进行讨论。在开始详细讨论之前,首先向读者介绍一下什么是解释器。按照编译原理相关书籍的介绍,解释器是将输入的源代码(脚本),直接编译并且直接运行,源代码被加载到解释器以后,不会输出目标代码,而是直接被解释执行,直接输出结果,如图1-1所示。
·图1-1
前文比较抽象地介绍了什么是解释器,现在来看一个比较直观的例子。例子在ubuntu16.04的云服务器上进行。首先,创建一个~/workspace/lua的目录,并且通过cd命令(Dos系统的目录切换命令),进入这个目录。通过wget https://www.lua.org/ftp/lua-5.3.5.tar.gz命令语句,获得lua-5.3.5的源码压缩包,如图1-2所示。
接下来,使用tar-zxvf lua-5.3.5.tar.gz命令进行解压,得到Lua源码目录lua-5.3.5,如图1-3所示。
·图1-2
·图1-3
通过cd命令,进入到lua-5.3.5的目录中,执行make linux指令,等待编译完成。然后,在lua-5.3.5/src目录下获得一个名称为lua和luac的可执行文件。这里的可执行文件lua就是Lua解释器,luac则是将Lua脚本编译成字节码的编译器。
在lua-5.3.5目录下,创建一个scripts目录,并且创建一个test.lua脚本,脚本里的代码只是一个print("hello world")语句,输入.../src/lua test.lua指令,可以得到图1-4所示的结果。
·图1-4
结合图1-1可以很自然地联想到,test.lua就是源代码(脚本),位于../src目录下名为lua的可执行文件就是解释器,而输出的hello world就是运行结果。现在,读者对Lua解释器应该有了更为直观的认识了。那么解释器和编译器又有什么区别呢?按照编译器的定义,编译器的作用就是将一种语言转化为另一种语言。在编译器实践中,通常编译器的输入语言是相对高级的语言,而输出语言通常是更贴近底层的语言,如图1-5所示。
·图1-5
编译器在对源语言执行编译时,并不会直接运行源语言的逻辑,而是将其转化为目标语言。比如前面对Lua源码进行编译,会得到很多“.o”文件。那么Lua源码则是源语言,GCC是编译器,“∗.o”文件则包含了目标语言(二进制机器码)。此外,还需要通过链接器,将诸多“.o”文件整合成可执行文件lua和luac。在lua和luac程序被启动之前,Lua源代码在编译和链接的过程中是没有被执行的,而解释器则会直接对输入的脚本代码进行执行操作,这就是编译器和解释器的核心区别。
在了解完编译器和解释器的概念之后,现在开始探讨Lua解释器的整体架构。Lua解释器主要有两种运行模式:一种是前面展示的,直接加载Lua脚本并执行,直接获得运行结果;另一种则是通过Lua编译器(即可执行文件),将Lua脚本编译成字节码暂存在磁盘中,当需要使用的时候,再去加载执行,如图1-6所示。
·图1-6
两种模式主要的区别是,编译脚本代码发生在不同的时期。第一种是解释器运行期间,加载脚本代码后,直接编译并直接运行。第二种模式则是预先将脚本编译,并以文件的形式存入磁盘,需要的时候,再加载到解释器中进行执行,此时省去了编译的过程。
这里就涉及一个新的问题,预先编译的模式能否在运行期比直接加载脚本并执行的模式更快呢?答案是否定的。因为不论是预先编译,还是直接加载脚本并且编译执行,它们生成的指令是一样的,因此在运行期间不会有效率上的差别。但是预先编译的方式,确实会在加载和执行时省去编译所需要的时间。
官方Lua中,luac和lua两个可执行文件都包含了一个内置编译器。它们内部包含的内置编译器是一样的,只是luac增加了输出字节码的逻辑。本书不会对如何构建luac编译器进行讨论,而是集中时间精力探索Lua解释器,因为只要搞懂Lua解释器的运行机制,读者回顾luac的时候就自然会驾轻就熟了。
继续研究Lua解释器的整体内部构造。前面将Lua当作是一个黑盒子,现在要做的则是打开这个黑盒子。Lua解释器可以分割成两大部分,分别是编译器和虚拟机。回顾一下图1-1的情景,脚本会被解释器加载编译并执行,直接输出结果。将其内部继续细化,得到图1-7所示的结果。从图中可以看到,解释器内部被分割成编译器和虚拟机。该运行方式和图1-6的方式非常相似,只是通过编译器编译后的字节码,图1-6的方式是存放在文件里,并且在编译的过程中,解释器程序未被启动。而图1-7的方式则是将字节码信息存在一个被叫作Proto的内存结构中,这个结构主要是存放编译结果(指令、常量等信息)。虚拟机在运行的过程中,会从Proto结构中取出一个个的字节码,然后再执行。
·图1-7
前面对编译器和解释器进行了说明,现在发现解释器内部还包含了一个编译器,如图1-7所示。有些读者可能会有些困惑,既然编译器和解释器是有区别的,那Lua解释器为什么又包含了编译器呢?前文也已经提到过,只要能将一种语言转化成另一种语言的程序,都是符合编译器的定义的。Lua解释器内部的编译器,本质上是将脚本代码编译成字节码,符合这个定义,因此也是编译器。
在编译原理中,有编译型语言和解释型语言之分。编译型语言的编译器,能够将源代码直接编译成机器码生成可执行文件,运行源码的逻辑需要将可执行文件加载到进程中执行,比如C/C++语言就是这种类型。编译型语言编译出来的机器码是和平台相关的,比如在x86平台上编译的程序,是不能在ARM平台上运行的。解释型语言则不同,只要目标平台能够运行该语言的解释器,它们的逻辑脚本可以不经过任何修改,就能在这些目标平台上运行。Lua就是这样的解释型语言。
Lua解释器的内置编译器和编译型语言(比如C、C++)是有很大的区别的。编译型语言的编译器强大且复杂,根据《编译原理:原理、技术与工具》一书中的定义,它包含了前端和后端。前端负责对源代码进行词法分析、语法分析、语义分析,再通过中间码生成器生成中间表示,然后交给编译器后端的代码生成器生成目标机器码,最后通过编译器后端的目标码优化器优化目标机器码,如图1-8所示。
·图1-8
Lua解释器的内置编译器,则没有这么复杂。首先它不负责生成机器码,主要工作就是将Lua脚本编译成虚拟机能够识别的字节码。其次,它的构造也很简单,主要包含了词法分析器和语法分析器。其语法分析器也不生成抽象语法树,而是直接生成字节码。
截止到现在,已完成对Lua解释器整体架构的介绍。下一节,将对Lua解释器的整体运行机制进行简要的介绍。