Lua 是怎么执行代码的?初识 Lua

发布时间 2023-06-26 12:42:09作者: 古明地盆

楔子

下面我们来一起学习一下 Lua 语言,相信很多人都听说过它,Lua 是一种可嵌入、轻量、快速、功能强大的脚本语言,使用较为广泛,主要用在游戏领域。另外,基于 Nginx 的 OpenResty 也是使用 Lua 来编写脚本的,很多存储框架(如 Redis)也支持使用 Lua 来编写脚本。

作为一门诞生已经超过 20 年的语言,Lua 在设计上是非常克制的。首先 Lua 的解释器是 C 编写的,源码总共才两万多行,也正因为 Lua 的精简性,导致它无法独立地开发大型应用,它存在的意义主要是为了和其它语言结合,提供相应的扩展功能,比如在 C++ 中引入 Lua 脚本等等。

然后就是作为一门以纯 C 代码编写的项目,Lua 代码优美、结构组织紧凑,是教科书般经典的 C 语言项目。并且它的性能非常的优秀,可以说是动态语言的性能巅峰了,所以我们学习它是完全有必要的。当然啦,既然要选择学习 Lua,说明你肯定有其它语言的基础,所以这里我们不光会介绍 Lua 的基础语法,还会剖析 Lua 的底层 C 实现。

Lua 的安装

然后我们来安装 Lua,这里安装最新的 5.4 版本,我们进入 Lua 的官网,根据操作系统选择合适的安装包即可,我这里是 Windows。

下载完之后,直接解压即可,目录里面就 4 个文件。

这几个文件的作用如下:

  • luac54.exe:Lua 编译器的可执行文件,负责将 Lua 脚本文件编译成字节码文件(后缀名为 .luac)。字节码文件可以更快地加载和执行,因此在发布阶段可以使用它们来保护源代码和提高执行效率。所以 Lua 虽然是解释型语言,但也是要经过编译的。而 Lua 解释器由一个 Lua 编译器加上一个 Lua 虚拟机组成,Lua 编译器将 .lua 文件编译成 .luac 文件之后,会交给 Lua 虚拟机执行。
  • lua54.exe: Lua 解释器的可执行文件,我们通过它来执行 Lua 脚本(后缀名为 .lua),或者通过交互模式运行 Lua 代码。当然在执行 Lua 代码之前,会先通过 Lua 编译器将其编译成字节码。只不过编译器的实现没有和虚拟机集成在一起,而是单独抽离成了 luac54.exe(会在编译 Lua 代码时被 lua54.exe 所调用)。
  • lua54.dll: Lua 虚拟机依赖的动态链接库(DLL)文件,它提供了运行 Lua 脚本的功能,其他程序可以通过加载这个 DLL 文件来嵌入 Lua 解释器,从而执行 Lua 脚本。
  • wlua54.exe:这是 Windows 平台上 Lua 解释器的可执行文件,它与 lua54.exe 的功能相同,但具有 Windows 特定的支持和接口,wlua54.exe 可以用于在 Windows 系统上执行 Lua 脚本,我们直接用 lua54.exe 即可。

可以看到 luac54.exe、lua54.exe、lua54.dll 组合起来,便构成了整个 Lua 解释器,而大小加起来还不到 800K,所以引入 Lua 扩展之后的应用程序,大小几乎没有变化,但是却极大地方便了程序的开发。特别是在游戏领域,对于端游来说,脚本还算是锦上添花的东西,但对于手游来说,脚本就是刚需了。

然后将该目录设置到环境变量中,通过在终端中输入 lua54,即可进入到 Lua 的交互式环境中。这里我就不演示了,下面会使用 IDE 进行操作,关于 IDE,我这里使用的是 PyCharm,下载一个 Lua 的插件即可。下面老规矩,我们来打印一个 "hello world",这个仪式感不能丢。创建一个 main.lua,里面写上一行打印语句。

print("Hello World") 

执行的时候,在控制台就会显示出 "hello world"。估计有小伙伴觉得,你这写的是 Python 吧。其实,Lua 中的打印函数也是 print,并且也是内置的,并且 Lua 中的字符串可以使用单引号、也可以使用双引号,当然三引号不可以。

Lua 是怎么执行代码的?

在 Lua 的行话里,一段可以被 Lua 解释器解释执行的代码就叫作 chunk。因此 chunk 可以很小,小到只有一两条语句,也可以很大,大到包含成千上万条语句和复杂的函数定义。前面提到过,为了获得较高的执行效率,Lua 并不是直接解释执行 chunk,而是先由编译器编译成内部结构(其中包含字节码等信息),然后再由虚拟机执行字节码。这种内部结构在 Lua 里就叫作预编译(Precompiled)chunk,由于采用了二进制格式,所以也叫二进制(Binary)chunk。

