Lua游戏开发(一)---Lua语言

介绍

Lua的设计和实现目标:提供一种嵌入式的脚本编程语言,简洁、高效、可移植并且是轻量级的。

传统上大部分虚拟机都是基于堆栈的,自Pascal的P-虚拟机开始一直到今天的Java虚拟机以及Microsoft.Net。Lua5.0的虚拟机是基于寄存器的虚拟机,Perl6(Parrot)也是。

简洁:寻求最简化的语言和最小化的源码(以C语言实现)。这也意味着Lua只有一些类似传统编程语言的简单的语法和少量的语言结构。

可移植:我们希望Lua能够在尽可能多的平台上运行。希望Lua内核能够在不做任何修改的情况下,在任何平台下都能顺利通过编译。并且希望Lua程序在任何平台下都不需要修改就能顺利执行,只要该平台上又一个Lua解释器。这也意味着需要用纯ANSI C实现Lua并注意移植问题,避开C语言及其库的阴暗面,并保证在C++编译器上也能顺利通过编译,而不希望看到警告信息。

可嵌入:Lua是一种可扩展的语言,我们希望能够容易地将Lua嵌入到应用程序中。

值的内部表示

Lua是动态类型的语言:类型是与值相关而不是与变量相关。Lua有8种基本的值类型:nil,boolean,number,string,table,function,userdate和thread。

nil:是标记类型,只有一种值,就是nil。

boolean:有true和false两种值。

number:双精度浮点数,对应C语言的double,不过可以在编译Lua的时候将其设置为float或long型。(一些小型机缺乏支持double数据类型的硬件)

string:字节数组,有一个显示的长度,因此可以容纳任何二进制数,包括0。

table:关联数组,可以通过任何值(除nil)来索引,也能容纳任意值。

function:可以是Lua函数或根据Lua虚拟机接口函数的原型编写的C函数。

userdata:一个指向用户内存块的指针,分两种情况:

    heavy,内存由Lua分配,并由垃圾回收机制负责处理;

    light,内存由用户分配并释放。

thread:代表协程。

任何类型都是first-class的:可以被存入全局变量、局部变量或table域中,或作为实际参数传递给参数,或从函数中返回。

Lua将值表示成带标志的联合结构,即(t,v)对,其中t是个整数,代表值v的类型,v是一个C语言union类型数据结构,存储实际的值。

typedef struct {
    int t;
    Value v;
} TObject;

typedef union {
    GCObject *gc;
    void *p;
    lua_Number n;
    int b;
} Value;

Value的n域用于表示number;b域用于表示boolean;p域用于表示light userdata;gc域用于需要垃圾回收机制处理的其他值(如 string, table,function,heavy userdata,thread等)

Lua用一个散列表将string内部化:Lua为每个字符串只保留一份拷贝,而且字符串是不变的:一旦内部化,字符串将不可更改。

表是Lua中唯一的表示数据结构的工具。Lua中没有内置对数组类型的支持,数组使用表和整数索引来模拟的。例如,在Perl中,如果你试图执行程序:$a[10000000000]=1;可能导致内存不足,因为这会导致Perl创建一个用用10亿个元素的数组,而等价的Lua程序:a={[10000000000]=1};创建的表只有一个表项。

截至Lua4.0版,表都是严格的以散列表(哈希表)实现的:所有的键、值都明确的存在于表中。在Lua5.0,表以一种混合型数据结构来实现,它包含一个散列表部分和一个数组部分。对于键、值对"x"->9.3,1->100,2->200,3->300,存储方式如下图

Lua游戏开发(一)---Lua语言_第1张图片

当表需要增长时,最初表的两个部分有可能都是空的。新的数组部分的大小是满足以下条件的最大的 n 值:1到 n 之间至少一半的空间会被利用(避免像稀疏数组一样浪费空间);并且 n/2+1到 n 之间的空间至少有一个空间被利用(避免 n/2 个空间就能容纳所有数据时申请 n 个空间而造成浪费)。当新的大小计算出来后,Lua 为数组部分重新申请空间,并将原来的数据存入新的空间。举例来说,假设 a 是一个空表,散列表部分和数组部分都是 0 大小。如果执行 a[1]=v,那么表就需要增长以容纳新键。Lua 会选择 n=1 作为新数组的大小(存储一个数据 1→v)。散列表部分仍保持为空。

