Lua遊戲開發(一)---Lua語言

JuggLee發表於2015-11-12

介紹

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,儲存方式如下圖

當表需要增長時,最初表的兩個部分有可能都是空的。新的陣列部分的大小是滿足以下條件的最大的 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<k(一個內建引數,一般是 250)時,取 R(X)。 G[X]是全域性變數表的 X 域。U[X]是第 X 個 upvalue。


暫存器儲存在執行棧中,它們實質是一個陣列。因此存取暫存器是很快的。常數和全域性變數也存於不同陣列中,因此存取它們也很快。全域性變數表是一個普通的 Lua 表,雖然要通過雜湊法來存取其域,但其實是很高效的,因為它是由(對應於全域性變數名的)字串來索引的,而字串雜湊值是已被預先計算出來了的。

相關文章