Lua SEMISH'94 論文

轉載自巴西軟體與硬體研討會 XXI 論文集 (1994) 273–283。 [ps]

擴充應用程式的語言設計與實作

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

摘要。

我們說明 Lua 的設計與實作,這是一種用於擴充應用程式的簡單而強大的語言。儘管 Lua 是一種程序語言,但它具有資料描述功能,且已廣泛用於生產中,包括使用者設定、一般用途資料輸入、使用者介面描述、應用程式物件描述,以及結構化圖形元檔案儲存等多項工作。

引言

對可自訂應用程式的需求日益增加。隨著應用程式變得越來越複雜,使用簡單參數進行自訂已不可行:使用者現在希望在執行時做出設定決策;使用者也希望撰寫巨集和指令碼以提高生產力 (Ryan 1990)。因此,現今較大型的應用程式幾乎都會附帶自己的設定或指令碼語言,供最終使用者程式設計。這些語言通常很簡單,但每種語言都有自己的特殊語法。因此,使用者必須為每個應用程式學習一種新語言 (而開發人員必須設計、實作和除錯這些語言)。

我們第一次使用專有指令碼語言的經驗,是在一個資料輸入應用程式中,為此設計了一種非常簡單的宣告式語言 (Figueiredo–Souza–Gattass–Coelho 1992)。(資料輸入是一個特別需要使用者定義動作的領域,因為預先編寫的驗證測試很難適用於所有應用程式。)當使用者開始要求此語言具有越來越強大的功能時,我們決定需要一種更通用的方法,並開始設計一種通用嵌入式語言。同時,另一種宣告式語言正被加入到一個不同的應用程式中,用於資料描述。因此,我們決定將這兩種語言合併為一種,並設計 Lua 成為一種具有資料描述功能的程序語言。從那時起,Lua 已超越其最初的根源,並用於其他幾個產業專案中。

本文說明 Lua 的設計決策和實作細節。

擴充語言

現在已將使用語言來擴充應用程式視為一項重要的設計技術:它能讓應用程式設計更簡潔,並提供使用者設定。由於大多數擴充語言都很簡單,且專門用於某項工作,因此它們被稱為「小語言」(Bentley 1986;Vald�s 1991),與用來撰寫應用程式的「大」主流語言形成對比。如今,這個區別已不再那麼明顯,因為許多應用程式的主要部分實際上都是使用擴充語言撰寫的。擴充語言有許多種

嵌入式語言與獨立語言不同之處在於,嵌入式語言只能嵌入在稱為嵌入式程式的主機用戶端中。此外,主機程式通常可以提供嵌入式語言的特定領域擴充,從而建立一個自訂化嵌入式語言版本以符合自己的目的,可能透過提供更高級別的抽象化來達成。為此,嵌入式語言同時具備自己的程式語法,以及用於與主機介面的應用程式介面 (API)。因此,與用於提供參數值和動作序列給主機的較簡單擴充語言不同,嵌入式語言和主機程式之間存在雙向通訊。請注意,應用程式設計人員使用主機程式所用的主流語言與嵌入式語言介面,而使用者則僅使用嵌入式語言與應用程式介面。

LISP 一直以來都是擴充語言的熱門選擇,因為它具有簡單、容易解析的語法和內建的擴充性(Beckman 1991;Nahaboo)。例如,Emacs 的主要部分實際上是用它自己的 LISP 變體編寫的;其他幾個文字編輯器也遵循相同的路徑。然而,在自訂方面,LISP 無法稱得上是使用者友善。C 和 shell 語言也不行;後者甚至有更複雜、不熟悉的語法。

在 Lua 設計中做出的基本決策之一是,它應該有一個乾淨但熟悉的語法:我們很快地決定採用簡化的類似 Pascal 的語法。我們避免使用基於 LISP 或 C 的語法,因為它可能會讓外人或非程式設計師感到沮喪。因此,Lua 主要是一種程序語言。不過,如前所述,Lua 獲得了資料描述功能,以增加其表達能力。

Lua 概念

