本节研究 lua 1.1 中的表对象, lua 内部称为 array, 底层实现为 hash, 类似于 php 中的 array(),
js 中的 {} 对象等. 先列出待研究的问题:
1. lua 对象的一般语法和使用, 示例.
2. 内部的底层是如何实现 lua 对象的? (底层实现问题, 内存布局问题)
3. 虚拟机的指令如何访问 lua 对象中的数据? (虚拟机和指令问题)
4. 如何生成这些指令的? (代码生成问题)
5. 表对象的构造语法及其相关指令支持的简述.
下面从第1个问题开始: lua 对象的一般语法和使用.
在 lua 1.1 中, 产生一个对象一般用 @{ ... } 或 @[...] 语法 (和后续 lua 版本语法略有不同), 例子:
a = @ { x=1, y=2, z=3 }
访问 a 中的元素如:
读取: b = a.x 或 b = a['x'], 写入: a.x = 5 或 a['xyz'] = 321
使用 a['123'] 方式访问, 可以使用非标识符(! NAME)的键来访问值.
问题2: 内部如何实现的?
内部使用 hash 作为 lua 表对象的底层实现, 代码位于在 hash.c 文件中.
可以简单的分为两部分:
1. lua_hashXXX() 一组函数, 用于实现底层 hash 表, 是一个典型的 hash 实现, 使用
链表法连接多个 hash node 以解决冲突.
2. lua_createarray() -- 创建表对象; lua_hashdefine() -- 查找/创建表项;
lua_next() -- 遍历 hash 表.
在 lua 的后期版本对 hash 实现做了很多改变, 故而这里宏观了解即可. hash 实现
细节应大同小异, 当前不需要详细了解.
问题3: 访问表对象内容的指令.
读写表对象内容的指令是 PUSHINDEXED 和 STOREINDEXED, 分别对应读写表项.
STOREINDEXED0 指令相当于 STOREINDEXED 指令操作数为 0 的优化.
在虚拟机执行函数 lua_exeucte() 中, 它们的执行为:
case PUSHINDEXED: // 执行此指令时栈顶元素为 [array, index )
index = pop(); array = pop(); // 栈中弹出 index,array 参数
value = lua_hashdefine(array, index) // 取 array[index] 的值, 如果没有返回 NIL
push(value) // 值压入栈中作为结果.
执行 STOREINDEXED0 指令时, 栈顶为 [array, index, value )
case STOREINDEXED0:
从栈中得到(弹出) value, index, array
lua_hashdefine(array, index) = value // 值插入到 array[index] 位置
执行 STOREINDEXED n 略有不同, 栈顶为 [array, index, ...(其它内容), value )
case STOREINDEXED:
n = *pc++; // array, index 所在位置的一个偏移量
从栈中弹出 value
array = *(top - array所在偏移量)
index = *(top - index所在偏移量)
lua_hashdefine(array, index) = value // 值插入到 array[index] 位置
这里有 STOREINDEXED 指令以及这样方式的处理, 是因为 lua 支持多赋值(以后有机会详细研究).
还有一些指令, 与 array 的创建/初始化有关, 在问题 5 中略说.
问题4: 读写表项目的代码如何生成的?
在语法文件中, 有读写表项目的两个产生式:
1. var -> var {代码块1} '[' expr1 ']' {代码块2}
2. var -> var {代码块1} '.' NAME {代码块3}
其中产生式1 实现语法形式 var['expr'], 产生式2 实现语法形式 var.NAME.
代码块1: 任务是产生将 $1 (var) 的值作为 array 压入栈中的代码, 因此运行时栈中为 [ array ).
代码块2: 之前, expr1 已经产生了值作为 index, 故而栈中为 [ array , index )
返回 var.$$ = 0
代码块3: 产生 PUSHSTRING NAME 的指令, 也即将字符串的 NAME 作为 index,
运行时此时栈为 [ array, NAME 作为 index ), 返回 var.$$ = 0
在 expr -> var 的产生式中, 使用 lua_pushvar() 函数, 由于 var.$$ = 0, 在该函数中区分:
> 0 -- 当做全局变量; < 0 -- 当做局部变量 (前面文章中已经提及)
= 0 -- 当做表项目访问, 产生代码 PUSHINDEXED.
写入相关的产生式:
varlist -> var ...
stat1 -> varlist '=' exprlist {代码块1}
非终结符 varlist 为 array 产生的代码为 PUSH array, PUSH index, exprlist 产生的代码为
PUSH value, 故而栈中为 [array, index, value ), 此后代码块1产生的指令为 STOREINDEXED0,
完成对 array[index] = value 的写入.
对于多赋值的情况, 会产生指令 STOREINDEX (也有 ADJUST), 以后再说.
迄今为止, Lua 虚拟机有三种寻址方式了:
1. 全局变量: PUSHGLOBAL, STOREGLOBAL (及其变体)
2. 局部变量: PUSHLOCAL, STORELOCAL
3. 表中项目: PUSHINDEXED, STOREINDEXED
问题5: 表构造语法和相关指令.
lua 1.1 的表构造语法为: (为容易理解做了改动)
array -> '@' optional-name '{' key=value-list '}'
array -> '@' optional-name '[' value-list ']'
其中 name 是可选的. 这一早期语法需要使用 '@' 符号前导, 后期版本的语法去掉了 @ 符号.
如果没有 @ 符号, {}, [] 语法就已经非常类似当今流行的脚本语言中定义对象和数组的语法了.
如果给出了 name, 则将其后面的对象当做 name 函数的参数, 调用名为 name 的函数, 这使得
可以写如下的配置语法: (这也是 lua 设计的一个目标...)
NPC {
name = '张三',
say = '欢迎 lua'
}
这样 PUSHOBJECT 指令被用于支持这种使用对象进行函数的调用(做准备工作).
为支持这种定义的语法, 几个相关的指令被实现:
CREATEARRAY: 创建一个空的 array.
STORELIST: 向 [] 方式定义的 array 中(批量)添加项目.
STORERECORD: 向 {} 方式定义的 array 中(批量)添加 key=value 对.
所谓批量, 指的是 lua 在创建 array 内容时, 一次在栈中保留数个 key/value 值, 然后用一条
STORExxx 指令一次性的添加到 array 中. 推论原因: 这样做可以减少指令的数量, 提高效率.