Lua其语言
“Lua 是一个小巧的脚本语言。是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组,由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo所组成并于1993年开发。 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。”[1]
玩过WOW的人也许对lua并不陌生,著名的WOW大脚插件便是基于lua写的。
lua的设计理念足够小巧跟专注,所以它只实现了本语言的必要脚本逻辑功能,类似IO库、CGI库等的库是不存在的,由此决定了绝大多数情况下,它只适合作为嵌入式语言来完成逻辑,而不能像php、python一样,可以直接独立完成一个应用程序。
Lua 的位置
语言层级
语言 | 描述 |
---|---|
脚本 | 即Lua所在层,跑在高级语言层的解释器,会对脚本逻辑进行运行时解释 |
高级语言 | 比如C++、C#等,经过gcc或者vs等编译器,链接、编译为汇编程序 |
汇编语言 | 比如masm。汇编语言已经可以直接操作内存地址、CPU寄存器等,非常底层而不会有高级语言的类型检查等高阶功能 |
操作系统 | 各类硬件的driver都属此列。操作系统将汇编转为机器能读的机器语言 |
机器语言 | 硬件厂商会刷进硬件的一些固化的操作。机器语言比汇编更加细致地进行硬件级操作,这也是普通人能接触到的最底层的部分了 |
微指令 | 机器语言到微指令,类似于汇编到机器码的关系 ,它其实是外界的一种合理猜测,此层的真实面目未知(个人没有见到) |
机器码 | 010101010… |
一般应用框架
以服务端举例,一些拥有相对完备的库的语言,一般可以采用类似的框架设计:
lua的作为轻量级定位的“嵌入式”语言,目前拥有的库较为简单,没有办法采用类似上述的框架,而有当下有常见的、另外两种用法。
依靠其他语言为底层,作为比其他语言更高层级的纯逻辑层:
服务器框架中,skynet以及我们的项目便是此种用法。
或者再退化一点,外挂在其他高级语言上,作为一种辅助部分(可以是配置读取工具,可以是比如活动等部分简单逻辑):
也常见于其他的服务端框架。
Lua的语法
lua 的语法较为简单且严谨,在后续的学习中会详细了解到。
同时可以参阅更为详细周到的、lua中文翻译的《programing in Lua》。
代码块
lua里代码块称之chunk。在C/C++中,chunk用{}花括号来表示,在python中,chunk用行的缩进表示。在lua中,chunk使用 end 关键字来匹配表示。比如
function foo()
-- function foo
end
if a > b then
-- do something
end
强语言、弱语言
强类型语言,如c语言,会要求变量在使用前声明其类型。
char c = "a";
lua并不要求对变量的类型声明,变量只有“tag”作用,运行期间的值类型是可变的,属于弱语言。
local c = "a"
c = 1
流程控制
判断流程,使用关键字 if else elseif
循环流程,有关键字 for while repeat
错误跳转,有 pcall、xpcall 关键字可以使用
函数执行,使用 function 关键字
内存管理
lua使用的内存会在超出其作用域之后,自动释放。自动释放内存的过程叫gc(garbage collect)。而并不需要显式的 alloc 以及 free。
gc通过一种叫marked_tree(给树涂色)的算法来进行。如果需要回收的对象过多,会导致gc树的规模庞大,而引起可能的程序卡顿。所以在使用lua时,应当注意避免频繁的临时变量产生以及过多的小对象申请。
面向对象
lua并没有提供原生的面向对象的语法。但有metatable功能提供,可以利用它来实现具体的面向对象功能。但个人建议,不要在lua中大量使用面向对象,而是多使用组合(combine)。
多文件
lua使用require关键字,来对分处不同文件的代码进行管理。进而实现lua的模块化。
数据类型
数值型
lua无分浮点型跟整型,所有的数字都是“数值型”。“数值型”在底层会自动适应浮点型和整型。
local num = 1.0001
字符串型
lua的字符串分长字符串跟短字符串(界限长度由编译的宏定义决定,5.2.5版本此值默认为长度20)。短字符串在内部会进行“字符串内部化”,换句话说就是短字符串在整个lua实例里都是对字符串唯一实例的引用。
local str = "test"
闭包型
也就是lua的“函数”类型。严格来说,lua没有直接暴露出真正的函数,所有的认识上的“函数”,其实都是闭包,而函数则是闭包下的原型(proto)。
local func = function()
end
table型
table的使用非常简便,它像是 vector + hash 的结合体。根据不同的使用场景,table的底层数据存储结构,会自动适应数组或者是哈希又或者是两者的结合体。这在后面会详细去论证。
-- 数组用法
local array = {}
table.insert(array, 1)
-- 哈希用法
local hash = {}
hash["a"] = 1
hash.b = 2
table还有一种进阶的特殊形式,弱表(weak table)。用以自动释放已经无引用的键值或者元素。
另外table还能设置自己的元表(meta table),提供对自己访问的控制能力。
nil型
lua并没有delete操作,而是使用nil值来进行“删除”。
local tb = { a = 2 }
tb.a = nil -- 刪除 a (将a置空)
当然由于这种特殊的操作,有时候置为nil,跟原本就不存在,是有一定的区别的。
boolean型
true or false
local b = true
userdata型
这是C/C++等底层所创建出来,用来供lua引用使用的结构。lua本身并不会产生userdata。
thread型
也就是协程(Coroutine)。lua并没有真正的thread,而是使用了协程的概念。
可以这么理解:传统的 thread 编程,是“多核多线程”,协程则是“单核多线程”。在同一时间,只会有一个协程在执行,直到运行的协程换出。
这样的好处是能够做到一定程度的异步(比如在一个协程读数据库换出时,另一个协程可以被唤醒做其他的工作),同时也不会让写程序的人有写锁类的烦恼。
解释原理
基于寄存器的解释器和基于栈的解释器
lua从5.0版本开始,它的解释器实现转为基于寄存器的解释器(Register-based virtual machine)。
不像JVM(java虚拟机)的基于栈的解释器(Stack-based virtual machine),lua的解释器的指令可以做到更短、更有效率。
举一个简单的 1 + 2 例子。
栈解释器原理是:
top |
---|
1 <-sp |
2 |
1001 |
2031 |
buttom |
执行:
pop 出来 1
pop 出来 2
ADD 1, 2, result(3)
push result
才能得到下面的结果:
top |
---|
3 <- sp |
1001 |
2031 |
buttom |
而lua在基于寄存器的情况下,指令就会变得很简单,在1、2都在寄存器的情况下,直接运算出来结果并放入寄存器就行:
寄存器 | 内容 |
---|---|
L1 | 1 |
L2 | 2 |
L3 | N/A |
L4 | N/A |
指令: ADD L1, L2, L3
就可以将结果放入到 L3 中进行使用了。
lua 的栈具体的实现会在很后期,我们一起探讨。
有兴趣可以使用工具ChunkSpy来查看lua生成的虚拟机指令(Vm Instructions)。
Lua的虚拟栈(交互栈)
lua 通过栈结构来进行数据的交互(注意此栈非上文中的虚拟机栈)。
比如下面的栈中,存入了一个名字为tb的table结构
local tb =
{
a_key = "a_val"
}
栈内容 | 正索引 | 负索引 |
---|---|---|
tb | 2 | -1 |
other | 1 | -2 |
需要取出 tb.a 的值,并压栈,那么过程是
1、先将 a_key 字符串(key)压栈
栈内容 | 正索引 | 负索引 |
---|---|---|
“a_key” | 3 | -1 |
tb | 2 | -2 |
other | 1 | -3 |
2、调用函数,读出在 -2 中的表,并pop -1 的key值,解析table获取到val值,再将val值重新放入到栈顶
栈内容 | 正索引 | 负索引 |
---|---|---|
“a_val” | 3 | -1 |
tb | 2 | -2 |
other | 1 | -3 |
上面的栈操作过程,就是lua源码中的 lua_gettable 函数。
总结
至此,我已经描绘了lua的大概轮廓。更加深入详细的信息,从lua的基本语法,到源码实现,我会一点一点进行总结,观众老爷们可以关注我的更新,跟我一起探讨。
ciao!
[1] 百度百科