Lua 是一種通用嵌入式程式設計語言,旨在支援具有資料描述功能的程序程式設計。作為一種嵌入式語言,Lua 沒有「主程式」的概念;它只能嵌入在主機客戶端中(Lua 以 C 函式庫的形式提供,連結到主機應用程式)。主機可以呼叫函式來執行 Lua 中的一段程式碼,可以寫入和讀取 Lua 變數,並且可以註冊 C 函式以供 Lua 程式碼呼叫。透過註冊的 C 函式,Lua 可以擴充以應付不同的領域,從而建立共用語法架構的自訂程式設計語言(Beckman 1991)。

本節簡要說明 Lua 中的主要概念。包含了一些實際程式碼範例,以提供該語言的概況。可以在其參考手冊中找到語言的精確定義(Ierusalimschy–Figueiredo–Celes 1994)。

語法

如前所述,我們明確設計 Lua 具有簡單、熟悉的語法。因此,Lua 支援幾乎傳統的陳述集,具有隱含但明確終止的區塊結構。傳統陳述包括簡單賦值;控制結構,例如 while-do-endrepeat-untilif-then-elseif-else-end;和函式呼叫。非傳統陳述包括多重賦值;區域變數宣告,可以放置在區塊內的任何地方;以及表建構函式,其中可能包含使用者定義的驗證函式(見下文)。此外,Lua 中的函式可以接受可變數量的參數,並且可以傳回多個值。這避免了在需要傳回多個結果時透過參照傳遞參數的需要。

環境和模組

Lua 中的所有陳述都在全域環境中執行。此環境保留所有全域變數和函式,在嵌入式程式開始時初始化,並持續到結束。全域環境可以由 Lua 程式碼或嵌入式程式操作,後者可以使用實作 Lua 的函式庫中的函式來讀取和寫入全域變數。

Lua 的執行單位稱為模組。模組可能包含陳述式和函式定義,且可能在檔案中或在主程式內的字串中。執行模組時,首先會編譯其所有函式和陳述式,並將函式新增至全域環境;然後依序執行陳述式。模組對全域環境所做的所有修改都會在其結束後持續存在。這些修改包括對全域變數的修改和新函式的定義(函式定義實際上是對全域變數的指定;見下文)。

資料類型和變數

Lua 是一種動態類型語言:變數沒有類型;只有值有。所有值都攜帶自己的類型。因此,語言中沒有類型定義。變數類型宣告的遺漏,表面上是一個小問題,但實際上是簡化語言的重要因素;它經常被視為許多已修改為延伸語言的類型語言變體中的主要功能。此外,Lua 有垃圾回收機制:它會追蹤哪些值正在使用,並捨棄未使用的值。這避免了明確管理記憶體配置的需要,而記憶體配置是程式設計錯誤的主要來源。Lua 中有七種基本資料類型

Lua 提供一些自動類型轉換。參與算術運算的字串會轉換為數字(如果可能)。相反地,當數字在預期為字串時使用時,該數字會轉換為字串。這種強制轉換很有用,因為它簡化了程式並避免了對明確轉換函式的需要。

全域變數不需要宣告;只有區域變數需要。任何變數都被假設為全域變數,除非明確宣告為區域變數。區域變數宣告可以放置在區塊內的任何地方。因此,由於只有區域變數會被宣告,且這些宣告可以緊鄰變數使用,因此通常很容易決定給定的變數是區域變數還是全域變數。

在第一次賦值之前,變數的值為nil。因此,Lua 中沒有未初始化的變數,這是程式設計錯誤的另一個主要來源。然而,對nil 唯一有效的操作是賦值和相等性測試(nil 的主要特性是與任何其他值不同)。因此,在需要「實際」值的情況下使用「未初始化」變數(例如,算術表達式)會導致執行錯誤,提醒程式設計人員該變數未正確初始化。因此,使用nil 自動初始化變數的目的是不是鼓勵程式設計人員避免初始化變數,而是讓 Lua 能夠發出實際未初始化變數的使用訊號。

