Lua DDJ 文章

轉載Dr. Dobb's Journal 21 #12(1996 年 12 月)26–33 頁。版權所有 © 1996 Miller Freeman, Inc.

Lua:一種可延伸的嵌入式語言
少數的元機制取代了大量的功能

作者:Luiz Henrique de Figueiredo、Roberto Ierusalimschy 和 Waldemar Celes

近年來,已經提出許多微型語言,用於擴充和自訂應用程式。一般而言,這些擴充語言應具備下列特點

由於擴充語言並非用於撰寫大型軟體,因此支援大型程式設計的機制(例如靜態類型檢查和資訊隱藏)並非必要。

我們在此介紹的可延伸嵌入式語言 Lua 符合這些需求。它的語法和控制結構簡單且熟悉。Lua 很小,整個實作不到 6000 行 ANSI C。除了大多數程序語言共有的功能之外,Lua 還具有使其成為強大高階可延伸語言的特殊功能

Lua 是一種通用嵌入式程式設計語言,旨在支援具有資料描述功能的程序設計。儘管它不在公有領域(TeCGraf 保留版權),但 Lua 可在 https://lua.dev.org.tw/ 免費取得,供學術和商業用途。此套件還包括一個數學函式(sincos 等)、I/O 和系統函式,以及字串處理函式的標準函式庫。這個選用函式庫會為系統新增約 1000 行程式碼。此外,還包括一個偵錯程式和一個獨立編譯器,可產生包含位元組碼的可攜式二進位檔。此程式碼可以在大多數 ANSI C 編譯器中不變更地編譯,包括 gcc(在 AIX、IRIX、Linux、Solaris、SunOS 和 ULTRIX 上)、Turbo C(在 DOS 上)、Visual C++(在 Windows 3.1/95/NT 上)、Think C(MacOS)和 CodeWarrior(MacOS)。

所有外部識別碼都以lua為前綴,以避免在與應用程式連結時發生名稱衝突。甚至 yacc 所產生的程式碼也會通過 sed 篩選器來符合此規則,如此一來便可以將 Lua 與使用 yacc 作為其他用途的應用程式連結。

Lua 實作

Lua 以一個小型 C 函式庫的形式提供,可連結至主機應用程式。例如,最簡單的 Lua 程式碼是 程式碼清單一 中的互動式獨立直譯器。在這個程式中,函式lua_dostring會呼叫直譯器來處理包含在字串中的程式碼區段。每一段 Lua 程式碼可能包含陳述式和函式定義的混合。

標頭檔 lua.h 定義了 Lua 的 API,其中包含約 30 個函式。除了lua_dostring之外,還有lua_dofile函式可用來直譯包含在檔案中的 Lua 程式碼,lua_getgloballua_setglobal可用來處理 Lua 全域變數,lua_call可用來呼叫 Lua 函式,lua_register可用來讓 C 函式可以從 Lua 存取,等等。

Lua 的語法有點類似 Pascal。為了避免懸掛的elseifwhile等控制結構會以明確的end作結。註解遵循 Ada 慣例,以 "--" 開頭,並執行到該行的結尾。Lua 支援多重指定;例如,x, y = y, x會交換xy的值。同樣地,函式也可以傳回多個值。

Lua 是一種動態型別語言。這表示值有型別,但變數沒有,因此沒有型別或變數宣告。在內部,每個值都有標籤來識別其型別;標籤可以在執行階段使用內建函式type來查詢。變數是無型別的,可以儲存任何型別的值。Lua 的垃圾回收機制會追蹤哪些值正在使用中,並捨棄未使用的值。

Lua 提供nilstringnumberuser datafunctiontable型別。nil是值nil的型別;其主要特性是它與任何其他值都不同。這很方便用作變數的初始值,例如。number型別表示浮點實數。string具有通常的意義。user data型別對應於 C 中的通用void*指標,並表示 Lua 中的主機物件。所有這些型別都很有用,但 Lua 的靈活性歸功於函式和表格,這是從 Lisp 和 Scheme 中學到的兩個關鍵教訓的結果

Lua 中的函式值可以儲存在變數中,作為參數傳遞給其他函式,儲存在表格中,等等。

當你宣告一個 Lua 函式(請參閱 清單二),函式主體會預先編譯成位元組碼,建立一個函式值。這個值會指定給一個具有指定名稱的全球變數。另一方面,C 函式是由主機程式透過適當的 API 呼叫提供的。Lua 無法呼叫尚未由其主機註冊的 C 函式。因此,主機完全控制 Lua 程式可以執行的動作,包括任何對作業系統的潛在危險存取。