这种混合型结构有两个优点。第一,存取整数键的值很快,因为无需计算散列值。第二,也是更重要的,相比于将其数据存入散列表部分,数组部分大概只占用一半的空间。

函数和闭包

线程和协同程序(协程)

虚拟机

Lua解释器在执行Lua程序时,首先将源码编译成虚拟机指令(opcode,操作码),然后执行这些指令。对每一个被编译的函数,Lua 为其创建一个原型,原型中含有一个由该函数的虚拟机指令组成的数组、一个所有被该函数用到的常数值(TObjects,字符串或实数)的数组(译者注:这很重要,因为这避免了在指令码中直接包含常数值进而导致指令长度的膨胀。事实上,可以把这些常数看成具有只读属性的全局变量,对它们的处理和全局变量的处理是一致的)。

在十年的时间里(从 1993 年 Lua 发布开始),各种版本的 Lua 都使用基于堆栈的虚拟机。从 2003 年开始,随着 Lua5.0 的发布,Lua 改用个基于寄存器的虚拟机。新的虚拟机也用堆栈分配活动记录,寄存器就在该活动记录中。当进入Lua 程序的函数体时,函数从栈中预分配一个足以容纳该函数所有寄存器的活动记录。函数的所有局部变量都各占据一个寄存器。因此,存取局部变量是相当高效的。

使用寄存器式虚拟机消除了用堆栈式虚拟机时为了在栈中拷贝数据而必需要的大量出入栈(push/pop)指令。在 Lua 中,这些出入栈指令相当费时,因为它们需要拷贝带标志的值(tagged value, TObject)正如第三节讨论过的。因此寄存器结构既消除了昂贵的值拷贝操作,又减少了为每个函数生成的指令码数量。Davis al.[6]反对基于寄存器的虚拟机,并以了 Java 虚拟机的字节码作为反证。某些编译器作者也因为可以很容易在编译期间为堆栈式虚拟机生成代码而反对寄存器式虚拟机。

与寄存器式虚拟机相关的两个难题是:指令大小和译码速度。寄存器式虚拟机的指令需要指明操作数位置,因此通常要比堆栈式虚拟机的同类指令长。(例如,当前 Lua 虚拟机的指令长度是 4 字节,而其他许多典型的堆栈式虚拟机的指令长度只有 1-2 字节,包括前一版本的 Lua 也是。)另一方面,为基于寄存器的虚拟机生成的操作码要比堆栈式虚拟机少,因此指令总长度大不了多少。堆栈式虚拟机的许多指令都有隐含的操作数。而寄存器式虚拟机中对应的指令需要从其中解码出操作数。解码过程增加了解释器的负担。有几个因素会淡化这种负面影响,第一,堆栈式虚拟机也花费一些时间处理隐含的操作数(例如,增减栈指针)。第二,由于寄存器式虚拟机中所有操作数都在指令中,而指令是一个机器字,对操作数的解码过程只包含一些很廉价的操作,如逻辑运算。另外 ,堆栈式虚拟机的指令常常需要多字节操作数。如 Java 虚拟机 JVM 的跳转和分支指令用了两字节的偏移量。出于对齐的关系,解释器无法一次获取这样的操作数(至少对于可移植的代码来说是如此,因为它必须总是假定最坏对齐的限制条件)。在寄存器式虚拟机上,由于操作数在指令里面,解释器无需单独获取它们。Lua 虚拟机有 35 条指令。大部分的指令直接对应着语言结构:数学运算、表的创建和索引、函数和方法调用、存取变量值等。也有一套用于实现控制结构的常规跳转指令。下图 列出了全部指令及其简短用法。一些符号的意义如下:R(X)是第 X 个寄存器;K(X)是第 X 个常数;RK(X)是 R(X)或 K(X-k),取决于 X 的值---当 X

Lua游戏开发(一)---Lua语言_第2张图片

寄存器保存在运行栈中,它们实质是一个数组。因此存取寄存器是很快的。常数和全局变量也存于不同数组中,因此存取它们也很快。全局变量表是一个普通的 Lua 表,虽然要通过散列法来存取其域,但其实是很高效的,因为它是由(对应于全局变量名的)字符串来索引的,而字符串散列值是已被预先计算出来了的。

你可能感兴趣的:(lua,lua,游戏开发)