函式在 Lua 中被視為一級值:它們可以儲存在變數中,作為引數傳遞給其他函式,並作為結果傳回。當在 Lua 中定義函式時,其主體會編譯並儲存在具有給定名稱的全球變數中。Lua 可以呼叫(並處理)使用 Lua 和 C 編寫的函式;後者具有類型Cfunction

提供類型userdata 以允許將任意(void*)C 指標儲存在 Lua 變數中;它在 Lua 中唯一有效的操作是賦值和相等性測試。

類型table 實作關聯陣列,也就是說,可以使用數字和字串索引的陣列。因此,此類型不僅可用於表示一般陣列,還可用於表示符號表、集合、記錄等。為了表示記錄,Lua 使用欄位名稱作為索引。此語言透過提供 a.name 作為 a["name"] 的語法糖來支援此表示法。

關聯陣列是一種強大的語言建構;許多演算法簡化到微不足道的程度,因為搜尋它們所需的資料結構和演算法是由語言提供的(Aho–Kerninghan–Weinberger 1988;Bentley 1988)。例如,計算文字中每個字詞出現次數的程式核心可以寫成

        table[word] = table[word] + 1
而無需搜尋字詞清單。(但是,按字母順序排列的報告需要一些實際工作,因為 Lua 內部表格中的索引是任意排序的。)

可以透過許多方式建立表格。最簡單的方式對應到一般陣列

        t = @(100)
此類表達式會產生一個新的空表格。維度(以上範例中的 100)是選用的,可以作為初始表格大小的提示。獨立於初始維度,Lua 中的所有表格會根據需要動態擴充。因此,參照 t[200] 甚至是 t["day"] 完全有效。

有兩種替代語法可以建立表格,而不用明確填寫每個項目:一種是清單(@[]),一種是記錄(@{})。例如,透過提供元素來建立清單容易多了,如下所示

        t = @["red", "green", "blue", 3]
相較於等效的明確程式碼
        t = @()
        t[1] = "red"
        t[2] = "green"
        t[3] = "blue"
        t[4] = 3
此外,在建立清單和記錄時,可以提供使用者函式,如下所示
        t = @colors["red", "green", "blue", "yellow"]
        t = @employee{name="john smith", age=34}
在此,colorsemployee 是使用者函式,會在建立表格後自動呼叫。此類函式可用於檢查欄位值、建立預設欄位,或任何其他副作用。因此,employee 記錄的程式碼等同於
        t = @()
        t.name = "john smith"
        t.age  = 34
        employee(t)
請注意,即使 Lua 沒有型別宣告,在建立表格後自動呼叫使用者函式的可能性,實際上讓 Lua 擁有使用者控制的型別建構函式。此非傳統建構是一個非常強大的功能,而且是使用 Lua 的宣告式程式設計表達式。

應用程式介面

實作 Lua 的函式庫有一個 API,也就是一組 C 函式,用於讓 Lua 與主機程式介接(大約有 30 個此類函式)。這些函式將 Lua 定義為嵌入式語言,並處理下列工作:執行包含在檔案或字串中的 Lua 程式碼;在 C 和 Lua 之間轉換值;讀取和寫入包含在全域變數中的 Lua 物件;呼叫 Lua 函式;註冊 C 函式以供 Lua 呼叫,包括錯誤處理常式。一個簡單的 Lua 詮譯器可以寫成如下

        #include "lua.h"
        int main(void)
        {
         char s[1000];
         while (gets(s))
           lua_dostring(s);
         return 0;
        }
這個簡單的詮譯器可以用 C 編寫的特定領域函式擴充,並透過 API 函式 lua_register 提供給 Lua 使用。擴充函式遵循一個通訊協定,以接收和傳回值給 Lua。

預先定義的函式和函式庫

Lua 中預先定義的函式組很小但很強大。它們大多數提供一些允許語言有一定程度反射性的功能。此類功能無法用語言的其他部分或標準 API 模擬。預先定義的函式處理下列工作:執行包含在檔案或字串中的 Lua 模組;列舉表格的所有欄位;列舉所有全域變數;型別查詢和轉換。