表格

表格對 Lua 來說就像清單對 Lisp 一樣:強大的資料結構機制。Lua 中的表格類似於關聯陣列。關聯陣列可以用任何類型的值作為索引,而不仅仅是數字。

許多演算法在使用關聯陣列實作時會變得微不足道,因為搜尋它們的資料結構和演算法是由語言隱式提供的。Lua 將關聯陣列實作為雜湊表。

與實作關聯陣列的其他語言不同,Lua 中的表格不受變數名稱約束。相反地,它們是動態建立的物件,可以像傳統語言中的指標一樣進行操作。換句話說,表格是物件,而不是值。變數不包含表格,只包含對它們的參考。指定、參數傳遞和函式傳回值始終會操作對表格的參考,並且不暗示任何類型的複製。雖然這表示在使用表格之前必須明確建立表格,但它也允許表格自由地參考其他表格。因此,Lua 中的表格可以用來表示遞迴資料類型,並建立通用圖形結構,甚至包含循環的圖形結構。

表格透過使用欄位名稱作為索引來模擬記錄。Lua 透過提供 a.name 作為 a["name"] 的語法糖,讓這件事變得更容易。集合也可以透過將其元素儲存在表格的索引中來輕鬆實作。請注意,表格(因此集合)不必是同質的;它們可以同時儲存所有類型的值,包括函式和表格。

Lua 提供建構函式,一種用於建立資料表的特殊表達式,這對於初始化清單、陣列、記錄等非常方便。請參閱 範例 1

使用者自訂建構函式

有時您需要更精細地控制正在建立的資料結構。遵循僅提供少數一般後設機制的理念,Lua 提供使用者自訂建構函式。這些建構函式寫成 name{...},這是 name({...}) 的更直觀版本。換句話說,使用此類建構函式會建立一個資料表,初始化它,並將其作為參數傳遞給函式。此函式可以執行任何需要的初始化,例如動態類型檢查、初始化不存在的欄位,以及輔助資料結構更新,甚至在主程式中也是如此。

使用者自訂建構函式可用於提供更高級別的抽象。因此,在具有適當定義的環境中,您可以撰寫 window1=Window{x=200, y=300, color="blue"} 並考慮「視窗」,而不是純粹的資料表。此外,由於建構函式是表達式,因此它們可以巢狀以宣告式樣式描述更複雜的結構,如 清單四 中所示。

物件導向程式設計

由於函式是一等值,因此資料表欄位可以參照函式。這是朝向物件導向程式設計邁出的一步,而且透過簡化定義和呼叫方法的語法,讓這項工作變得更容易。

方法定義寫成 範例 2(a),這等於 範例 2(b)。換句話說,定義方法等於定義函式,並有一個稱為 self 的隱藏第一個參數,以及將函式儲存在資料表欄位中。

方法呼叫寫成 receiver: method(params),這會轉換成 receiver.method(receiver,params)。方法的接收器會傳遞為方法的第一個引數,讓參數 self 具有預期的意義。

這些建構不提供資訊隱藏,因此純粹主義者可能會(正確地)聲稱物件導向的重要部分遺失了。此外,Lua 不提供類別;每個物件都攜帶自己的方法分派資料表。儘管如此,這些建構極為輕量,而且類別可以使用繼承進行模擬,這在其他基於原型語言(例如 Self)中很常見。

後備

因為 Lua 是一種非類型語言,因此可能會發生許多異常的執行時間事件:將算術運算套用在非數字運算元、對非表格值進行索引、呼叫非函數值。在類型化的獨立語言中,有些條件會由編譯器標記;其他條件則會導致在執行時間中止程式。嵌入式語言中止其主機程式是很不禮貌的,因此嵌入式語言通常會提供錯誤處理的掛鉤。

在 Lua 中,這些掛鉤稱為「備援」,也用於處理不完全是錯誤條件的情況,例如存取表格中不存在的欄位和發出垃圾回收訊號。Lua 提供預設的備援處理常式,但你可以呼叫內建函式 setfallback 來設定自己的處理常式,並提供兩個引數:識別備援條件的字串(請參閱 表 1),以及在發生條件時要呼叫的函式。setfallback 會傳回舊的備援函式,因此必要時可以串連備援處理常式

透過備援進行繼承

備援最有趣的用途之一是在 Lua 中實作繼承。簡單繼承允許物件在稱為其「父項」的另一個物件中尋找不存在欄位的值;特別是,此欄位可以是方法。這種機制是一種物件繼承,與 Smalltalk 和 C++ 中採用的更傳統類別繼承不同。