我们以 Java 和 Python 作为对照,存放 chunk 的源文件(一般以 .lua 为后缀)对应 .java 源文件和 .py 源文件,二进制 chunk 则对应编译好的 class 文件和 pyc 文件。Java 的 class 文件和 Python 的 pyc 文件里面除了字节码外,还有常量池、符号表等信息。类似地,二进制 chunk 里也有这些信息。当然啦,Lua 程序员一般不需要关心二进制 chunk,因为 Lua 解释器会在内部进行编译。

假设当前有一个 main.lua 文件,我们直接通过 lua54 main.lua 即可执行,内部会自动编译。当然啦,我们也可以先编译,再执行。

  • luac54 -o main.luac main.lua
  • lua54 mian.luac

先手动将 main.lua 编译成 main.luac,然后 luc54.exe 直接执行 main.luac 即可。

需要说明的是,在 Lua 5.3 以及之前的版本,编译器的名字就叫 luac.exe,解释器的名字叫 lua.exe。但我当前下载的版本,里面带上了 54,表示具体版本。后续为了方便,有时候会把 luac54 说成 luac,把 lua54 说成 lua,不过名字什么的无所谓,大家知道就好。

然后再来说一说 luac,它主要有两个用途:第一,作为编译器,把 Lua 源文件编译成二进制 chunk 文件:第二,作为反编译器,分析二进制 chunk,将信息输出到控制台。

编译 Lua 源文件

将一个或者多个文件名作为参数调用 luac 命令就可以编译指定的 Lua 源文件,如果编译成功,在当前目录下会出现 luac.out 文件,里面的内容就是对应的二进制 chunk。如果不想使用默认的输出文件,可以使用 -o 选项对输出文件进行明确指定。

luac54 main.lua  # 生成 luac.out
luac54 -o main.luac main.lua  # 生成 main.luac

编译生成的二进制 chunk 默认包含调试信息(行号、变量名等),可以使用 -s 选项告诉 luac 去掉调试信息。另外,如果仅仅想检查语法是否正确,不想产生输出文件,可以使用 -p 选项进行编译。

luac54 -s main.lua  # 生成 luac.out,不包含调试信息
luac54 -s -o main.luac main.lua  # 生成 main.luac,不包含调试信息

luac54 -p main.lua  # 只进行语法检查

Lua 编译器以函数为单位进行编译,每一个函数都会被 Lua 编译器编译为一个内部结构,这个结构叫作原型(Prototype)。原型主要包含 6 部分内容,分别是:函数基本信息(包括参数数量、局部变量数量等)、字节码、常量表、Upvalue表、调式信息、子函数原型列表。由此可知,函数原型是一种递归结构,并且 Lua 源码中函数的嵌套关系会直接反映在编译后的原型里。

细心的你一定会想到这样一个问题:main.lua 里面只有一条打印语句,并没有定义函数,那么 Lua 编译器是怎么编译这个文件的呢?由于 Lua 是脚本语言,如果我们每执行一段脚本都必须要定义一个函数,岂不是很麻烦?所以这个吃力不讨好的工作就由 Lua 编译器代劳了。

Lua编译器会自动为我们的脚本添加一个 main 函数(即主函数),并且把整个程序都放进这个函数里,然后再以它为起点进行编译,那么自然就把整个程序都编译出来了。这个主函数不仅是编译的起点,也是未来 Lua 虚拟机解释执行程序时的入口。而我们写的 Hello World 程序被 Lua 编译器加工之后,就变成了下面这个样子。

function main()
    print("Hello World")
    return
end

把主函数编译成函数原型后,Lua 编译器会给它再添加一个头部(Header),然后一起 dump 成 luac.out 文件,这样一份热乎的二进制chunk文件就新鲜出炉了。

如果对上面这些内容不是太理解也没关系,目前只是从宏观的角度上简单聊一下,后续会详细解释。

查看二进制 chunk

二进制 chunk 之所以使用二进制格式,是为了方便虚拟机加载,然而对人类却不够友好,因为其很难直接阅读。而 luac命令兼具编译和反编译功能,使用 -l 选项可以将 luac 切换到反编译模式。

luac54 -l main.luac

反编译的结果如下:

里面的指令代表啥,暂时先不需要知道,后续会一一介绍。

小结

以上我们就简单介绍了一下 Lua,了解了一下它的定位,以及基本结构。下一篇文章,我们来学习 Lua 的数据结构。