另一方面,函式庫提供有用的常式,可透過標準 API 直接實作。因此,它們對語言而言並非必要,而是以獨立的 C 模組提供,可視需要連結到應用程式。目前,有字串處理、數學函數以及輸入與輸出的函式庫。

持久性

列舉函數可提供 Lua 內部全域環境的持久性,亦即,可以撰寫 Lua 程式碼來撰寫 Lua 程式碼,在執行時,還原所有全域變數的值。我們現在將展示一些使用 Lua 儲存和擷取值的方法,並使用以該語言本身撰寫的文字檔作為儲存媒體。若要還原以這種方式儲存的值,只要執行輸出檔即可。

若要儲存具有名稱的單一值,下列程式碼就夠了

        function store(name, value)
          write(name .. '=')
          write_value(value)
        end
在此,「..」是字串串接運算子,而 write 是用於輸出的函式庫函數。函數 write_value 會根據值類型,輸出適當的表示方式,使用預先定義的函數 type 傳回的字串
        function write_value(value)
          local t = type(value)
              if t = 'nil'    then write('nil')
          elseif t = 'number' then write(value)
          elseif t = 'string' then write('"' .. value .. '"')
          end
        end

儲存表格稍微複雜一些。首先,write_value 會擴充為

          elseif t = 'table' then write_record(value)
假設表格用作記錄(亦即,沒有循環參照,且所有索引都是識別碼),則可以使用表格建構函數直接撰寫表格的值
        function write_record(t)
          local i, v = next(t, nil)   -- "next" enumerates the fields of t
          write('@{')                 -- starts constructor
          while i do
            store(i,v)
            i, v = next(t, i)
            if i then write(', ') end
          end
          write('}')                  -- closes constructor
        end

實作

延伸語言總是會以某種方式由應用程式來詮釋。簡單的延伸語言可以直接從原始碼詮釋。另一方面,嵌入式語言通常是功能強大的程式語言,具有複雜的語法和語意。針對嵌入式語言,更有效率的實作技術是設計一個適合語言需求的虛擬機器,將延伸程式編譯成此機器使用的位元組碼,然後透過詮釋位元組碼來模擬虛擬機器(Betz 1988、1991;Franks 1991)。我們選擇這種混合架構來實作 Lua;它相較於直接詮釋原始碼,具有下列優點

此架構於 Smalltalk 中率先採用(Goldberg–Robson 1983;Budd 1987)(位元組碼一詞即源自於此),並於基於 P 碼的成功 UCSD Pascal 系統中使用(Clark–Koehler 1982)。在這些系統中,虛擬機器的位元組碼用於降低複雜度和提升可攜性。此路徑也用於移植 BCPL 編譯器(Richards–Whitby-Strevens 1980)。

可使用標準工具,例如 lexyacc(Levine–Mason–Brown 1992),建置延伸程式編譯碼。在 70 年代後期廣泛使用的良好編譯器建置工具,是多種小型語言萌芽的主要原因,特別是在 Unix 環境中。我們使用 yacc 進行 Lua 的語法分析。最初,我們使用 lex 編寫詞法分析器。在使用生產程式進行效能分析後,我們發現此模組幾乎佔用載入和執行延伸程式所需時間的一半。接著,我們直接使用 C 重寫此模組;新的詞法分析器速度比舊的快兩倍以上。

Lua 的虛擬機器

我們在 Lua 中使用的虛擬機器是堆疊機器。這表示它沒有隨機存取記憶體:所有暫存值和局部變數都保存在堆疊中。此外,它沒有通用暫存器,只有控制堆疊和程式執行的特殊控制暫存器。這些暫存器為堆疊基底堆疊頂端程式計數器

虛擬機器的程式是指令序列,稱為位元組碼。程式的執行是透過解釋位元組碼來達成,每個位元組碼對應到一個操作堆疊頂端的指令。例如,陳述式

        a = b + f(c)
編譯成
        PUSHGLOBAL   "b"
        PUSHGLOBAL   "f"
        PUSHMARK
        PUSHGLOBAL   "c"
        CALLFUNC
        ADJUST        2
        ADD
        STOREGLOBAL  "a"