在 Lua 中實作簡單繼承的方法之一是將父項物件儲存在一個不同的欄位中,例如 parent,並設定一個「索引」備援函式;請參閱 清單五。此程式碼定義一個函式 Inherit,並將其設定為索引備援。每當 Lua 嘗試存取物件中不存在的欄位時,備援機制就會呼叫函式 Inherit。此函式首先檢查物件是否有一個包含表格值的欄位 parent。如果是,它會嘗試在父項物件中存取所需的欄位。如果欄位不存在於父項中,則會自動再次呼叫備援。此程序會重複「向上」執行,直到找到欄位的數值或父項鏈結束。當需要更好的效能時,可以使用 Lua 的 API 在 C 中實作相同的繼承架構。

反射式功能

作為一種直譯式語言,Lua 提供一些反射式功能。其中一個範例是前面提到的函式 type。其他強大的反射式函式包括遍歷表格的 next,以及遍歷所有全域變數的 nextvar。函式 next 會取得兩個引數,一個表格和表格中的索引,並傳回一些實作相依順序中的「下一個」索引。(請記住,表格是實作為雜湊表。)它也會傳回與表格中索引關聯的值。(請記住,Lua 中的函式可以傳回多個值。)函式 nextvar 有類似的行為,但它遍歷全域變數,而不是表格的索引。

使用反射式功能的一個有趣範例是動態類型化。如前所述,Lua 沒有靜態類型化。然而,有時檢查給定值是否具有正確的類型以防止程式出現奇怪的行為是有用的。使用 type 可以輕鬆檢查簡單的類型。但對於表格,我們必須檢查所有欄位是否存在且正確填寫。

使用 Lua 的資料描述功能,你可以使用值來描述類型:單一類型由其名稱描述,而表格類型由將每個欄位對應到其所需類型的表格描述(清單六)。有了這些描述,你可以撰寫一個單一的、多型的函式來檢查值是否具有給定的類型;請參閱 清單七

反身機制也允許程式處理自己的環境。例如,程式可以建立一個「受保護的環境」來執行另一段程式碼。這種情況在基於代理的應用程式中很常見,當主機執行透過網際網路接收的不可信程式碼時(例如,包含可執行內容的網頁,在 Java 的時代是很流行的一件事)。一些擴充語言必須提供特定的支援來確保執行安全,但 Lua 足夠靈活,可以使用語言本身來執行此操作。清單八 顯示如何將整個全域環境儲存在一個表格中。類似的函式會還原已儲存的環境。由於所有函式都是指派給變數的一等值,因此從全域環境中移除函式非常容易。清單九 顯示一個在受保護的環境中執行一段程式碼的函式。

將 Tk 繫結到 Lua

Lua 的自然用途是 GUI 的說明,您需要有描述物件(小工具)層級的機制,並將使用者動作繫結到這些物件。Lua 適合此類任務,因為它結合了資料描述機制與簡單、強大且可擴充的語意。事實上,我們已經使用 Lua 開發了多個 UI 工具組。

儘管 Tk 是多功能的 GUI 工具組,但 Tcl 並不是所有人都感到自在的語言。由於我們將 Lua 視為 Tcl 的替代方案,因此我們決定實作 Tk/Lua 繫結,允許從 Lua 存取 Tk 小工具。

在可能的情況下,我們保留了 Tk 的哲學,包括小工具名稱、屬性和命令。眾所周知,嘗試改善現有的 API 是一件很誘人的事,但從長遠來看,讓它保持原樣對 Tk 使用者來說比較好,因為他們不必學習新的概念。(對我們來說也比較好,因為我們不必撰寫新的手冊!)

建立 Tk/Lua 小工具

我們已將所有 Tk 小工具對應到 Lua。您可以使用 Lua 表格建構函式來建立小工具並描述其屬性。例如,範例 3(a) 建立一個按鈕並將其儲存在 b 中;b 現在是一個代表按鈕的物件。在此定義之後,您可以使用一般的 Lua 語法來處理物件。因此,指定 b.label="Hello world from Lua!" 會變更按鈕的標籤,如果按鈕已顯示在螢幕上,則會更新其影像。可以建立一個限制在 20 個字元的文字輸入小工具,方法是使用 e=entry{width=20}。在將此小工具對應到顯示的視窗後,e.current 會包含使用者指派給小工具的目前值。(我們使用 current 欄位來儲存小工具值,而不是像在 Tcl/Tk 中使用全域變數。)

