前言与概述
出自书籍:李创.Lua设计与实现[M].北京.人民邮电出版社.2017:186.
前言
假如采用C++这样的编译型语言来开发游戏,那么典型的开发流程大致时这样的:擂起袖子来写了一大段代码,然后编译解决调试编译的错误,中间可能还要处理类似崩溃、段错误、内存泄露等问题。另外,由于重新编译了代码,又需要重启服务器,而重启过程中势必涉及数据的加载。总而言之,采用纯编译型语言开发的情况下,相当一部分时间并没有用在真正的业务逻辑开发中。
项目采用的是C++编写的核心引擎模块,暴露核心接口给Lua脚本层,网络数据的收发都在C++层完成,而业务逻辑采用Lua实现。这个架构也是很多游戏服务器采用的经典架构。使用这个架构来开发游戏服务时,不再会把大量的精力放在语言本身的问题上,而可以集中精力来做业务逻辑。另外,借助于 Lua 的热更新能力,整个开发过程中需要重启服务的次数并不多。
Lua作为一门诞生已经超过20年的语言,在设计上是非常克制的。在Lua5.1.4这个版本来说,已经是Lua发展了十几年之后稳定了很长时间的版本,其解释器加上周边的库函数等不过就是一万多行的代码量,而如果再进行精简,只需要吃透几千行代码就能明白其核心原理,这是一个性价比极高的诱惑。
Lua在设计上,从一开始就把简洁、高效、可移植性、可嵌入型、可扩展性等作为自己的目标。打一个可能不是太恰当的比方,Lua专注于做一个配角,作为胶水语言来辅助像C、C++这样的主角来更好地完成工作。当其他语言在前面攻城拔寨时,它在后方完成自己辅助的作用。在现在大部分主流编程语言都在走大而全的路线,在号称学会某一门语言就能能成为所谓的“全栈工程师”的年代,Lua始终恪守本分地做好自己的胶水语言的本职工作,不得不说是异类的存在。
“上善若水,守善利万物而不争”,简单、极致、强大的可扩展性,大概是能想到最适合用来描述Lua语言设计哲学的句子。
概述
我们首先对Lua语言的历史进行简单回顾,了解其发展经历及设计哲学。接下来,会对Lua的源码组织、函数命名等做一些介绍。Lua是C语言项目的典范,在这方面也做得很规范,整齐,这给我们阅读源码带来了便利。最后,我们简单介绍一下Lua虚拟机整体的工作流程。
前世今生
Lua语言于1993年诞生于巴西里约热 Pontifical Catholic 学(简称PUC-Rio )的Tecgraf 实验室,作者是Roberto Ierusalimschy、Luiz Henrique de Figueiredo 和Waldemar Celes。Tecgraf实验室创立于 1987年,主要专注于图形图像相关的工具研发,创立之后,该实验室的工作就是向客户提供基本的图形相关的软件工具,比如图形库、图形终端等。
1977年到1992年,巴西政府实施了“市场保护”政策,这使得计算机软硬件存在巨大的贸易壁垒。在这种大环境下,Tecgraf实验室的很多客户由于政治和经济上的原因,都不能国外公司购买定制化的软件。这些原因都驱使Tecgraf实验室的工作人员从头开始构建面向本国用户的软件工具。
Petrobras (巴西石油公司)是Tecgraf 最大客户之一,Tecgraf为其开发了两门语言,分别是DEL和SOL,这两门语言是Lua语言的前身。
语言(Lua)的前身之一是SOL语言,在葡萄牙语中这个单词的意思是“太阳”,它们决定给这门新的语言起名为Lua,葡萄牙语的意思是“月亮”。Lua语言就这样诞生了。
1996年对Lua来说是很重要的一年,Lua开始在国际上获得了关注,迎来国际用户。在这一年,作者在Softtware: Practice & Experience杂志上发表了一篇关于Lua的论文,引来了不少的关注。同年12 月, Lua 2.5版本发布,Dr. Dobb’s Journal杂志也专门针对Lua做了报告。由于这本杂志在程序员圈子里受众非常多,吸引了软件业中不少从业者注意,这其中包括当时任职于Lucas艺术旗下Grim Fandango游戏项目的主管Bret Mogilefsky。由于Lua的良好特性,他在自己的项目中使用Lua替换掉了项目原来用的脚本语言,后来又在Game Developers Conference (简称为GDC ,是游戏程序员最重要的会议之一)分享了自己使用Lua的成功经验。从此,Lua在游戏圈就开始流行起来了,这其中包括了后来大获成功的WOW等。如今Lua语言已经是游戏领域使用最广泛的脚本语言之一。
Lua虽然起源于巴西,也是从巴西公司的项目中受需求驱动而开发的,但是从一开始这门语言的设计者就把眼光投向世界。在很长一段时间里, Lua的文档只有英语版本,而不是作者的母语葡萄牙语。前面提到的1996年发表的论文,同样也可以看作Lua作者们国际化视野的一个标志。
Lua语言从一个开始就将自己定位成一个“嵌入式的脚本语言”,提供了如下的特性。
- 可移植性:使用clean C编写的解释器,可以在Mac、Unix、Windows等多个平台轻松编译通过。
- 良好的嵌入性:Lua提供了非常丰富的API,可供宿主程序与Lua脚本之间进行通信和交换数据。
- 非常小的尺寸:Lua 5.1 版本的压缩包,仅有208KB,解压缩之后也不过是835KB, 一张软盘就可以装下。Lua解释器的源代码只有 17 000多行的C代码,编译之后的二进制库文件仅有143KB,这些都决定了使用Lua的设备并不会因为添加了它导致非常明显的空间 占用。
- Lua 的效率很高,是速度最快的脚本语言之一:为了提高Lua的性能,作者们将最初使用Lex、Yacc等工具自动生成的代码都变成了自己手写的词法分析器和解析器。
这意味着,用户使用C、C++等语言进行主要功能的开发,而一些需要扩展、配置等会频繁动态变化的部分使用Lua语言来进行开发。 Lua语言的以上几个特性,都决定了它能很好地完成这些辅助作用。Lua的作者甚至戏称这门语言是一门能穿过针孔的语言( Passing a Language through the Eye of a Needle ),“小而精”大概是对Lua语言最好的描述了。
作为一门从发展中国家起源的语言,在一开始的选择和定位上,Lua都做了现在看来正确的选择:面向国际,老老实实做好辅助作用 。在一个点上做精做细,而不是走大而全的路线去与类似背景的语言进行竞争,这是Lua后来取得巨大成功的原因。这也能理解为什么过去了这么多年,至今Lua解释器的代码只有非常少的代码量(以本书中分析的5. 1.4版本来看,全部代码只有17 193行。如果只算核心部分,那就更少了)。
这也是一直很推崇Lua解释器源码,并且决定将这门语言进行分析的原因: Lua解释器的代码是殿堂级的语言代码范本,Lua作者对语言特性、设计目标、受众的取舍值得我们学习。一万多行的源码中,就能学习到一门工业级脚本语言的实现,性价比是极高的。
除了在游戏领域的广泛使用,Lua在其他领域也获得了运用。
- OpenResty使用Lua来扩展Nginx服务器的功能,使用者仅需要编写Lua代码就能轻松完成业务逻辑。值得一提的是,这个项目的作者是中国人章亦春。
- Redis服务提供Lua脚本。
- Adobe的Lightroom项目使用Lua来编写插件。
还有很多非游戏领域的成功项目,在此不一一列举了。
那么,如何在你的项目中使用Lua语言呢?以笔者比较熟悉的游戏服务器领域来说,一般是这样组织和分工的。
- C\C++语言实现的服务器引擎内核,其中包括最核心的功能,比如网络收发、数据库查询、游戏主逻辑循环等。以下将这一层简称为引擎层。
- 向引擎层注册一个Lua主逻辑脚本,当接收到用户数据时,将数据包放到Lua脚本中进行处理,主逻辑脚本主要是一个大的函数表,可以根据接收到的协议包的类型,调用相关的函数进行处理。以下将这一层简称为脚本层。
- 引擎层向脚本曾提供很多API,能方便地调用引擎层的操作,比如脚本层处理完逻辑之后调用引擎层的接口应答数据等。
可以看到,在这个架构中,引擎层实现了游戏服务的核心功能,这部分的变动相对而言不那么频繁;而游戏的逻辑、玩法是变动很频繁的,这部分使用脚本来完成。这个组合架构的优势在于如下几点。
- 编码效率高:由于引擎层相对稳定,而脚本不需要进行编译就能直接运行,省去了很多编译的时间。
- 开发效率高:大部分脚本,包括Lua在内都支持热更新功能,这意味着在调试开发期间,可以不用停服务器就能调试新的脚本代码,这省去了重启服务的时间,比如加载数据库数据、静态配置文件等的耗时。
- 对人员素质要求相对低: 一般的游戏服务器团队配置,都是由主程级别的人来把控引擎的质量,其他的成员负责编写脚本玩法逻辑,即使出错,大部分时候并不会导致服务器岩机等严重问题。
源码组织
讲解基于Lua 5.1.4版本进行分析,打开src目录下的Makefile文件,可以看到这样一段代码:
在Lua 5.3.6中这部分也是几乎一样的,如下:
从中能看到,Lua源码大体分为三个部分:虚拟机核心、内嵌库以及解释器、编译器。
虚拟机核心的文件列表如表1-1所示。需要补充说明的是,Lua解释器中,内部模块对外提供的接口、数据结构都以“lua模块名简称_”作为前缀,而供外部调用的API则使用"lua_”前缀。
TIPS:简单地说,供外部调用的API就是供C\C++这种角色使用的,而内部模块对外提供的接口、数据结构只供Lua内部使用。
文件名 | 作用 | 对外接口前缀 |
---|---|---|
lapi.c | C语言接口 | lua_ |
lcode.c | 源码生成器 | luaK_ |
ldebug.c | 调试库 | luaG_ |
ldo.c | 函数调用及栈管理 | luaD_ |
ldump.o | 序列化预编译的Lua字节码 | |
lfunc.c | 提供操作函数原型及闭包的辅助函数 | luaF_ |
lgc.c | GC(垃圾回收) | luaC_ |
llex.c | 词法分析 | luaX_ |
lmem.c | 内存管理 | luaM_ |
lobject.c | 对象管理 | luaO_ |
lopcodes.c | 字节码操作 | luaP_ |
lparser.c | 分析器 | luaY_ |
lstate.c | 全局状态机 | luaE_ |
lstring.c | 字符串操作 | luaS_ |
ltable.c | 表操作 | luaH_ |
lundump.c | 加载预编译字节码 | luaU_ |
ltm.c | tag方法 | luaT_ |
lzio.c | 缓存流接口 | luaZ_ |
文件名 | 作用 |
---|---|
lauxlib.c | 库编写时需要用到的辅助函数库 |
lbaselib.c | 基础库 |
ldblib.c | 调试库 |
liolib.c | IO库 |
lmathlib.c | 数学库 |
loslib.c | OS库 |
ltablib.c | 表操作库 |
lstrlib.c | 字符串操作库 |
loadlib.c | 动态扩展库加载器 |
linit.c | 负责内嵌库的初始化 |
Luau 5.3.6新增内嵌库如下:
文件名 | 作用 |
---|---|
lbitlib.c | bit位操作库 |
lcorolib.c | 协程库 |
lutf8lib.c | utf8库 |
lctype.c | 封装C标准库中的ctype文件 |
文件名 | 作用 |
---|---|
lua.c | 解释器 |
luac.c | 字节码编译器 |
Lua虚拟机工作流程
Lua虚拟机工作流程先大概了解一下。
Lua代码是通过翻译成Lua虚拟机能识别的字节码运行的,以此它主要分为两大部分。
- 翻译代码以及编译为字节码的部分。这部分代码负责将Lua代码进行词法分析、语法分析等,最终生成字节码。设计这部分的代码文件包括llex.c(用于进行词法分析)和lparser.c(用于进行语法分析),而最终生成的代码则使用了lcode.c文件中的功能。在lopcodes.h、lopcodes.c文件中,则定义了Lua虚拟机相关的字节码指令的格式以及相关的API。
- Lua虚拟机相关的部分。在第一步中,经过分析阶段之后,生成了对应的字节码,第二步就是将这些字节码装载到虚拟机中执行。Lua虚拟机相关的代码在lvm.c中,虚拟机执行的主函数是luaV_execute,不难想象这个函数是一个大的循环,依次从字节码中取出指令并执行。Lua虚拟机对外看到的数据结构是lua_State,这个结构体将一直贯穿到整个分析以及执行阶段。除了虚拟机的执行之外,Lua的核心部分还包括了进行函数调用和返回处理的相关代码,主要处理函数调用前后环境的准备和还原,这部分代码在ldo.c中,垃圾回收部分的代码在lgc.c中。Lua是一门嵌入式的脚本语言,这意味着它的设计目标之一必须满足能够与宿主系统进行交互,这部分代码在lapi.c中。