Lua 的虛擬機器有約 60 個指令;因此,可以使用 8 位元組碼。許多指令(例如 ADD)不需要額外參數;這些指令直接在堆疊上操作,並在編譯碼中只佔用一個位元組。其他指令(例如 PUSHGLOBALSTOREGLOBAL)需要額外參數,並佔用多於一個位元組。由於參數佔用一個、兩個或四個位元組,因此會在某些架構中產生對齊問題,可透過在對齊邊界補上 NOP 來解決。

許多指令僅存在於最佳化目的。例如,有一個 PUSH 指令,它將數字作為參數並將其推入堆疊,但也有單一位元組的最佳化版本,用於推入常見值,例如零和一。因此,我們有 PUSHNILPUSH0PUSH2PUSH3。此類最佳化可減少編譯位元組碼所需的空間和解釋指令所需的時間。

回想一下,Lua 支援多重賦值和函式的多重回傳值。因此,有時必須在執行階段將值清單調整為給定的長度:如果值多於所需,則會捨棄多餘的值;如果需要的值多於目前有的,則會使用任意數量的nil 來延伸清單。調整會在堆疊中使用 ADJUST 指令來執行。

儘管多重賦值和回傳是 Lua 強大的功能,但它們卻是編譯器和直譯器中複雜性的重要來源。由於函式沒有型別宣告,編譯器不知道函式會回傳多少值。因此,調整必須在執行階段執行。同樣地,編譯器不知道函式需要多少個參數。由於這個數字可能會在執行階段變動,因此參數清單會用 PUSHMARKCALLFUNC 指令括起來。

延伸 Lua 以使用主機提供的函式的方法之一,就是為每個此類函式指定一個位元組碼(Betz 1988)。儘管這個策略會簡化直譯器,但它的缺點是能新增的外來函式少於 200 個,因為 Lua 有 8 位元組碼,而且已經有大約 60 個用於其原始指令。我們選擇讓主機明確註冊外來函式,並將這些函式視為原生 Lua 函式來處理。因此,只有一個 CALLFUNC 指令;直譯器會根據所呼叫函式的型別決定要執行什麼動作。

Franks (1991) 提出了一個相當不同的策略:主機中的所有外來函式都可以由嵌入式語言呼叫;不需要明確註冊。這會透過讀取和詮釋連結器產生的對應來完成。這個解決方案對應用程式程式設計師來說非常方便,但不可移植,因為它依賴對應檔的格式和作業系統使用的重新定位策略(Franks 使用了 DOS 的特定編譯器)。

內部資料結構

如前所述,Lua 中的變數沒有型別;只有值有型別。因此,值會在一個有兩個欄位的 struct 中實作:一個型別和一個包含實際值的 union。這些 struct 出現在堆疊和符號表中,符號表會保留所有全域符號。

數字會直接儲存在 union 中。字串會保存在單一陣列中;字串 型別的值會包含指向這個陣列的指標。函式 型別的值會包含指向位元組碼陣列的指標。Cfunction 型別的值會包含主機程式提供的實際 C 函式指標;userdata 型別的值也會這樣做。

表格會實作為雜湊表,並透過單獨鏈結來處理衝突(這說明了為什麼表格中的索引會任意排序)。如果在建立表格時給定了一個維度,則會使用這個維度作為雜湊表的尺寸。因此,透過提供一個大約等於表格中預期索引數量的維度,會發生的衝突會很少,進而讓索引位置非常有效率。此外,如果表格用作陣列,而且只有數值索引,則在建立時選擇正確的維度可以保證不會發生衝突。

Lua 中的所有內部資料結構都是動態配置的陣列。當這些陣列中沒有更多可用槽位時,會自動使用標準的標記和清除演算法來執行垃圾回收。如果沒有回收任何空間(因為所有值都有被參照),則會重新配置陣列,使其目前的尺寸加倍。