小工具不會自動對應到視窗上。與 Tk/Tcl 環境不同,Tk/Lua 中沒有目前視窗的概念。您必須建立一個視窗(可以是主視窗或頂層小工具)來容納其他小工具,然後明確將它對應到螢幕上;請參閱 範例 3(b)

這樣一來,使用者可以自由描述他們的對話框,甚至交叉參照小工具並在必要時對應它們。我們也消除了明確封裝小工具的需求,因為以描述性方式指定配置會更自然。因此,視窗(主視窗和頂層視窗)和框架小工具會用作自動封裝其內容的容器。例如,若要顯示具有兩個底部按鈕的訊息,您可以撰寫 範例 3(c)。除了所有一般 Tk 小工具外,我們還實作了兩個額外畫布,一個使用簡化的 API 對應到 Xlib,另一個使用 OpenGL。

這些函式庫提供的幾乎所有函式都已對應到 Lua。因此,您可以建立使用自訂小工具上直接操作的精緻圖形應用程式,而且全部只用 Lua。

存取小工具指令

所有 Tk 小工具指令都實作為 Tk/Lua 中的物件方法。它們的名稱、參數和功能都已保留。如果 lb 代表清單方塊小工具,則 lb:insert("New item") 會在清單中插入新項目,遵循清單方塊的 Tk insert 指令。另一方面,最常使用的 Tk 小工具指令 configure 已不再需要,因為其效果現在可透過簡單的指定來取得。

主視窗和小工具繼承視窗管理員的方法。如果 w 代表視窗,則 w:iconify() 會有其一般效果。

幕後花絮

實作 Tk/Lua 並不困難。使用 Tcl/Tk 的 C 介面,我們建立一個服務提供者並註冊它以從 Lua 存取。實作繫結的 Lua 程式碼使用物件導向方法,搭配前面提到的索引備援。每個小工具實例都從類別物件繼承,其中小工具類別位於階層的頂端。此類別提供所有小工具使用的標準方法。清單十 顯示此通用類別的定義及其將焦點設定到小工具的方法。它也顯示按鈕類別定義。

正如您現在所知,每個小工具都是使用表格建構函式建立的。建構函式設定實例類別、建立小工具並將它儲存在全域陣列中。不過,我們也使用了一個小技巧,建構函式不會傳回新表格,而是傳回小工具位置作為數字 ID (清單十一)。

因此,當 Lua 嘗試索引小工具,例如在 b.label 中,它會呼叫後備,因為數字無法被索引。此技巧讓我們可以完全控制小工具語意。例如,如果 b 是按鈕(實際上,b 會儲存 ID),而您設定 b.label = "New label,",則後備會負責呼叫適當的服務指令來更新小工具。

清單十二 顯示 Tk/Lua 的「可設定」後備函式。此後備會在每次嘗試索引非表格值時被呼叫。首先,我們會檢查第一個參數是否對應到有效的小工具 ID。如果是,我們會透過存取全域陣列來擷取小工具表格。否則,我們會將發生情況傳送給先前註冊的後備。

tklua_IDtable 表格中的小工具有一個稱為 tkname 的內部欄位,用來儲存對應的 Tk 小工具名稱。此名稱用於呼叫 Tk 指令。我們會檢查是否有對應的 Tk 小工具,以及索引值是否為有效的 Tk 屬性。如果是,我們會要求服務提供者變更小工具屬性(呼叫已註冊的 C 函式 tklua_configure)。指定 h[f]=v 可確保我們可以使用小工具表格來儲存 Tk 屬性以外的值。

實作「可取得」後備的方式類似。除了這兩個後備之外,Tk/Lua 也會使用索引後備來實作繼承(清單五),以及使用「函式」後備來呼叫小工具指令或視窗管理員指令。

結論

擴充語言總是會以某種方式被解釋。簡單的擴充語言可以直接從原始碼中被解釋。另一方面,嵌入式語言通常是具有複雜語法和語意的強大程式語言。嵌入式語言現在有一個更有效率的實作技巧:設計一個適合語言需求的虛擬機器,將擴充程式編譯成此機器使用的位元組碼,然後透過解釋位元組碼來模擬虛擬機器。我們選擇此混合架構來實作 Lua,因為詞彙和語法分析只會執行一次,因此執行速度較快。此外,它允許擴充程式只以預先編譯的位元組碼形式提供,因此載入速度較快且環境較安全。

範例 1: (a) 中的表格定義等同於 (b)。

(a)
     t = {}  -- empty table
     t[1] = i
     t[2] = i*2
     t[3] = i*3
     t[4] = i+j

     s = {}
     s.a = x   -- same as s["a"] = x
     s.b = y