垃圾回收對程式設計師來說非常方便,因為它避免了明確的記憶體管理。當 Lua 被用作獨立語言(它經常是)時,垃圾回收是一種資產。然而,當 Lua 被用於嵌入在主機程式中(這是它的主要目的)時,垃圾回收會為需要與 Lua 介接的應用程式設計師帶來新的擔憂:應注意不要將 Lua 表格和字串儲存在 C 變數中,因為如果它們在 Lua 環境中沒有任何進一步的參考,這些值可能會在垃圾回收期間被回收。更確切地說,程式設計師必須在將控制權返回 Lua 之前,將這些值明確複製到 C 變數中。儘管這是一個不同的範例,但它並不比使用標準 C 函式庫的記憶體管理的熟悉 malloc-free 協定差。

結論

Lua 自 93 年中期以來已廣泛用於生產中,用於以下任務

此外,Lua 目前正被視為視覺化程式設計系統的基礎。

在執行階段載入和執行 Lua 程式的能力已被證明是讓設定成為使用者和開發人員的輕鬆任務的主要組成部分。此外,單一一般用途嵌入式語言的存在阻礙了不相容語言的倍增,並鼓勵更好的設計,這種設計清楚地將應用程式中包含的主要技術與其設定問題分開。

本文中描述的 Lua 實作可透過匿名 ftphttps://lua.dev.org.tw/ftp/lua-1.1.tar.gz 取得。

致謝

我們要感謝 ICAD 和 TeCGraf 的工作人員使用和測試 Lua。文中提到的工業應用是由 PETROBRAS(CENPES)和 ELETROBRAS(CEPEL)的研究中心合作開發的。

參考文獻

M. Abrash、D. Illowsky,「使用迷你解釋器建立自己的迷你語言」,Dr. Dobb's Journal 14 (9)(1989 年 9 月)52-72。

A. V. Aho、B. W. Kerninghan、P. J. Weinberger,AWK 程式設計語言,Addison-Wesley,1988 年。

B. Beckman,「互動式圖形中微語言的方案」,軟體、實務與經驗 21 (1991) 187-207。

J. Bentley,「程式設計珍珠:微語言」,ACM 通訊 29 (1986) 711-721。

J. Bentley,更多程式設計珍珠,Addison-Wesley,1988 年。

D. Betz,"嵌入式語言",Byte 13 #12 (1988 年 11 月) 409–416。

D. Betz,"您自己的微型物件導向語言",Dr. Dobb's Journal 16 (9) (1991 年 9 月) 26–33。

T. Budd,A Little Smalltalk,艾迪生-韋斯利,1987 年。

R. Clark、S. Koehler,UCSD Pascal 手冊:程式設計人員的參考及指南,普倫蒂斯-霍爾,1982 年。

M. Cowlishaw,REXX 程式設計語言,普倫蒂斯-霍爾,1990 年。

L. H. de Figueiredo、C. S. de Souza、M. Gattass、L. C. G. Coelho,"繪圖資料擷取介面產生",Anais do SIBGRAPI V (1992) 169–175 [葡萄牙語]。

N. Franks,"為您的軟體新增擴充語言",Dr. Dobb's Journal 16 (9) (1991 年 9 月) 34–43。

A. Goldberg、D. Robson,Smalltalk-80:語言及其實作,艾迪生-韋斯利,1983 年。

R. Ierusalimschy、L. H. de Figueiredo、W. Celes Filho,"Lua 程式設計語言參考手冊",Monografias em Ci�ncia da Computa��o 4/94,里約天主教大學資訊系,1994 年。

J. R. Levine、T. Mason、D. Brown,Lex & Yacc,歐萊禮與合夥人,1992 年。

C. Nahaboo,嵌入式語言目錄,可自 colas@indri.inria.fr 取得。

M. Richards、C. Whitby-Strevens,BCPL:語言及其編譯器,劍橋大學出版社,1980 年。

B. Ryan,"無限制的腳本",Byte 15 (8) (1990 年 8 月) 235–240。

R. Vald�s,"微型語言,重大問題",Dr. Dobb's Journal 16 (9) (1991 年 9 月) 16–25。