(b)
     t = {i, i*2, i*3, i+j}
     s = {a=x, b=y}

範例 2: (a) 中的方法定義等同於 (b)。

(a)
     function object:method(params)
       ...
     end

(b)
     function object.method(self, params)
       ...
     end

範例 3: (a) 建立並儲存按鈕;(b) 明確地將視窗對應到螢幕上;(c) 顯示包含兩個底部按鈕的訊息。

(a)
     b = button{label = "Hello world!"
                command = "exit(0)"
               }


(b)
     w = toplevel{b}
     w:show()

(c)
     b1 = button{label="Yes", command="yes=1"}
     b2 = button{label="No", command="yes=0"}
     w  = toplevel{message{text="Overwrite file?"},
                   frame{b1, b2; side="left"};
                   side="top"
                  }

表格 1: 後備條件。

字串 條件
"arith" 運算元無效。
"order" 運算元無效,無法比較順序。
"concat" 運算元無效,無法串接字串。
"getglobal" 讀取未定義的變數。
"index" 在表格中取得不存在的索引值。
"gettable" 在非表格值中讀取索引值。
"settable" 在非表格值中寫入索引值。
"function" 呼叫非函數值。
"gc" 在垃圾回收期間,針對每個要回收的表格呼叫。
"error" 發生致命錯誤時呼叫。

清單一

#include <stdio.h>
#include "lua.h"

int main()
{
 char line[BUFSIZ];
 while (fgets(line,sizeof(line),stdin)!=0)
   lua_dostring(line);
 return 0;
}

清單二

function map(list, func)
  local newlist = {}
  local i = 1
  while list[i] do
    newlist[i] = func(list[i])
    i = i+1
  end
  return newlist
end

清單三

list = {}
i = 4
while i >= 1 do
   list = {head=i,tail=list}
   i = i-1
end

清單四

S = Separator{
     drawStyle = DrawStyle{style = FILLED},
     material =  Material{
       ambientColor  = {0.377, 0.377, 0.377},
       diffuseColor  = {0.800, 0.771, 0.093},
       emissiveColor = {0.102, 0.102, 0.102},
       specularColor = {0.0, 0.0, 0.0}
     },

     transform = Transform{
       translation = {64.293, 20.206, 0.0},
       rotation    = {0.0, 0.0, 0.0, 0.0}
     },
     shape = Sphere{radius = 10.0}
   }

清單五

function Inherit(t,f)
  if f == "parent" then  -- avoid loops
    return nil
  end
  local p = t.parent
  if type(p) == "table" then
    return p[f]
  else
    return nil
  end
end

setfallback("index", Inherit)

清單六

TNumber="number"
TPoint={x=TNumber, y=TNumber}
TColor={red=TNumber, blue=TNumber, green=TNumber}
TRectangle={topleft=TPoint, botright=TPoint}
TWindow={title="string", bounds=TRectangle, color=TColor}

清單七

function checkType(d, t)
  if type(t) == "string" then
    -- t is the name of a type
    return (type(d) == t)
  else
    -- t is a table, so d must also be a table
    if type(d) ~= "table" then
      return nil
    else
      -- d is also a table; check its fields
      local i,v = next(t,nil)
      while i do
        if not checkType(d[i],v) then
          return nil
        end
        i,v = next(t,i)
      end
    end
  end
  return 1
end

清單八

function save()
  -- create table to hold environment

  local env = {}
  -- get first global var and its value
  local n, v = nextvar(nil)
  while n do
    -- save global variable in table
    env[n] = v
    -- get next global var and its value
    n, v = nextvar(n)
  end
  return env
end

清單九

function runProtected(code)
  -- save current environment
  local oldenv = save()
  -- erase "dangerous" functions
  readfrom,writeto,execute = nil,nil,nil
  -- run untrusted code
  dostring(code)
  -- restore original environment
  restore(oldenv)
end

清單十

widgetClass = {}
function widgetClass:focus()
 if self.tkname then
  tklua_setFocus(self.tkname)
 end
end
buttonClass = {
 parent = widgetClass,
 tkwidget = "button"
}

清單十一

function button(self)
 self.parent = classButton
 tklua_ID = tklua_ID + 1
 tklua_IDtable[tklua_ID] = self
 return tklua_ID
end

清單十二

function setFB(id, f, v)
 local h = tklua_IDtable[id]
 if h == nil then
  old_setFB(id,f,v)
  return
 end
 if h.tkname and h:isAttrib(f) then
  tklua_configure(h.tkname,f,v)
 end
 h[f] = v

end
old_setFB = setfallback("settable",setFB)