Lua的演進

lua的優點:

  • 可移植性
  • 容易嵌入
  • 體積小
  • 高效率

這些優點都來自於lua的設計目標:簡潔。從Scheme獲得了很多靈感,包括匿名函數,合理的語義域概念

3 lua前身

巴西被商貿限制,引入計算機軟件和硬件受限,巴西人有強烈的民族情緒去創造自己的軟件。三名作者都是同一個實驗室Tecgraf的,這個實驗室與很多工業實體有合作關係。成立的頭十年,重點是創造交互性的圖形軟件,幫助合作伙伴進行設計。巴西石油公司是其中一個重要夥伴。有大量的遺留數據需要處理。於是誕生了DEL,一個領域專用語言,主要用來描述數據流圖的數據的。後來人們對DEL需求越來越多,不止是一門簡單的數據描述語言可以解決的了。

lua爲解決實際問題而生,受到三位作者學過的語言影響。自己創造的是..(兩個句號)連接字符串。自豪的是,在13年的演進裏,lua的類型系統只修改了兩次。lua誕生的時候,基本類型包括nil,number,string,table,C function,Lua function,userdata。97年的時候,Lua3.0將C function和Lua function合併了。03年的時候,提出了boolean值類型,增加了thread協程類型。

1993年,第一版Lua由Waldemar在Roberto指導下完成。詞法分析用了Unix上經典的yacc和lex。解釋器將lua代碼翻譯成針對一個基於棧的虛擬機的指令。C API很容易擴展,因此最早只有5個函數(print,tonumber,type,next,nextvar)和3個庫(input/output, string, math)。

5 Lua的歷史

 

lua的發佈模式和其他社區不一樣。alpha版本已經相當穩定,beta版本幾乎可以作爲final版,除非是用來修復bug。這個發佈模式對於lua的穩定性有很大幫助,但不利於嘗試新特性。因此,從5.0版本開始,添加了新的work版本。work版本是lua當前開發版的snapshot,有助於lua社區邁向開源社區的哲學:早發佈、多發佈。

lua的標準庫被刻意保持在一個很小的範圍,因爲大部分需要的功能都會由宿主或第三方庫提供。4.0對C API重新設計了,C API有很大改動,之後就向着完美一點點前進了。結果是不再有任何內置函數,所有標準庫都是基於C API實現,沒有通過特別的後門去訪問Lua內部結構。

Lua的vm在4.0版本以前一直是基於棧的。在3.1版本,我們對多個指令添加了變量來提高性能。後來發現這個實現複雜度很高,性能提升不明顯,於是在Lua 3.2版本去掉了。從Lua 5.0開始,vm改爲基於寄存器。代碼生成器因此有更多機會去優化和減少常見Lua程序的指令數了。

5.1 Lua 1

Lua的成功使得Lua被大規模用在數據描述上,而預編譯Lua代碼爲VM字節碼的特性,迫使Lua編譯器必須非常快,即使是對於大型項目。通過將lex產生的scanner替換爲一個手寫的版本,我們幾乎讓Lua編譯器的速度翻了一倍。同時,我們修改了Lua VM的table構造函數,構造一個大型的表不再需要一條條指令傳參數進去了,可以一次調用完成。從那以後,優化重點就變成了預編譯時間。

1994年,我們發佈了帶有這些優化的lua版本:Lua1.1。這次發佈和第一篇描述Lua的設計和實現的文章是重合的。之前從未公開發布過的版本,就被稱爲Lua 1.0(Lua1.0的一個1993年7月的snapshot在2003年10月發佈,以此慶祝Lua十週年)

Lua 1.1最早以源碼方式在ftp提供下載。這遠在開源運動興盛蓬勃之前。Lua1.1有限制性的用戶協議,對於學術用途是免費的,但是商業用途則需要協商。那部分協議沒有湊效:儘管我們有一些初始聯繫人,從沒有商業使用是經過協商的。其它腳本語言(比如Tcl)的免費促使我們意識到,限制商用甚至會反過來影響學術用途的發展,因爲一些學術項目是打算最終走向市場的。因此,當發佈Lua2.1的時候,我們將Lua作爲無限制免費軟件發佈了。我們天真的以一種學院氣的口吻對之前的協議重新措辭,並覺得新協議寫的很直觀了。稍後,隨着開源許可證的散播,我們的協議文本變成了干擾着一些用戶的噪音:我們的協議沒有清晰的說明是否和GPL協議相容。在2002年5月,在郵件列表裏進行了冗長的討論後,我們決定將來發布的Lua版本(從Lua 5.0開始)都使用非常清晰直觀的MIT協議。2002年7月,自由軟件基金會(FSF)確認了我們之前的協議是GPL兼容的。但我們已經決定採納MIT協議了。對我們協議的疑問從此都消失了。

5.2 Lua 2

1990年的時候,面向對象邁向巔峯,對於Lua沒有面向對象的支持,我們受到了很大的壓力。我們不想將Lua變成面向對象,因爲我們不想“修復”一種編程範式(fix a programming paradigm)。特別是,我們不覺得Lua需要將對象和類作爲基礎語言概念,我們覺得可以透過table來實現(table可以保存方法和數據,因爲函數是第一類對象)。直到今天,Lua也沒有強加任何對象和類模型給用戶,我們初心不變。很多用戶建議和實現了面向對象模型;面向對象也是郵件列表裏經常討論的問題,我們覺得這是健康的。

另一方面,我們希望允許Lua可以面向對象編程。我們決定提供一套靈活的機制,讓用戶可以選擇對應用來說合適的模型,而不是修復模型。1995年2月發佈的Lua2.1,標誌着這些靈活語義的問世,極大的增加了Lua的表達能力,從此,靈活語義就變成了Lua的標誌。

靈活語義的一個目標是允許table作爲對象和類的基礎。爲了實現這個目標,我們需要實現table的繼承。另一個目標是將userdata變成應用數據的天然代理,可以作爲函數參數而不只是一個句柄。我們希望能夠索引userdata,就好像他們只是一個table,可供調用他們身上的方法。這讓Lua可以更爲自然的實現自己的主要設計目標:通過提供腳本訪問到應用服務和數據,從而擴展應用。我們決定實現一套fallback機制,讓Lua把未定義行爲交給程序員處理,而不是直接在語言本身實現這些特性。

在Lua2.1的時候,我們提供了fallback機制,支持以下行爲:table索引,算術操作符,字符串拼接,順序比較,函數調用。當這些操作應用到“錯誤”的類型上,對應的fallback就會被調用到,允許程序員決定Lua如何處理。table索引fallback允許userdata和其它值類型表現的跟表一樣。我們也定義了當Key不在table時的fallback,從而實現多種形式的繼承(通過委託)。爲了完善面向對象編程,我們添加了兩個語法糖:function a:foo(…)就好比function a.foo(self,…)一樣,以及a:foo(…)作爲a.foo(a, …)的語法糖。在6.8節我們會討論fallback的細節。

從Lua1.0開始,我們就提供了值類型的內省函數(introspective functions):type,可以用來獲取Lua值的類型;next,可以用來遍歷一個table;以及nextvar,可以遍歷全局環境。(正如第四章所述,這是爲了實現類似SOL的類型檢查)爲了應付用戶對完整調試設施的強烈需求,1995年12月發佈的Lua2.2引入了一個debug API來獲取運行中的函數信息。這個API爲用戶提供了以C語言編寫自己的內省函數工具鏈的手段,比如編寫自己的調試器和性能分析工具。debug API剛開始的時候相當簡潔:debug庫允許訪問Lua的調用棧,訪問當前執行的代碼行行數,以及一個可以查找指定值的變量名的函數。根據M.Sc.的Tomas Gorham的工作,debug API在1996年5月發佈的Lua2.4版本里得到了完善,提供了函數訪問局部變量,提供了鉤子在行數變化和函數調用時觸發。

因爲Lua在Tecgraf的廣泛使用,很多大型的圖形源文件都是用Lua寫的,作爲圖形編輯器的輸出格式。加載這些源文件會隨着文件大小變得越來越大和越來越複雜而變的越來越長。從第一版開始,Lua就預編譯所有程序到字節碼,再執行字節碼。大型代碼文件的加載時間可以通過保存bytecode來縮減。這和處理圖形源文件特別有關係。所以在Lua 2.4版本,我們引入了一個外部編譯器Luac,可以編譯一個Lua文件爲字節碼並保存爲二進制文件。這個二進制文件的格式經過精心選擇,可以輕鬆加載同時體積小巧。通過luac,程序員可以在運行期避免詞法分析和代碼生成,這些操作早期還是比較耗時的。除了更快的加載,luac還允許離線的語法檢查,以及隨意的用戶改動。很多產品(比如模擬人生和Adobe的Lightroom)都是透過預編譯格式發佈Lua腳本的。

在luac的實現過程裏,我們開始將Lua的核心重構成清晰的分離模塊。於是,我們現在可以輕易移除詞法解析的模塊(詞法解析器,語法解析器和代碼生成器),這些部分佔了大約35%的核心代碼,剩下的部分可以加載預編譯的Lua腳本。這種代碼剪裁,對於在移動設備、機器人和感應器這些小設備裏嵌入Lua,是有顯著意義的。

從第一版開始,Lua就自帶有一個庫來進行字符串處理。這個庫在Lua2.4之前功能有限。但是,隨着Lua的成熟,Lua需要進行更重量級的字符串處理。我們認爲,沿革Snobol,Icon,Awk和Perl的傳統,爲Lua添加模式匹配是自然而然的。但是,我們不想將第三方的模式匹配引擎打包到Lua裏面去,因爲這些引擎通常都很大,我們也希望避開因爲引入第三方庫帶來的代碼版權問題。

1995年的第二學期,作爲Roberto指導下的學生項目,Milton Jonathan,Pedro Miller Rabinovitch,Pedro Willemsens和Vinicius Almendra爲Lua寫出了一個模式匹配庫。那個設計的經驗引導我們寫出了我們自己的模式匹配引擎。1996年的12月,我們在Lua2.5中添加了兩個函數:strfind(最早職能查找純文本)和新的gsub函數(名字來源於Awk)。gsub函數可以用來全局替換符合指定模式的子串。它接受一個新的字符串或者一個函數作爲參數,函數參數會在每次遇到匹配時調用,並預期該函數返回新子串以供替換。爲了縮小實現的規模,我們沒有支持完整的正則表達式。我們支持的模式包括字符類,重複,以及捕獲(但是沒有可選或組匹配)除了簡潔性,這種模式匹配還十分強大,是Lua的一個強有力的補充。

那一年是Lua歷史上的轉折點,因爲Lua獲得了全球的曝光。在1996年6月,我們在《Software:Practice & Experience》雜誌上發佈了一篇Lua的文章,爲Lua帶來了外部的關注,至少是學術圈子的關注。在1996年的12月,Lua 2.5剛剛發佈後,雜誌Dr.Dobb’s Journal也發表了Lua的文章。Dr.Dobb’s Journal是一本面向程序員的流行刊物,那篇文章爲Lua帶去了軟件工業界的關注。在那次發佈之後,我們收到了很多消息,其中一條是1997年1月收到的,來自Bret Mogilefsky,LucasArts出品的冒險遊戲——Grim Fandango的首席程序員。Bret告訴我們,他從Dr.Dobb’s上讀到了Lua,他們打算用Lua代替自己寫的腳本語言。1998年10月,Grim Fandango發佈,1999年5月Bret告訴我們“大量的遊戲都是用Lua寫的”。那個時候,Bret參加了GDC(Game Developers’ Conference, 遊戲開發者會議)的有關遊戲腳本的圓桌會議。他談到了Grim Fandango應用Lua的成功經驗。很多我們認識的開發者,都是在那次事件裏認識到Lua的。在那以後,Lua在遊戲程序員之間口耳相傳,變成了遊戲工業裏可資銷售的技能。

因爲Lua的國際化曝光,向我們提問Lua的信息越來越多。1997年2月,我們建立了郵件列表,好更有效率的處理這些問題,讓其他人也能幫忙回答問題,並開始建設Lua社區。這個列表至今發佈了超過38000條消息。很多熱門遊戲對Lua的使用,吸引了很多人到郵件列表裏。現在已經有超過1200個訂閱者了。Lua列表十分友善,同時又富有技術性,我們對此深感幸運。郵件列表逐漸變成了Lua社區的焦點所在,也是Lua演進的動力源泉。所有的重大事件,都是首先在郵件列表裏出現的:發佈重大通知,請求新特性,bug報告等等。

在Usenet新聞組裏建立comp.lang.lua討論組曾經在郵件列表裏討論過兩次,分別是1998年的4月和1999年的7月。兩次的結論都是郵件列表的流量不能保證創建一個新聞組。而且,更多人傾向於郵件列表。隨着多個閱讀和搜索完整郵件存檔的web界面問世,創建新聞組變得無關緊要了。

5.3 Lua 5.3

Lua2.1裏引入的fallback機制,可以很好的支持靈活擴展的語義,但這個機制是全局的:每個事件只有一個鉤子。這讓共享或重用代碼變的很艱難,因爲同一事件的fallback在模塊裏只能定義一次,不能共存。1996年12月,聽取了Stephan Herrmann的建議後,我們在1997年7月發佈的Lua 3.0中,我們解決了fallback衝突問題。我們將fallback替換爲tag方法:鉤子是以(event, tag)的形式掛在字典裏的。Tags是在Lua2.1引入的整數標籤,可以附在userdata上。最初的動機是希望同類的C對象,在Lua裏都有相同的tag(不過,Lua沒有強迫要對Tag提供解釋)。Lua3.0裏,我們對所有值類型提供了tag支持,以支持對應的tag方法。6.8節裏還會繼續討論fallback的演進。

1998年7月Lua 3.1引入了函數式編程,Lua擁有了匿名函數和upvalue支持的函數閉包。引入閉包是被高階函數的存在所啓發的,例如gsub,可以使用函數作爲參數。在Lua 3.1點工作中,郵件列表裏討論了多線程和協作式多任務,這些討論主要源於Bret Mogilefsky在Grim Fandango中,對Lua2.5和Lua 3.1 alpha版所作的改動。雖然沒有最終定論,但這個話題一直很熱門。協作式多任務在2003年4月發佈的Lua5.0裏提供了,詳見6.7節。

從Lua 1.0到Lua 3.2,C API總體上沒有變化,都是針對一個隱式Lua虛擬機進行操作的。但是,新的應用程序比如web,需要多狀態支持。爲了解決這一問題,Lua 3.1引入了多個獨立Lua狀態的設計,可以在運行時自由切換。而完全可重入的API則需要等到Lua4.0。同時,兩個非官方的Lua3.2修改版出現了,它們都帶有顯式的Lua狀態,一個是1998年,Roberto Ierusalimschy和Anna Hester爲CGILua寫的、基於Lua 3.2 alpha的版本。另一個是1999年,Erik Hougaard基於Lua 3.2 final寫的版本。Erik的版本是公開發布的,並應用在Crazy Ivan robot裏面。CGILua是作爲CGILua發行版的一部分發布,從未以獨立包的形式出現。

1999年7月,Lua 3.2主要是一個維護性的版本。沒有新奇的特性,除了一個可以用Lua編寫調試器相關代碼的debug庫。但是,從那時起,Lua就處在一個相對穩定的版本,因此Lua 3.2有很長的生命期。因爲下一個版本Lua 4.0是用的全新的不兼容API,很多用戶就只停留在Lua 3.2,沒有再遷移到4.0版本了。比如Tecgraf就從來沒有遷移到4.0版本,打算直接上Lua 5.0.很多Tecgraf的產品還是用的Lua 3.2

5.4 Lua 4

2000年12月,Lua 4.0正式發佈。正如上文所述,4.0的主要改變是完全可重入的API,爲那些需要多份Lua state的應用而設計。因爲改造API爲完全可重入已經是主要改動,我們藉此機會重新設計了API,依賴清晰的堆棧實現與C層的值交換。這是Reuben Thomas在2000年7月提出的。

Lua 4同時引入了for語句,這是郵件列表中的日常話題和大部分Lua用戶最想要的特性。我們早期沒有引入for語句,是因爲while循環更爲一般化。但是,用戶總是投訴忘記在while循環的尾部更新控制變量,從而引起死循環。而且,我們在好語法這一點上沒有達成一致。我們覺得Modula語言的for語句限制性太大了,因爲它既不能迭代table裏的元素,也不能迭代文件裏的行。C傳統上的for循環也不適合Lua。基於Lua 3.1引入的閉包和匿名函數,我們決定使用高階函數來實現迭代。所以,Lua 3.1提供了一個高階函數來迭代table,可以對table的每一對鍵值對回調用戶自定義函數。比如,要打印table t裏面的每一個鍵值對,只需要寫foreach(t,print)

在Lua 4.0我們終於設計了一個for循環,它有兩種方式:一個數字式的循環以及一個表遍歷的循環(1997年10月由Michael Spalinski提出)。這兩種方式覆蓋了大部分常用應用環境;對於更一般化的循環,依然有while循環可供使用。要打印table t裏的所有鍵值對,可以這樣寫:

for k, v in t do
  print(k, v)
end

添加for循環只是一個很簡單的一個改動,但它的確改變了Lua程序的外觀。Roberto不得不重寫了Programming in Lua草稿裏的許多例子。Roberto在1998年開始寫書了,但是他從來都沒寫完,因爲Lua是一個不斷變動的目標。隨着Lua 4.0的發佈,書裏大部分內容需要改寫,幾乎所有的代碼塊都要重寫了。

Lua 4.0發佈後,我們開始爲Lua 4.1工作。在Lua 4.1版本,我們面臨的主要挑戰,大概是要不要支持、如何支持多線程吧,這在當時是一個大問題。隨着Java的用戶增長以及Pthreads的出現,很多程序員開始考慮多線程,將其作爲編程語言的關鍵特性進行考量。但是,對於我們來說,Lua支持多線程需要考慮幾個嚴肅的問題。首先,在C層面支持多線程就會用到非ANSI C的原語——儘管Pthread很流行,但仍然有很多平臺(當時和現在)都缺乏這個庫的支持。第二,更重要的是,我們不相信標準多線程模型:那個共享內存的搶佔式併發模型。我們仍然認爲,對於a=a+1都沒有確定結果的語言,沒人能寫出正確的程序。

對於Lua 4.1,我們開始用Lua的經典方式解決這些難題:我們只實現了一個簡單的多棧共存的機制,我們稱爲threads(線程)。外部的庫可以使用這些Lua線程來實現多線程,比如基於Pthreads來實現。同樣的機制也能用來實現協程,一個協作式的、非搶佔的多線程模型。2001年7月,Lua 4.1 alpha發佈,帶有額外的多線程和協程庫支持;同時也引入了弱表,以及標誌性的基於寄存器的虛擬機。我們一直很想用基於寄存器的虛擬機進行實驗。

Lua 4.1 alpha發佈後的第二天,John D.Ramsdell在郵件列表裏開始了關於語法域的大討論。經過數十封郵件的討論,我們清晰的發現,Lua需要完全詞法作用域的支持,而不是Lua 3.1開始的upvalue機制。在2001年10月,我們想到了實現高效的完全詞法作用域的支持,並在同年的12月發佈了一個可以工作的版本。那個版本還引入了新的table混合模型,可以在合適的時候將table作爲數組實現。因爲那個版本實現了新的基礎算法,我們決定作爲可工作版本來發布,儘管我們已經爲Lua 4.1發佈了一個alpha版本。

2002年2月,我們爲Lua 4.1發佈了一個新的工作版本,帶有三個相對新穎的特性:一個基於迭代函數的通用的for loop,取代tags和fallbacks的元表和元方法,以及協程。在那次發佈後,我們意識到Lua 4.1帶來了太多的鉅變——可能Lua 5.0 比較適合作爲下個版本的版本號。

5.5 Lua 5

對於Lua 4.1這個版本號來說,最後的改動來自於Christian Lindig和Norman Ramsey在哈佛大學召開的Lua庫設計會議。這次會議的一個主要結論是,Lua需要某種模塊系統。我們一直認爲模塊可以透過table來完成,但是連標準庫都沒有按這種方式實現。於是我們決定下個版本邁出這一步。

將庫函數放入table裏面是一個巨大的衝擊,因爲這會影響到所有使用了至少一個庫函數的程序。比如,老版本的strfind函數現在叫string.find(在string庫裏的find域,存儲在叫string的table裏);openfile變成了io.open;sin變成了math.sin,諸如此類。爲了讓轉換變的簡單點,我們提供了一個兼容腳本,在裏面定義了老函數去應用新函數:

strfind = string.find
openfile = io.open
sin = math.sin
…

但是,將庫打包到table裏面去始終是一個大改動。2002年6月,當我們放出了帶有這一改動的可工作版本時,我們放棄了"Lua 4.1"這個名字,並將其命名爲"Lua 5.0 work0"。最終版本的進展從那時開始就相當穩定了,2003年4月,我們發佈了Lua 5.0。這次發佈讓Lua的特性得以穩定下來,讓Roberto可以完成他的新書了,新書在2003年12月發佈。

Lua 5.0發佈後不久,我們立即開始Lua 5.1的工作。最初的動機是實現增量式的垃圾回收系統,以滿足遊戲程序員的需要。Lua使用的是傳統的標記-清除垃圾回收算法,直到Lua 5.0爲止,垃圾回收都是原子執行的。副作用是對於某些應用,可能會在垃圾回收時有很長的暫停。在那個時間點,我們的主要考慮是,添加寫屏障需要實現增量式垃圾回收,對於Lua的性能會有負面影響。爲了補償這一劣勢,我們也常侍了分代回收。我們也希望保留老一套的自適應行爲,可以根據總內存佔用調節垃圾收集的頻率。而且我們希望回收器保持簡潔的特點,就像Lua的其他部分一樣。

我們在增量式分代GC上花了超過一年的時間。但因我們沒有接觸過對內存有強烈需求的應用(比如遊戲),所以很難在真實場景中測試收集器的表現。從2004年的3月到12月,我們放出了數個可工作版本,試圖得到收集器在應用中表現的具體反饋。我們最終收到了奇怪的內存分配行爲的報告,併成功重現了但沒有解釋。2005年1月,一名Lua社區的活躍成員Mike Pall,通過內存分配圖解釋了問題的根源:在某些特定場景,增量行爲、分代行爲和自適應行爲之間會有微妙的交互,導致收集器“自適應”到越來越低頻率的收集上去。因爲太複雜和不可預測,我們放棄了分代方式,並在Lua 5.1實現了更簡單的增量回收。

在這段時間裏,程序員嘗試了Lua 5.0的模塊系統。新包開始湧現,老包也開始轉移到新系統去。包作者們希望指導構建模塊的最佳實踐。2005年7月,在Lua 5.1的開發期間,一場由Mark Hamburg組織的國際性Lua會議,在San Jose的Adobe召開。其中一場演講是關於Lua 5.1的新特性,引發了關於模塊和包的冗長討論。結果是我們對模塊系統作出了一些細微但收效甚大的改變。儘管我們對Lua有“機制而非策略”的指南,我們還是定義了一系列的策略來編寫模塊和加載包,並做了些小改動讓這些策略跑的更好。2006年2月,Lua 5.1發佈。儘管Lua 5.1的初衷是增量垃圾收集,模塊系統的改進可能是最容易留意到的。另一方面,增量垃圾收集沒有被注意到,說明了它成功的避免了長時間的停頓。

6 特性演進

在這一節,我們會詳細討論Lua的一些特性的演進。

6.1 類型

Lua的類型系統是相對穩定的。很長時間裏,Lua只有6個基本類型:nil,number,string,table,function和userdata(實際上,直到Lua 3.0爲止,之前的C函數和Lua函數有不同的內部類型,但是這個差異對於調用者是透明的)。唯一真正的改變來自於Lua 5.0,這個版本引入了兩個新類型:thread和boolean。

引入的類型thread用於表示協程。像其他Lua值類型一樣,thread是第一類的值。爲了避免創造新的語法,所有針對thread的基礎操作都是通過library來提供的。

很長時間裏,我們都很抗拒引入布爾值類型:nil就是false,其他所有一切都是true。這種情況對於我們的目的是簡潔夠用的。但是,nil也作爲table裏元素缺失的值,以及未定義變量的默認值。在某些應用裏,需要一個table的域被置爲false但仍然可見表示存在,一個顯式的false可以表示這種情況。Lua 5.0裏,我們最終引入了布爾值類型true和false。nil還是作爲false。回顧這個設計,可能在布爾表達式裏引用nil報錯會是更好的解決辦法,就跟其他表達式裏一樣。這樣,nil扮演未定義值類型的代理,就會更加具有一致性。但是,這個改動很可能會導致很多現存的程序報錯。LISP有類似的問題,空列表代表了nil和false。Scheme顯式使用false,並將空列表作爲true。但一些Scheme實現仍然將空列表作爲false。

6.2 Table

Lua 1.1有三種語法來創建一個table:@()@[]@{}。最簡單的形式是@(),用於創建一個空表。可以在創建時提供一個可選的長度參數,作爲優化性能的提示信息。@[]這種形式是用來構建數組的,比如@[2,4,9,16,25]。在這種table裏,key是隱式的從1開始的自然數。@{}這種形式是用來構造記錄的,比如@{name="John",age=35}。這種table是鍵值對的集合,鍵是顯式的字符串。一個table可以用這三種的其中一種方式構造,構造後可以動態修改,不因構造方式受限。此外,還能在構造列表和記錄的時候提供用戶函數調用,比如@foo[]或者@foo{}。這個語法是從SOL裏繼承的,是過程數據表述的表達式,Lua的一個主要特性(參見第二節)。語法是構建好table後,調用對應的函數,將構造好的table作爲單參數傳入函數。函數允許隨意檢查和修改table,但是函數的返回值會被忽略:table就是表達式的最終值。

Lua 2.1裏,table創建的語法是統一的,並得到簡化:開頭的@被移除了,唯一的構造方式是{…}。Lua 2.1也允許混合的構造方式,比如

grades{8.5, 6.0, 9.2; name="John", major="math"}

數組的部分和記錄的部分被分號分割開。最後,foo{...}變成了foo({...})的語法糖。也就是說,函數加上表構造器,變成了普通的函數調用。所以,函數需要顯式返回table(或其他值)。從構造器裏拿掉@是一個平凡的改動,但它的確改變了語言的感覺,不僅僅是它的外觀。改變語言觀感的平凡改動不應該被忽視。

然而,這個語法的簡化和table構造器的語義改變帶來了副作用。Lua 1.1裏,=用來判斷相等。Lua 2.1裏table構造器統一之後,表達式{a=3}變成有歧義了,因爲它可以表示```("a", 3)````這個鍵值對,或者(1, b),其中b是表達式a=3的值。爲了解決二義性,Lua 2.1用==取代了`=`。改動後,`{a=3}`表示一個含有鍵值對`("a", 3)`的table,而`{a==3}`表示`(1, b)`。

這些改變讓Lua 2.1變得不兼容Lua 1.1了(所以大版本號也變了)。但是,因爲那時候實際上所有的Lua用戶都是Tecgraf的,這並不是一個致命的改變:現存的代碼可以經過我們編寫的特定工具進行轉換。table構造器的語法從那時起幾乎不變了,唯一的例外是Lua 3.1引入的:記錄的key可以用上任意表達式了,比如{[10*x+f(y)]=47}。尤其是這一改動允許了key爲任意的字符串了,包括保留字和帶有空格的字符串。因此,{function=1}是無效的,因爲function是一個保留字,但是{["function"]=1}是有效的。從Lua 5.0開始,也可以自由的混合數組部分和記錄部分了,因此沒有必要再在table構造器裏使用分號了。

雖然table的語法演進了,但是table的語義得到完整保留:table依然是關聯數組,可以存放任意鍵值對。但是,實踐中經常使用的table要麼是單純的數組(連續的整型key)或者記錄(字符串作爲key)。因爲table是Lua裏唯一的數據結構機制,我們投入了大量的努力去實現table,讓table在Lua的核心代碼裏高效運行。直到Lua 4.0爲止,table都是作爲純哈希表實現的,所有的鍵值對都是顯式存儲的。在Lua 5.0版本我們引入了table的混合表示:每個table包含了一個哈希部分和一個數組部分,兩個部分都可以是空的。Lua檢測一個table是不是作爲一個數組來使用,並自動將數字索引的值移動到數組部分,而非原本的存儲在哈希部分。這種分裂只在底層實現層次進行;訪問table域是透明的,即使是對虛擬機來說。table會自動根據內容使用兩個部分。

這個混合機制有兩個優點。第一,訪問整型key的操作會變得更快了,因爲不再需要哈希。第二,更重要的是,數組部分只佔原來哈希部分的一半大小,因爲哈希部分需要同時存儲key和value,而數組部分的key已經隱含在下標了。結果是,如果一個table是作爲數組使用的,它的表現就像數組一樣,只要它的整型key是密集分佈的。而且,哈希部分沒有內存或者時間的代價,因爲作爲數組使用時,哈希部分不存在。反過來說,如果table是作爲記錄使用而非數組,那麼數組部分就是空的。這些節省下來的內存是重要的,因爲對於Lua程序來說,創建大量小table是很常見的(比如用table來表示object)。Lua的table也能優雅的處理稀疏數組:語句a={[1000000000]=1}在哈希部分創建了一個鍵值對,而非一個10億元素的數組。

另一個打造高效table實現的原因,是我們可以使用table實現各種各樣的任務。比如,在Lua 5.0版本里的標準庫函數,從Lua 1.1開始就作爲全局變量存在,現在移動到table中去了。最近,Lua 5.1帶來了完整的包和模塊系統,這些系統都是基於table實現的。

table扮演了Lua核心的一個突出角色。有兩個場景我們直接用table代替了核心代碼的特殊數據結構:Lua 4.0版本,用table來表示全局環境(用於保存全局變量),Lua 5.0實現了可擴展語義(參見6.8)。從Lua 4.0開始,全局變量就存儲在普通的Lua table裏,稱爲全局table,這是John Belmonte在2000年4月提出的簡化建議。在Lua 5.0我們用元表和元方法取代了tag和tag方法(Lua 3.0引入的)。元表是普通的Lua table,元方法是作爲元表的域存儲的。Lua 5.0也引入了環境table,可以附加到Lua函數上;它們就是Lua函數索引的全局環境。Lua 5.1將環境變量table擴展到C函數、userdata和協程,取代了全局的環境變量。這些改動簡化了Lua的實現、Lua和C程序員所用的API,因爲全局變量和元方法可以在Lua裏操控,不再需要特殊函數了。

6.3 字符串

字符串在一門腳本語言裏扮演了主要的角色。因此,創建和處理字符串的設施是腳本語言易用性的重要部分。

文本字符串的語法有一個有趣的演進過程。從Lua 1.1開始,一個文本串可以用單引號或者雙引號來定義,也可以包含像C一樣的轉義序列。同時使用單引號和雙引號來定義字符串,並且具有相同語義,在當時有點不尋常。(比如,在腳本語言的傳統裏,Perl是會展開雙引號字符串裏的變量的,而單引號字符串則保持不變)這兩種引號表示方式允許字符串包含其中一種引號,而不需要使用轉義符。但是任意文本的字符串還是需要轉義符序列的。

Lua 2.2引入了長字符串,傳統編程語言所沒有,卻普遍存在於腳本語言的一項特性。長字符串可以有許多行,不需要解析其中的轉義符序列;它們提供了一個靈活的方式,將任意文本作爲字符串,不再需要擔心其中的內容(是否需要轉義符)。但是,要爲長字符串定義一個好的語法,沒有想象中的簡單,尤其是它們會被用於包含任意的程序文本(其中可能包含其他的長字符串)。這讓我們思考兩個問題:長字符串應該如何結束,它們是否可以嵌套。直到Lua 5.0爲止,長字符串都是被包含在一對匹配的[[...]]中,並且可以包含嵌套的長字符串。可惜,結束分隔符]]可以是有效Lua程序的一部分,以一種不平衡的方式出現:a[b[i]],或者在其他上下文環境裏,比如XML的<[!CDATA[...]]>

Lua 5.1引入了長字符串的新形式:用[===[…]===]包圍文本,=的數量可以是任意的(包括0)。這種新的長字符串不嵌套:一個長字符串遇到匹配數量的=就會結束。不管怎麼說,現在包裹任意文本變得更簡單了,即使文本包含其他長字符串或者不匹配的]=…=]序列:只要使用適當數量的=字符。

6.4 塊註釋

Lua的註釋是開始,到行尾結束。這是最簡單的註釋,非常有效。很多語言使用單行註釋,不過它們用的符號不一樣。使用表示註釋的語言包括Ada和Haskell。

我們從未覺得需要多行註釋,或者塊註釋,除非是作爲關閉一塊代碼的快捷方式。使用什麼語法永遠是一個問題:使用C語言的熟悉的/*...*/語法,或者其他語言的,都和Lua的單行註釋格式不搭配。同樣,塊註釋是否能嵌套也是一個問題,困擾着用戶,並影響到詞法分析器的複雜度。嵌套的塊註釋的場景,一般是程序員需要註釋掉某些代碼塊。自然,它們希望代碼塊裏的註釋可以正確處理,這隻有在塊註釋可以嵌套的情況下才能發生。

ANSI C支持塊註釋,但是不允許嵌套。C程序員通常使用C預處理器的慣用法來關閉代碼塊:#if 0 …#endif。這種方式有清晰的優勢,它可以優雅的處理好被關閉代碼裏的註釋。帶着同樣的動機和啓發,我們強調了關閉Lua代碼塊的需要——不是塊註釋的——通過在Lua 3.0引入類似C語言的pragma進行條件編譯。儘管塊註釋可以用上條件編譯,我們不覺得這是它的正確用法。在Lua 4.0的改造工作中,我們認爲條件編譯帶來的詞法器複雜度太大,不值得繼續支持,同時對於用戶來說,它的語義對於用戶有一定複雜性,對於完整宏支持的設施也沒有達成共識(參見第7章)。所以,Lua 4.0裏我們去掉了條件編譯的支持,Lua仍然沒有支持塊註釋。

塊註釋是在Lua 5.0才被最終引入的,形式是--[[...]]。因爲他們故意模仿了長字符串(參見6.3)的語法,所以很容易修改詞法器進行支持。這種相似性也幫助用戶掌握概念和語法。塊註釋也可以被用於關閉代碼:慣用法是在需要關閉代碼的前後加上兩行,分別包含—[[—]]。要打開代碼,只需要在第一行添加一個-:這兩行便都變成單行註釋。

像長字符串一樣,塊註釋也可以嵌套,同樣有長字符串所遇到的問題。特別是包含不平衡的]]有效的Lua代碼,比如a[b[i]],不能在Lua 5.0被可靠的註釋掉。Lua 5.1長字符串的新語法也能用於塊註釋,形式是—[===[…]===],這個問題因此得到簡潔穩定的解決方案。

6.5 函數

Lua裏的函數一直是第一類對象。一個函數可以在運行時創建,通過編譯和執行包含其定義的字符串實現。隨着Lua 3.1引入的匿名函數和upvalue,程序員可以在運行時創建函數,不再需要從文本里編譯了。

Lua的函數無論是C或者Lua的,都沒有聲明。被調用時,他們會接受不定數量的參數:多餘的參數會被丟掉,缺失的參數會被賦予nil值。(這吻合了多重賦值的語義)。C函數一直都能處理不同數量的參數。Lua 2.5介紹了可變參數類型Lua函數,標誌是參數列表以結尾(只在Lua 3.0出現的實驗特性)。當一個變參函數被調用,對應的參數將會收集到一個叫arg的table裏。這種方式雖然很簡單便捷,但是要把這些參數傳給另一個函數,就需要解包這個table。因爲程序員經常將參數傳遞給另一個函數,Lua 5.1允許...用於參數列表和賦值表達式的右值。這避免了沒必要的創建arg table。

Lua執行的基本單位是代碼塊(chunk);它只是一個語句序列。Lua的一個代碼塊就像其他語言的main程序:它可以包含函數定義和可執行代碼。(實際上,一個函數定義就是可執行代碼:一個賦值)同時,一個代碼塊和一個普通的Lua函數非常相似。比如,代碼塊和普通Lua函數一直有着同一類字節碼。但是,在Lua 5.0之前,代碼塊需要一些內部的“魔法”來開始執行。Lua 2.2開始,代碼塊開始類似普通函數,那時候,作爲未記錄在文檔的特性,函數外可以定義局部變量了(只在Lua 3.1變成了官方發佈)。Lua 2.5允許代碼塊返回值。Lua 3.0代碼塊變成了內部的函數,只是他們在被編譯後立即執行;他們不以函數形式暴露給用戶層。最終,在Lua 5.0邁出了這一步,將代碼塊的加載和執行變成了兩步,來爲宿主程序員提供更好的控制性和錯誤報告。結果,Lua 5.0裏代碼塊變成了日常無參數的匿名函數。Lua 5.1裏代碼塊變成了匿名變參函數,因爲可以在運行時進行值傳遞。這些值都可以透過新的機制進行傳遞。

從別的角度看,代碼塊就像其他語言的模塊一樣:他們一般用來提供全局環境的函數和變量。最初,我們沒想過Lua會用於大規模編程,所以我們不覺得需要顯式的模塊。而且,我們覺得table就足夠應付模塊的需要了。在Lua 5.0裏,我們將所有標準庫打包進table裏了,以此說明table足夠應付模塊。這鼓勵了其他人將庫打包進table裏,讓共享庫變得更爲簡單。我們現在覺得Lua可以用來大規模編程了,特別是在Lua 5.1帶來了基於table的包系統和模塊系統之後。

6.6 詞法範圍

在Lua開發的早期,我們開始考慮全詞法域的第一類函數。這是一個優雅的構造,完美演繹了Lua的少而精哲學,提供少量但強大的構造方式。同時也讓Lua適合進行函數式編程。但是,我們找不到全詞法的合理實現方式。一開始,Lua就使用了一個簡單的基於數組的棧來記錄活躍記錄(所有臨時變量和局部變量存儲的地方)。這個實現簡單高效,我們找不到理由推翻它。當我們允許嵌套函數和全詞法域之後,一個內部函數使用的變量,可能有着比創建它的函數更長的生命期,所以我們不能用棧的方式去處理這些變量。

簡單的Scheme實現在堆上分配棧幀。1987年,Dybvig[20]描述瞭如何使用棧去分配棧幀,前提是棧幀不包含嵌套函數使用的變量。他的方法需要編譯器提前知道,一個變量是不是嵌套函數的自由變量。這不適合Lua編譯器,因爲Lua編譯器解析完表達式後就立即生成操作變量的代碼,它無法得知任一變量是否會在稍後的運行中用作嵌套函數的自由變量。我們希望實現Lua時保持這個設計,因此沒有使用Dybvig的方法。基於相同的原因,我們也不能使用先進的編譯器技巧,比如數據流分析。

現在,有很多優化策略去避免使用堆來存儲棧幀(e.g.,[21]),但它們全都需要編譯器有中間表達方式,Lua編譯器卻沒有。McDermott的基於棧的棧幀分配建議[36],特別強調是針對解釋器的,是我們所知唯一的不需要中間表示方式的代碼生成。就像我們的實現一樣【31】,他的提案將變量放在棧上,如果它們被嵌套的閉包使用又超出了詞法域,就將它們移動到堆上。但是,他的提案假設環境是以關聯表的方式組織的。所以,將一個環境移動到堆上,解釋器只需要修正列表頭部,所有的局部變量自動到堆上了。Lua使用真實的記錄作爲活躍記錄,局部變量的訪問轉譯成直接訪問棧頂對應偏移地址,所以不能使用McDermott的方法。

在很長一段時間裏,這些困難一直困擾着我們,無法在Lua裏引入嵌套的第一類函數,並實現全詞法域支持。最終,在Lua 3.1裏,我們接受了一個稱爲upvalue的妥協實現。在這個方案裏,內部函數不能訪問和修改運行時的外部變量,但可以訪問函數創建時的變量。這些變量稱爲upvalue。upvalue的主要優勢,在於可以簡單的實現:所有局部變量在棧上;創建一個函數時,函數被打包在一個閉包之中,閉包包含了函數使用的外部變量的副本。換句話說,upvalue是外部變量的凍結值。爲了避免誤解,我們創建了新的語法來訪問upvalue:%varname。這個語法清晰表明代碼是在訪問變量的凍結值,而不是變量本身。upvalue雖然不能修改,但依然非常有用。有需要的時候,我們可以使用table作爲upvalue來模擬可變外部變量:儘管我們不能改變變量指向的table,我們仍可以改變table的域本身。這個特性在以下場景特別有用:匿名函數作爲高階函數的參數,用於table遍歷和模式匹配。

2000年12月,Roberto在他的書【27】的第一版手稿裏寫到"Lua通過upvalue提供了一種合適的詞法作用域形式"。在2001年7月,John D. Ramsdell在郵件列表中爭論到,"一門語言要麼是詞法作用域的,要麼不是;在詞法作用域前添加'合適的'這個形容詞毫無意義"。那條消息促使我們尋找更好的解決方案,以及支持全詞法域的方式。2001年10月,我們做出了了全詞法域支持的初步實現,併發布到郵件列表中。構想是每個upvalue都是間接訪問的,當變量在詞法域的時候指向棧;在詞法域結束的時候,會將變量值移到堆區,並將指針指向堆區。開放閉包(帶有指向棧的upvalue)被保存在一個列表裏,允許重定向和重用開放的upvalue。重用對於獲得正確語義有其必要性。如果兩個閉包,共享一個外部變量,各自有自身的upvalue,那麼在詞法域結束的時候,每個閉包都有自己的變量拷貝了,但正確的語義要求它們共享變量。爲了保證重用,創建閉包的算法是這樣做的:對於閉包使用的每個外部變量,首先搜索開放閉包的列表,如果發現一個upvalue指向同樣的外部變量,就重用那個upvalue;否則,創建新的upvalue。

Edgar Toering,Lua社區的活躍成員,誤解了我們對詞法域的描述。結果是他理解的比我們的最初的構想要更好:只保留開放upvalue的列表,而不是開放閉包的列表。因爲閉包使用的局部變量的數量,一般遠小於使用它們的閉包的數量(局部變量的數量被代碼文本靜態限制了),他的實現比我們的更加高效。而且也更容易適配到協程中(基本上同一時間實現),因爲我們可以爲每個棧保留一個獨立的upvalue列表。我們在Lua 5.0添加了全詞法域支持,用的正是這個算法,因爲它符合了我們的所有期望:它可以用一遍編譯器實現;沒有讓只訪問內部局部變量的函數增加運行代價,因爲它們依然在棧上處理所有局部變量;而且訪問外部局部變量的代價僅僅是一次額外的中轉【31】。

6.7 協程

我們一直在爲Lua尋找某種形式的第一類延續性。這種尋找是受Scheme的第一類延續的啓發誕生的(Scheme真是我們的靈感源泉),也是遊戲程序員對於"軟"多線程的需要(一般描述爲"以某種方式掛起一個角色,並且稍候繼續")。

2000年的時候,Maria Julia de Lima在Lua 4.0 alpha版實現了完整的第一類延續性,作爲她在讀PhD的部分工作成果【35】。她使用了一個簡單的實現,因爲就像詞法域問題一樣,以更高明的技巧實現協程,對於Lua總體上的簡潔而言,實在太複雜了。對於她的實驗來說,結果是令人滿意的,但是作爲最終產品,就顯得太慢了。無論如何,她的實現發現了Lua的一個奇特問題。因爲Lua是一門可擴展語言,有可能(而且很普遍)會從C調用Lua,或者從Lua調用C。因此,在一個Lua程序執行的任意點,當前延續性通常都是有部分是Lua,部分是C。儘管可以操作Lua的延續性(特別是通過操控Lua調用棧),卻不能操作C的延續性,尤其是在ANSI C標準下。那時候,我們對這個問題沒有很深的瞭解。特別是,我們找不到C調用的準確限制。Lima的實現只是簡單的禁止了所有C調用。再次強調,這對於她的實驗而言是可用的,但是對於Lua的官方發行版來說,完全無法接受。因爲Lua和C代碼可以輕易融合正是Lua的一個標記。

2001年12月,Thatcher Ulrich在沒有理解這一困難的前提下,在郵件列表裏宣佈: 我已經爲Lua 4.0創建了一個補丁,讓Lua到Lua的調用變成非遞歸的(比如"無堆棧"(stackless))。這帶來了sleep()調用的實現, 允許從宿主程序退出【省略】,並讓Lua state可以通過新的API函數lua_resume回到剛剛的執行環境。

換句話說,他提議了一個非對稱的協程機制,基於兩個原語:yield(他稱爲sleep)和resume。他的補丁遵循了郵件列表中,Bret Mogilefsky提到的,Grim Fandango公司針對Lua 2.5和3.1添加協作式多任務,這一實現的高階描述(Bret不能提供細節,因爲是公司私有的)。

在這次公告之後,2002年2月在哈佛舉行了Lua庫設計大會,有了一些關於Lua的第一類延續性的討論。有人認爲,如果第一類延續性太複雜,我們可以實現一次性延續性。其他人認爲實現對稱的協程更好。但我們找不到這些機制的合適實現,可以解決C調用的問題。

我們花了不少時間研究,才意識到爲什麼在Lua實現對稱協程那麼困難,爲什麼Ulrich的基於非對稱協程的實現,避開了我們遇到的難點。一次性延續性和對稱協程都涉及到操作全延續性。所以,只要這些延續性包含了任何C的部分,就不可能捕捉到他們(除非用ANSI C以外的工具)。相反,一個基於yield和resume的非對稱的協程機制,只操作偏序延續性:yield捕獲了當前調用點到對應resume之間的延續性【19】。在非對稱協程上,當前延續性可以包含C的部分,只要他們在被捕獲的延續性之外。換句話說,唯一的限制是我們不能跨C調用的yield。

意識到這一點之後,基於Ulrich的概念驗證實現,我們在Lua 5.0實現了非對稱協程。主要的改動是爲虛擬機執行指令的解釋器循環,不再能遞歸調用了。在之前的版本里,當解釋器循環執行一個CALL指令,他會遞歸調用自己來執行被調用的函數。Lua 5.0裏,解釋器更像是一個真實的CPU:當他執行一個CALL指令的時候,他將一些上下文信息推到棧上,然後處理被調用函數,被調用函數返回的時候,再恢復上下文。改成這樣以後,實現協程變得很直接了。

不像大多數非對稱協程的實現,Lua裏協程是stackfull【19】的。通過協程,我們可以實現對稱協程,甚至是Scheme的call/1cc操作符(調用當前一次性延續)。但是,使用C函數在這些實現裏還是受到嚴重限制。

我們希望Lua 5.0引入的協程成爲一個標誌,標誌着協程作爲強有力的控制結構的復興。

6.8 可擴展語義

在5.2節,我們介紹了可擴展語義的一個例子,Lua 2.1裏允許程序員使用fallback作爲通用機制,處理Lua未定義的場景。fallback因此提供了一種可恢復異常處理的受限形式。尤其是,通過使用fallback,我們可以讓一個值對不是爲其設計的操作進行響應,或者讓一個值表現得像另一個值一樣。例如,我們可以讓userdata和table響應算術運算符,userdata可以表現得像table一樣,字符串表現得像函數一樣,諸如此類。此外,我們可以讓一個table響應不存在的key,這是實現繼承的基石。通過table索引的fallback機制,以及一點點定義和調用方法的語法糖,面向對象編程及繼承在Lua裏成爲了現實。

儘管對象、類和繼承不是Lua的核心概念,他們可以直接在Lua裏實現,並根據應用程序的需要採取多種多樣的方式實現。換句話說,Lua提供了機制,而非策略——我們由始至終都緊密追隨的宗旨。

最簡單的繼承是通過委託繼承,Self率先引入這機制並被其他基於原型的語言所採用,比如NewtonScript和JavaScript。下面的代碼展示了Lua 2.1裏通過委託實現繼承的一個實現。

function Index(a, i)
  if i == "parent" then
    return nil
  end
  local p = a.parent
  if type(p) == "table" then
    return p[i]
  else
    return nil
  end
end
setfallback("index", Index)

當訪問一個table的缺失域(比如屬性或者方法),就會觸發索引的fallback。實現繼承就是將索引的fallback指向一條向上的祖系鏈,並有可能再次觸發fallback,直到遇到一個擁有指定域的table,或者這條鏈完結。

在設置了索引fallback以後,以下代碼會打印red,雖然b沒有color域:

a=Window{x=100,y=200,color="red"}
b=Window{x=300,y=400,parent=a}
print(b.color)

通過parent域進行委託,這過程中沒有黑魔法或者硬編碼。程序員擁有完整的自由:他們可以使用parent或者其他名字,可以通過嘗試一系列的父系函數實現多繼承,諸如此類。我們沒有硬編碼任一行爲的決定,引出了Lua的主要設計概念:元機制。我們提供了方法讓用戶可以編碼他們需要的特性,以他們想要的方式實現,並只實現他們需要的特性,而非將語言本身塞滿特性。

Fallback機制極大擴展了Lua的表達能力。但是,fallback是全局的句柄:對於每個事件,只有一個函數可以被執行。結果就是,很難在一個程序裏混合不同的繼承機制,因爲只有一個鉤子來實現繼承(只有索引fallback)。對於一個單一的開發組,基於自己的對象系統進行開發,這可能不是什麼嚴重的問題,但是一個組嘗試使用另一個組的代碼,就會變成問題,因爲他們的對象系統並不一定一致。不同機制的鉤子可以串在一起,但是鏈式調用很慢,很複雜,充滿錯誤,並且不禮貌。fallback鏈不鼓勵代碼共享和重用;實際上幾乎沒有人使用。這讓使用第三方庫變得很難。

Lua 2.1允許標記userdata。Lua 3.0我們將標記擴展到所有值,並用標記方法(tag methods)取代了fallback機制。標記方法是隻在帶有指定標記的值上執行的fallback。這讓實現獨立的繼承機制成爲可能。這種情況下不需要鏈式調用,因爲一個標記的方法不會影響另一個標記的方法。

標記方法機制工作的很好,一直存續到Lua 5.0爲止,我們在Lua 5.0實現了元表和元方法來取代標記和標記方法。元表只是普通的Lua table,所以可以用Lua直接操作,不需要特殊函數。就像標記一樣,元表可以用來表示userdata和table的用戶定義類型:所有“同類”對象應該共享同一個元表。不像標記,元表和他們的內容會在所有引用消失後自動被回收掉。(相反,標記和標記方法會等到程序結束纔會被回收。)元表的引入同時簡化了實現:標記方法需要在Lua核心代碼裏添加特殊的私有表示方法,元表主要就是標準的table機制。

下面的代碼展示了Lua 5.0裏,繼承是如何實現的。index元方法取代了index標記,元表裏則是用__index域來表示。代碼通過將b的元表裏的__index域指向a,實現了b繼承a。(一般情況下,index元方法都是函數,但我們允許它設爲table,以直接支持簡單的委託繼承。)

a=Window{x=100, y=200, color="red"}
b=Window{x=300, y=400}
setmetatable(b, {__index = a})
print(b.color) —>red

6.9 C API

Lua提供了C函數庫和宏,允許宿主和Lua通信。這層Lua和C之間的API是Lua的一個主要組成部分;它讓Lua變成了一門嵌入式語言。

就像Lua的其他部分一樣,API在Lua的演進裏發生了很多變化。可惜,和語言本身相比,API設計很少受到外來影響,主要是因爲這方面很少研究活動。

API一直是雙向的,因爲從Lua 1.0開始,我們就考慮了從C調用Lua,以及從Lua調用C,並認爲兩者同樣重要。能從C調用Lua讓Lua變成了擴展語言,即一門用於通過配置、宏和其他終端用戶定製功能,來擴充應用程序特性的語言。能從Lua調用C讓Lua變成了可擴展語言,因爲我們可以用C函數擴展Lua,爲其提供新設施。(這就是爲什麼我們說Lua是一門可擴展的擴展語言【30】)這兩個視角的共同點是,API必須處理好C和Lua之間的失配:C是靜態類型的,Lua是動態類型的,C是人工管理內存,Lua是自動進行垃圾回收的。

目前,C API通過引入一個虛擬棧來交換Lua和C的數據,解決了這兩個失配問題。每個Lua的C函數調用都會使用一個新的棧幀,包含了函數調用的參數。如果C函數需要給Lua返回值,它就在返回前把這些值壓到棧上。

每個棧槽可以保存任一Lua值類型。每個Lua類型都有C的唯一表示(比如字符串和數字),目前有兩個API 函數:一個是注入函數,用於壓棧一個Lua值,對應給出的C值;一個是投影函數,可以返回一個C值,對應指定棧位置的Lua值。Lua值類型裏沒有對應C表示的(比如table和函數),可以通過API及他們的棧位置來操作。

實際上,所有的API函數都從棧上獲得他們的操作數,並將結果放到棧上。因爲棧可以存放任意Lua值類型,這些API函數都可以操作任意Lua類型,因此解決了類型失配問題。爲了避免C使用中的Lua值被回收掉,棧上的所有值都不會被回收。當一個C返回,它的Lua棧幀消失了,所有的C函數使用的Lua值都會被釋放。如果沒有其他引用,這些值最終都會被回收。這就解決了內存管理的失配問題。

我們花了好多時間才實現成現在這套API的樣子。爲了討論API的演進,我們以等價於如下Lua函數的C代碼進行說明:

function foo(t)
    return t.x
end

換句話說,這個函數接受一個單參數,參數類型應該是table,並返回table裏儲存在x域的值。儘管很簡單,這個例子足夠說明API裏的三個重要問題:如何獲取參數,如何索引table,如何返回值。

在Lua 1.0裏,foo函數的C代碼如下:

void foo_l(void) {
    lua_Object t = lua_getparam(1);
    lua_Object r = lua_getfield(t, "x");
    lua_pushobject(r);
}

注意,我們所索引的值是存儲在字符串索引x,因爲t.xt["x"]的語法糖。而且所有的API都是以lua_(或者LUA_)開頭的,以此避免和其他C庫的命名衝突。

爲了暴露這個C函數給Lua,並使用foo作爲函數名,我們會這樣做

lua_register("foo", foo_l);

之後,foo就能像普通Lua函數一樣從C調用了:

t = {x = 200}
print(foo(t))         —> 200

API的一個關鍵部件是lua_Object類型,定義如下:

typedef struct Object *lua_Object;

簡而言之,lua_Object是一個抽象類型,用於在C裏表示Lua值。傳遞給C函數的參數,通過調用lua_getparam獲得,這個函數會返回lua_Object。在上述例子裏,我們調用一次lua_getparam來獲得table,這個table應當是foo函數的第一個參數。(其餘的參數會自動略過。)當table在C裏變成可用的(作爲lua_Object),我們就可以通過調用lua_getfield函數獲取x域的值。這個值在C裏也是表示爲lua_Object,最後會通過lua_pushobject壓棧,送回到Lua中去。

棧也是API的另一個關鍵部件。它用來從C裏傳值給Lua。每個Lua類型都有對應的C版本push函數:number類型有lua_pushnumber,string類型有lua_pushstring,特殊值nil可以用lua_pushnil。也有允許C回傳任意Lua值到Lua的lua_pushobject函數。當一個C函數返回,所有棧上的值都會返回給Lua,作爲C函數的結果(Lua的函數可以返回多個值)。

概念上,lua_Object是一個union類型,因爲它可以指代任意Lua值。很多腳本語言,比如Perl,Python和Ruby,依然使用union類型來在C層表示它們的值。這種表示方法的主要弊端,在於很難爲語言設計垃圾回收。沒有額外信息的情況下,垃圾回收器不可能知道,一個值是否被C代碼裏的一個union值任用。沒有這個信息,垃圾回收器有可能回收值,導致union變成了懸空指針。即使這個union是C函數的局部變量,這個C函數還是能再次調用Lua,並觸發垃圾回收流程。

Ruby通過檢查C棧來解決這個問題,這種方法難以移植。Perl和Python則是以另一種方式實現,通過爲union類型提供顯式引用計數函數來解決這個問題。一旦你增加了一個值的引用計數,垃圾回收器就不會回收那個值了,直到你將計數減爲0。但是,讓程序員保證引用計數的正確性並不容易。很容易會產生錯誤,但後期很難查出來(對內存泄漏和懸空指針排查過的人都可以作證)。此外,引用計數解決不了循環引用的問題。

Lua從來沒有提供過這類引用計數函數。Lua 2.1之前,爲了保證一個未被引用的lua_Object不被回收,你可以做的事情不多,頂多就是自己持有這個對象的引用,並避免調用Lua。(只要你可以保證union指向的值同樣儲存在一個Lua變量裏,你就是安全的。)Lua 2.1帶來了一個重要的變化:它跟蹤了所有傳給C的lua_Object,保證它們在C函數活躍的時候不會被回收掉。當C函數返回到Lua之後,(只有在這個時候)所有這些lua_Object的值引用都會被釋放,所以它們可以被回收。【JNI使用一個類似的方法來處理局部引用】

更具體地說,Lua 2.1裏,一個lua_Object不再是一個指向Lua內部數據結構的指針,而是一個內部數組的索引,這個內部數組存儲了所有傳給C的值:

typedef unsigned int lua_Object;

這個改動讓lua_Object的使用變得可靠了:當一個值在數組裏,Lua就不會回收它。當C函數返回了,整個數組就被釋放,並回收掉沒有其他引用的函數所使用的值。(這個改動也給實現垃圾回收帶來了更多的靈活性,因爲它可以按需移動對象了;但是,我們沒有這麼做)

對於簡單的使用,Lua 2.1的行爲是非常實際的:這種方案安全,C程序員也不用擔心引用計數。每個lua_Object就像C裏的一個局部變量:對應的Lua值會保證處理它的C函數的生命期內都存活。對於複雜的使用場景:這個簡單的方法有兩個缺陷,需要額外的機制保證:有時候,lua_Object的值需要鎖定一段比C函數生命期更長的時間;有時候,需要更短的鎖定時間。

第一種情況有一個簡單的解決辦法:Lua 2.1引入了引用系統。函數lua_lock從棧上取一個Lua值,並返回一個引用。這個引入是一個數字,可以以後任意時間使用來獲得Lua值,使用lua_getlocked函數即可。(同時還有lua_unlock函數,用於銷燬一個引用)通過這種引用方法,要保存非本地的C變量就變得簡單了。

第二種情況更爲微妙。存在內部數組的對象只有函數返回的時候纔會被釋放。如果函數使用了太多的值,就會發生數組越界,或者內存不足錯誤。比如,考慮以下的高階迭代函數,會迭代調用函數並打印結果,直到調用返回nil:

void l_loop(void) {
  lua_Object f = lua_getparam(1);
  for (;;) {
    lua_Object res;
    lua_callfunction(f);
    res = lua_getresult(1);
    if (lua_isnil(res)) break;
    printf("%s\n", lua_getstring(res));
  }
}

這段代碼的問題是,每個調用返回的字符串都不能回收,直到循環的底部(整個函數結束爲止),因此有可能發生數組越界或者內存耗盡。這種錯誤很難追蹤,因此Lua 2.1的實現設置了內部數組保存lua_Object的上限。Lua會報錯:“C函數裏太多對象”而非泛泛的“內存耗盡”錯誤,讓錯誤變得容易跟蹤,但沒有完全避免這一問題。

爲了解決這個問題,Lua 2.1的API提供了兩個函數,lua_beginblock和lua_endblock,爲lua_Object的值創建動態範圍(blocks);所有在lua_beginblock之後創建的值,都會在對應的lua_endblock調用後,從內部數組裏被移除。但是,因爲塊原則(block discipline)不能強制讓C程序員遵守,很容易就會忘記使用這些區塊。而且,這些顯式作用域控制用起來有點棘手。比如,一個欠周到的修復前述例子的方法,是用塊包裹住for循環的循環體,但這樣做存在問題:我們需要在break語句所在位置,也調用lua_endblock。這個Lua對象擴大生命週期的困難,經歷了數個版本,最後在Lua 4.0才解決,我們重構了整個API。無論如何,就像我們之前提到的那樣,對於普通應用,API是非常易用的,而且大部分程序員永遠不會遇到這裏描述的情景。更重要的是,API是安全的。錯誤用法會產生定義好的錯誤,但不會產生懸空指針或者內存泄露。

Lua 2.1還爲API帶來了其他改變。其中之一是引入了lua_getsubscript,允許使用任何值來索引table。這個函數沒有顯式參數:它從棧上取出table和key。老的lua_getfield被重定義爲宏,以實現兼容:

#define lua_getfield(o,f) \
    (lua_pushobject(o), lua_pushstring(f), \
     lua_getsubscript())

(C API的向後兼容性通常用宏來實現,只要代價可以接受。)

除了上述修改,從語法上講,API從Lua 1到Lua 2的變化是很少的。比如說,我們的說明函數foo可以直接用Lua 1.0的版本,在Lua 2.0依然可以運行。lua_Object的含義變了,lua_getfield用新的原語實現了,但對於普通用戶來說,就好像沒有變化一樣。因此,API一直很穩定,持續到Lua 4.0版本。

Lua 2.4擴展了引用機制以支撐弱引用。Lua程序的一個常見設計,是使用一個Lua對象(通常是一個table)作爲一個C對象的代理。常見的場景是,C對象必須知道它的代理是什麼,並保留對代理的引用。但是,那個引用會阻止對代理對象的回收,即使對象從Lua裏不能再訪問了。在Lua 2.4版本,就可以創建一個弱引用指向proxy;那個引用不會阻止代理對象的回收。獲取一個被回收的引用將返回一個特殊值LUA_NOOBJECT。

Lua 4.0爲C API帶來了兩個新穎的設計:支持多個Lua虛擬機和用於C和Lua交換值的虛擬棧。要實現多個獨立的Lua虛擬機,就需要去掉所有全局的虛擬機。直到Lua 3.0爲止,都只有一個Lua虛擬機存在,而且使用了很多靜態變量,分散在代碼裏。Lua 3.1 引入了多個獨立Lua虛擬機;所有靜態變量都歸集到一個單一的C結構體裏去。添加了一個新的API來切換虛擬機,但任意時刻只有一個Lua虛擬機是活躍的。所有其他API函數都是針對活躍虛擬機操作的,而活躍虛擬機並沒有出現在調用裏。Lua 4.0在API引入了顯式的Lua虛擬機。這引起了和前述版本的不兼容。【17】所有和Lua通訊的C代碼(尤其是向Lua註冊的C函數)需要在C API中添加一個顯式的虛擬機變量。既然所有C函數都必須重寫,我們藉此機會在Lua 4.0裏大改了C-Lua交流方式:我們替換了lua_Object的概念,代之以顯式的虛擬棧,用來在Lua和C之間雙向通信。這個棧也能用來存儲臨時值。

在Lua 4.0裏,我們的foo函數可以改寫如下:

int foo_l (lua_State *L) {
   lua_pushstring(L, "x");
   lua_gettable(L, 1);
   return 1;
}

第一個區別是函數簽名:foo_l現在接受一個Lua虛擬機,用於接收操作數和返回函數返回值。在之前版本里,函數調用完畢後,所有殘留在棧上的值都會返回Lua。現在,因爲所有操作都使用棧,它可以包含除返回值外的中間值,所以函數需要告訴Lua,棧上到底有幾個元素是返回值。另一個區別是,不再需要lua_getparam了,因爲函數參數在函數開始時就在棧上,並可以透過他們的index直接訪問,就好像其他棧上的值一樣。

最後的區別是lua_gettable的使用,這個函數用於替代lua_getsubscript,用於訪問table裏的域。lua_gettable接受一個棧索引(而不是一個Lua對象),索引指向所操作的table,從棧頂彈出key,並將結果壓棧。該函數不改變table在棧中的位置,因爲table經常重複索引。在函數foo_l裏,lua_gettable使用的table在棧位置1,因爲他是函數的第一個參數,key是字符串"x",需要在調用lua_gettable之前壓棧。這個函數調用將棧中的key,換成了對應的table裏的值。所以,在lua_gettable之後,棧上有兩個值:棧位置1的table,和棧位置2上key索引到的結果。這個C函數返回1,告訴Lua使用棧頂的值作爲函數的唯一返回值。

爲了更清楚的說明新的API,這是我們循環例子的Lua 4.0實現:

int l_loop (lua_State *L) {
  for (;;) {
    lua_pushvalue(L, 1);
    lua_call(L, 0, 1);
    if (lua_isnil(L, -1)) break;
    printf("%s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
  }
  return 0;
}

爲了調用一個Lua函數,我們將它壓到棧上,並壓入其參數(上面的例子沒有)。然後我們調用lua_call,告訴lua要從棧上獲取多少個參數(因此隱式的表達了函數在棧上的位置),要在調用裏獲取多少個結果。在例子裏,我們沒有使用參數,期待一個結果。lua_call函數將函數和參數從棧上移除,並壓入指定數目的結果。調用lua_pop會從棧上去掉一個返回值,讓棧在循環開始時處在相同的位置。爲方便起見,我們可以用正數索引棧,表示從底部開始的位置,或者負數來表示從棧頂開始的位置。在示例中,我們使用-1作爲索引,讓lua_isnil和lua_tostring從棧頂開始索引元素,該位置包含了函數調用的結果。

後見之明,在API裏使用一個單獨的棧是很明顯的簡化措施,但是Lua 4.0發佈的時候,很多用戶都抱怨新api的複雜性。儘管Lua 4.0的API有更簡潔的概念模型,直接操作棧還是需要一些思考來保證正確性。很多用戶都寧願用前一版的API,即使它沒有任何清晰的概念模型來表達其原理。簡單的任務不需要概念模型,之前的API因此工作的很好。更復雜的任務經常會打破用戶自定義的自有模式,因爲大部分的用戶從未用C來編碼複雜項目。所以,新API第一眼看上去太複雜了。但是,這些懷疑論最後都消失了,因爲用戶理解並肯定了新模型,這個模型證明了自身的簡潔、不易出錯。

Lua 4.0多個虛擬機共存的可能性,導致了有關引用機制的意料之外的問題。之前,一個C庫如果需要保留固定對象,可以創建這個對象的引用,並存儲在全局C變量裏。在Lua 4.0,如果一個C庫是和多個虛擬機協作的,它就需要爲每個虛擬機保留單獨的引用,不能用全局C變量來表示了。爲了解決這個難題,Lua 4.0引入了註冊表(registry),註冊表就是一個普通的Lua表,只在C層使用。有了註冊表,C庫想要保留Lua對象,就能選擇唯一的key索引,並在註冊表裏用這個key來關聯對象。因爲每個獨立的Lua虛擬機都有自己的註冊表,C庫可以在所有虛擬機裏都使用同樣的key來操作對應的對象。

我們可以輕易的在註冊表上實現之前的引用機制,只需要使用整數key來表示索引即可。要創建一個新的索引,我們只需要找到沒使用的整數key,並在那個key存儲值。獲取一個引用就是一個簡單的table訪問。但是,我們不能使用註冊表實現弱引用。所以,Lua 4.0保持了之前的引用機制。Lua 5.0裏,由於語言本身引入了弱表,我們終於可以將核心裏的引用機制幹掉,並移到庫裏。

C API在向着完整性緩慢演進。從Lua 4.0開始,所有標準庫函數都可以只使用C API來編寫。在Lua 4.0之前,Lua有數目可觀的內置函數(Lua 1.1有7個,Lua 3.2有35個),大部分可以使用C API編寫,但因爲追求速度,我們並沒有這樣做。有少量內置函數是不能用C API來編寫的,因爲C API並不完整。比如,Lua 3.2之前是不可能用C API來遍歷table的內容的,儘管在Lua裏可以使用內置函數next去實現。目前,C API也尚不完整,不是所有Lua能幹的都可以用C API完成;比如,C API還缺少對Lua值進行算術運算的函數。我們計劃下個版本解決這個問題。

6.10 userdata

從第一版開始,Lua的一個重要特性就是可以直接操作C數據,這個特性是通過提供userdata這種特殊Lua數據類型來實現的。這種能力是Lua可擴展性的基礎。

對於Lua程序來說,userdata類型在Lua的演進裏從未改變:儘管是第一類值類型,userdata還是透明的類型,在Lua裏唯一有效的操作就是相等性判斷。其他針對userdata的操作(比如創建,探測,修改)都需要C函數提供。

對於C函數而言,userdata類型在Lua的演進裏經歷很多變更。在Lua 1.0裏,一個userdata值只是一個簡單的void*指針。這種簡化方法等主要問題是,C庫沒有辦法檢查一個userdata是不是有效的。儘管Lua代碼不能創建userdata值,但可以將一個庫創建的userdata傳給另一個庫,而另一個庫期待的其實是指向不同結構的指針。因爲C函數沒有機制檢查這種失配,指針適配往往對應用程序造成致命後果。我們一直認爲,Lua程序導致宿主程序崩潰是不可接受的。Lua應該是一門安全的語言。

爲了克服指針失配的問題,Lua 2.1引入了標記的概念(也是Lua 3.0裏標記方法的概念來源)。一個標記只是一個任意的整數值,關聯到一個userdata上。一個userdata的標記只能在創建時設置一次,假設每個C庫都使用自己的獨特的標記,C代碼可以檢查userdata的標記,確保userdata就是所期待的類型。(一個庫作者如何選擇不會和別人重複的tag還是一個問題。這個問題只有在Lua 3.0纔得到解決,在這個版本Lua提供了lua_newtag作標記管理)

Lua 2.1更大的問題是C資源的管理。userdata往往指向一塊在C裏動態分配的內存,當對應userdata在Lua裏被回收,這塊內存也需要釋放掉。但是,userdata是值,不是對象。因此,他們不會被回收(就像number不會被回收一樣)。爲了克服這個限制,一個典型的設計是利用table作爲C結構在Lua中的代理,將實際的userdata存儲在預定義的域裏。當table被回收,它的析構函數就釋放對應的C結構。

這種簡單的解決方案會產生潛在問題。因爲userdata是存在代理table的一個普通域裏面,一個惡意的用戶有可能在Lua裏污染它。特別是,一個用戶可以創建userdata的副本,並在table被回收後繼續使用這個副本。這個時候,對應的C結構已經被銷燬了,userdata就變成懸空指針了,這會帶來災難性後果。爲了改善對userdata生命週期的控制,Lua 3.0將userdata從值類型改爲對象類型,以便垃圾回收。用戶可以使用userdata析構函數(垃圾回收的標記方法)來釋放對應的C結構。Lua的垃圾回收器的正確性,保證了userdata不能在回收後再被使用。

但是,userdata作爲對象會帶來一致性問題。給出一個userdata,要獲得對應的指針是很簡單的,但是我們經常需要做相反的事:給出一個C指針,獲得對應的userdata。在Lua 2中,兩個具有相同指針和相同標記的userdata就是相同的;相等性是基於它們的值。所以,給出指針和標記,我們就能獲得userdata。在Lua 3,因爲userdata變成了對象,所以相等性變成了一致性:兩個userdata只有在兩者同一的情況下才會相同(也就是說,這兩個userdata是同一個)。每個userdata的創建都不一樣。因此,一個指針和標記不足以獲得對應的userdata。

爲了解決這一困難,並解決與Lua 2的不兼容問題,Lua 3採用了下面的語意來將一個userdata壓到棧上:如果Lua已經有一個userdata,擁有給出的指針和標記,那麼那個userdata就會被推到棧上;否則就創建一個新的userdata並壓入棧。所以,C代碼要翻譯一個C指針倒對應的userdata就變簡單了。(實際上,C代碼可以沿用Lua 2的。)

但是,Lua 3的行爲有一個主要缺陷:它將兩個基本操作合成了一個原語(lua_pushuserdata):搜索userdata和創建userdata。比如,沒有辦法在不創建userdata的情況下,檢查一個給定的C指針有沒有對應的userdata。而且,也不能不管C指針,直接創建一個新的userdata。如果Lua已經有對應值的userdata,就不能再創建新的userdata了。

Lua 4引入了新函數lua_newuserdata以緩解這個問題。不像lua_pushuserdata,這個函數總是會創建一個新的userdata。而且,更重要的是,這些userdata可以存儲任意C數據,而不僅僅是指針。用戶可以告訴lua_newuserdata需要分配多少內存,lua_newuserdata會返回一個指針,指向所分配的內存。通過讓Lua給用戶分配內存,很多常見的與userdata相關的任務都簡化了。比如,C代碼不需要處理內存分配錯誤,因爲Lua已經接手了。更重要的是,C代碼不需要處理內存釋放:這種userdata使用的內存會在userdata被回收的時候,被Lua自動釋放。

但是,Lua 4依然沒有提供一個漂亮的解決方案來應對搜索問題(即通過給定C指針尋找一個userdata)。所以,它保留了lua_pushuserdata的行爲,結果產生了一個混合的系統。只有在Lua 5的時候,我們才刪除了lua_pushuserdata,並去掉userdata創建和搜索的關聯。實際上,Lua 5將整個搜索設施都移除了。Lua 5也引入了輕userdata(light userdata),只用來存放C指針,跟Lua 1的普通userdata完全一致。應用程序可以使用弱表來關聯C指針(light userdata所包含的)和對應的“重”userdata。

正如其他Lua演進一樣,Lua 5的userdata比Lua 4的更加靈活;也更容易解釋和實現。對於簡單的用例,比如只需要存儲一個C結構,Lua 5的userdata就很易用。對於更爲複雜的需求,比如需要將一個C指針映射到一個Lua的userdata,Lua 5提供了機制(light userdata和弱表)給用戶來實現適合宿主應用的策略。

6.11 反射性

Lua從剛開始的版本就支持一些反射設施。提供支持的主要原因是,Lua的建議用法是作爲替換SOL的配置語言。正如第四章所述,我們的設想是,程序員若有需要,可以使用語言本身,寫日常的類型檢查。

比如,如果一個用戶寫了下面的代碼:

T = @track{ y=9, x=10, id="1992-34" }

我們希望可以檢查到,track的確有一個域y,這個域就是一個數字。我們也希望可以校驗出track沒有外來的域(比如檢查出打字輸入錯誤)。爲了完成這兩個任務,我們需要訪問到Lua值的類型,以及遍歷一個table的機制,並訪問所有鍵值對。

Lua 1.0用兩個函數完成了所需的功能,這兩個函數延續到今天:type和next。type函數返回一個描述給定值的類型的字符串(比如number, nil, table之類)。next函數接受一個table和一個鍵,並返回一個table裏的“下一個”鍵(以任意順序)。以next(t, nil)調用會返回“第一個”鍵。通過next函數,我們可以遍歷一個table並處理所有的鍵值對。比如,下面的代碼可以打印table t的所有鍵值對:

k = next(t, nil)
while k do
    print(k, t[k])
    k = next(t,k)
end

所有這些函數都有一個簡單的實現:type檢查給定值的內部標記,並返回對應的字符串;next根據內部table的表示,在table裏尋找給定的key並訪問下一個key。

在Java和Smalltalk這些語言裏,反射需要具體的概念比如類、方法、實例變量。而且,這種具像化還需要新的概念比如元類(具體類的類)。Lua不需要這些東西。Lua裏,大部分類似Java反射包提供的設施都是免費的:類和模塊都是table,方法就是函數。所以,Lua不需要任何特別的機制來具像化;它們就是樸素的程序值。類似的,Lua不需要特殊機制來在運行時建立方法調用(因爲函數是第一類值,而且Lua的參數傳遞機制天然支持可變數量參數傳遞),也不需要特殊的機制來訪問一個全局變量或者根據一個給定的名字訪問一個實例變量(因爲它們都是普通的table域)。

7 回顧 這一節我們會給出一個Lua演進的簡短評論,討論其中做得好的,我們後悔的,以及我們不後悔,但是覺得可以以不同方式實現的。

最高明的決策,莫過於Lua 1.0的時候決定使用table作爲Lua唯一的數據結構機制。table被證明是強大而高效的。table在語言裏的核心地位和它的實現,是Lua的主要特性。我們頂住了用戶要求添加其他數據結構的壓力,主要是添加“真正的”數組和元祖,對此我們表現得十分頑固,但我們也提供了高效實現的table和靈活的設計。比如,我們可以實現一個set(集合),只需要將它的元素作爲table的key即可。全賴Lua的table接受任意值作爲key,這個特性才得以實現。

另一個則是我們對可移植性的堅持,最初是因爲Tecgraf客戶處在大量不同的平臺上。這允許Lua運行在我們從未夢想得到支持的平臺上。特別是,可移植性是Lua被遊戲開發廣泛採用的原因之一。受限的環境,比如遊戲主機,一般不支持完整語意的全套標準C庫。最終,通過減少Lua核心對標準C庫的依賴,我們將Lua核心打造成只依賴獨立的ANSI C實現標準。這一舉措主要是爲了嵌入的靈活性,但同樣增加了可移植性。比如,從Lua 3.1開始,只要擺弄代碼裏少數幾個宏,就可以讓Lua使用一個應用程序自定義的內存分配器,而不是直接使用malloc等內存分配函數。從Lua 5.1開始,內存分配器可以在創建Lua虛擬機的時候動態提供。

事後來看,我們認爲在一個小型委員會的撫養下成長,對於Lua的演進是十分積極的。由一個龐大委員會設計的語言,傾向於變得十分複雜,從未完全實現過它們支持者的期望。大部分成功的語言都是被撫養長大,而非設計出來的。它們遵循着一個緩慢的自下而上的過程,以一門小語言開始,帶着謙虛的目標。語言的成長是真實用戶實際反饋的成果,這個過程裏設計漸漸浮出水面,實際有效的新特性得到認同。這個描述正是對Lua演進的真實寫照。我們傾聽用戶的反饋和建議,但我們只在三人同意的前提下才添加一個新的特性;否則,就留待未來決定。稍後再添加特性比移除特性要容易得多。這個開發過程對於保持語言的簡潔性至關重要,而簡潔性是我們最重要的資本。絕大部分Lua的其他特性——速度、體積小、可移植性——都是從簡潔性而來。

從第一個版本開始,Lua就有真實用戶了,也即是除了我們自己以外的用戶,他們根本不關心語言本身,只關心如何高效率的使用這門語言。用戶總是爲語言做出重要貢獻,通過建議,抱怨,使用報告,和問題等形式。我們的小小委員會在管理這類反饋的時候再一次扮演了重要角色:它的結構給我們足夠的慣性,去貼近用戶、傾聽用戶,但不完全遵循它們的建議。

Lua的最佳描述是:一個封閉開發的開源項目。這意味着,儘管源碼對於審查和採用是免費可得的,Lua還是沒有以合作的方式開發。我們會採納用戶建議,但從未逐字逐句採納他們的代碼。我們總是嘗試實現我們自己的設計。

Lua演進的另一個不尋常的方面,是我們處理不兼容修改的方式。很長一段時間裏,我們都認爲簡潔和優雅比兼容前一個版本更爲重要。當一個老的特性被新特性取代,我們就刪掉老的特性。我們經常(但非總是)提供一些兼容性工具,比如一個兼容的庫,一個轉換腳本,或者編譯時的選項來保留老特性。在這些情形下,用戶需要在轉移到新版本的時候採取一定措施。

我們並未真正後悔這種演進方式。但是,我們最終變得更爲保守了。不僅因爲我們的用戶和代碼基變得比以前更大,還因爲我們覺得作爲一門語言,Lua變得更成熟了。

我們應該從一開始就引入布爾類型,但我們希望從最爲簡潔的語言開始。沒有一開始就引入布爾類型,帶來了一些不幸的副作用。其中一個是我們現在有兩個false值了:nil和false。另一個是,Lua函數用於警告調用者錯誤發生的常見用法,是返回nil值接着一段錯誤信息。如果用false而非nil,就更適合這個場景了,nil可保留作爲缺少值的含義。

在算術運算時,自動將字符串轉化爲數字,是我們從Awk裏參考的,其實可以忽略。(在字符串操作中自動將數字轉換爲字符串則很方便,也更少問題。)

除了我們的“機制,而非策略”的規則外——我們覺得在Lua演進裏這個規則很有價值——我們應該爲模塊和包提供一個準確的指導方針。缺乏通用的構建模塊和安裝包的策略,限制了不同工作組共享代碼,不利於發展整個社區的代碼基。Lua 5.1提供了模塊和包的指導方針,我們希望能夠補救現存的局面。

正如6.4節提到的,Lua 3.0引入了條件編譯的支持,主要是爲了提供一種方式來關閉代碼。我們收到很多請求,要求加強Lua的條件編譯,這些請求甚至來自於不使用條件編譯的用戶!目前最大的需求是一個完整的宏處理器,就像C的預處理器一樣。提供這樣一個宏處理器,跟我們提供擴展機制的哲學是一致的。但是,我們希望宏也能用Lua編寫,而不是其他特殊語言。我們不希望直接在詞法器上添加宏設施,以免其變得臃腫不堪,拖慢編譯過程。更重要的是,那個時候Lua語法分析器還不是完全可重入的,所以沒有辦法在詞法器裏調用Lua。(這個限制在Lua 5.1去掉了)所以郵件列表裏和Lua開發者裏有無窮無盡的討論。我們依然希望能夠爲Lua提供一個宏系統:它會給Lua提供更靈活的語法,支持更靈活的語義。

8 結論

Lua在很多大型公司裏大獲成功,比如Adobe,Bombardier,Disney,Electronic Arts,Intel,LucasArts,Microsoft,Nasa,Olivetti和Philips。這些公司很多產品都直接在商用產品裏嵌入Lua,並向最終用戶暴露Lua腳本。

Lua在遊戲領域特別成功。最近流行一個說法:“Lua正迅速變成遊戲腳本的事實標準”【37】。兩個在gamedev.net進行的非正式的投票【5,6】,分別在2003年9月和2006年6月結束,結果顯示Lua正是最炙手可熱的遊戲開發腳本語言。GDC專門針對遊戲開發中的Lua的圓桌會議舉行了兩次,分別是2004年和2006年。很多著名遊戲都使用Lua:Baldur’s Gate, Escape from Monkey Island, FarCry, Grim Fandango, Homeworld 2, Illarion, Impossible Creatures, Psychonauts, The Sims, World of Warcraft。現在有兩本使用Lua進行遊戲開發的書【42,25】,而且很多其他遊戲開發的書都會開闢專門的章節來講述Lua【23,44,41,24】。

Lua在遊戲界的廣泛使用是我們的驚喜收穫。我們並沒有爲Lua考慮過遊戲開發這個目標。(Tecgraf主要考慮的是科學軟件)事後看來,這種成功是可以理解的,因爲所有讓Lua變得與衆不同的特質,對遊戲開發都十分重要:

可移植性:很多遊戲跑在非傳統的平臺上,比如遊戲機上,這些平臺需要特殊的開發工具。構建Lua只需要一個ANSI C編譯器。

容易嵌入:遊戲是性能敏感型應用。他們既需要性能,來應付圖形和模擬,還需要靈活性,以應對創意的部分。很多遊戲都用至少兩種語言開發,這並非偶然,一種語言用於腳本編寫,另一種用於構建引擎。在這個框架內,輕鬆集成Lua到其他語言(在遊戲界主要是C++)是一個巨大的優勢。

簡潔:很多遊戲設計師,腳本編寫者,關卡作者都不是專業的程序員。對他們來說,一門語法簡潔,語義清晰的語言尤其重要。

高效率,小體積:遊戲是性能敏感應用程序;分配給腳本運行的事件通常都很小。Lua是最快的腳本語言之一【1】。遊戲機是受限環境。腳本解釋器應該節約資源。Lua核心的體積只有100K。

對代碼的控制:不像其他大部分的軟件企業,遊戲產品很少演化。很多時候,一個遊戲發佈之後,就沒有更新或者新版本了,只有新遊戲。所以,冒險在遊戲裏使用一門新的腳本語言更容易。一門腳本語言是否會演進,如何演進,對於遊戲開發者來說不是一個很關鍵的點。他們所需要的就只是在遊戲裏使用的版本。因爲他們對Lua源碼有完全訪問的權限,他們可以選擇永遠保持在同一個Lua版本。

自由的協議:很多商業遊戲是不開源的。很多遊戲公司甚至拒絕使用任何開源代碼。因爲競爭非常激烈,所以遊戲公司傾向於保守技術上的祕密。對他們來說,像Lua一樣的自由協議是非常方便的。

協程:如果腳本語言支持多任務,編寫腳本遊戲會變的更簡單,因爲一個角色或者活動可以被暫停,並稍後恢復。Lua通過協程支持協作式多任務【14】。

過程式數據文件:Lua的原始設計目標是提供威力強大的數據描述設施,這允許遊戲使用Lua組織數據文件,替換特殊格式的文本數據文件,帶來諸多益處,尤其是同源性和表達力。

致謝

Lua是在大家的幫助下成長起來的。Tecgraf的所有人都以不同形式爲Lua貢獻——使用這門語言,討論它,向Tecgraf以外傳播它。特別鳴謝Marcelo Gattass,作爲Tecgraf的主管,總是鼓勵我們並給予我們完全的自由,去演進Lua這門語言和它的實現。Lua不再是Tecgraf的一個產品,但依然在PUC-Rio內開發,開發組屬於2004年5月建成的LabLua實驗室。

沒有用戶的話,Lua就只是另外一門語言,註定被遺忘。用戶和他們的使用是一門語言的試金石。特別鳴謝我們郵件列表的成員,感謝他們的建議、抱怨和耐心。郵件列表相對較小,但非常友善,並有一些Lua開發團隊以外的技術水平很高的人士參與,他們慷慨的向整個社區分享自己的專業知識,讓人獲益匪淺。

我們感謝Norman Ramsey的牽頭,Norman Ramsey建議我們在HOPL III上發表一篇有關Lua的論文,並幫助我們聯繫到會議管理團隊。我們感謝Julia Lawall,幫助我們通讀數遍這篇文章的草稿,並代表HOPL III委員會仔細處理本文。我們感謝Norman Ramsey,Julia Lawall,Brent Hailpern, Barbara Ryder,以及未具名的編輯,感謝他們詳細的評論和富有建設性的建議。

我們也向Andre ́ Carregal, Anna Hester, Bret Mogilefsky, Bret Victor, Daniel Collins, David Burgess, Diego Nehab, Eric Raible, Erik Hougaard, Gavin Wraith, John Belmonte, Mark Hamburg, Peter Sommerfeld, Reuben Thomas, Stephan Herrmann, Steve Dekorte, Taj Khattra和Thatcher Ulrich致謝,感謝他們對Lua發展歷程的補充,並對相關文本進行潤色。Katrina Avery做了很好的拷貝-編輯工作。

最後,我們感謝PUC-Rio,IMPA和CNPq對我們Lua工作一如既往的支持,以及FINEP和微軟研究院對數個Lua相關項目的支持。

相關文獻:

[1] The computer language shootout benchmarks. http:
//shootout.alioth.debian.org/.
[2] Lua projects. http://www.lua.org/uses.html.
[3] The MIT license. http://www.opensource.org/
licenses/mit-license.html.
[4] Timeline of programming languages. http://en.
wikipedia.org/wiki/Timeline of programming
languages.
[5] Which language do you use for scripting in your game
engine? http://www.gamedev.net/gdpolls/viewpoll.
asp?ID=163, Sept. 2003.
[6] Which is your favorite embeddable scripting language?
http://www.gamedev.net/gdpolls/viewpoll.asp?
ID=788, June 2006.
[7] K. Beck. Extreme Programming Explained: Embrace
Change. Addison-Wesley, 2000.
[8] G. Bell, R. Carey, and C. Marrin. The Virtual Reality
Modeling Language Specification—Version 2.0.
http://www.vrml.org/VRML2.0/FINAL/, Aug. 1996.
(ISO/IEC CD 14772).
[9] J. Bentley. Programming pearls: associative arrays. Communications
of the ACM, 28(6):570–576, 1985.
[10] J. Bentley. Programming pearls: little languages. Communications
of the ACM, 29(8):711–721, 1986.
[11] C. Bruggeman, O. Waddell, and R. K. Dybvig. Representing
control in the presence of one-shot continuations. In
SIGPLAN Conference on Programming Language Design
and Implementation, pages 99–107, 1996.
[12] W. Celes, L. H. de Figueiredo, and M. Gattass. EDG: uma
ferramenta para criac¸ ˜ao de interfaces gr´aficas interativas.
In Proceedings of SIBGRAPI ’95 (Brazilian Symposium on
Computer Graphics and Image Processing), pages 241–248,
1995.
[13] B. Davis, A. Beatty, K. Casey, D. Gregg, and J.Waldron. The
case for virtual register machines. In Proceedings of the 2003
Workshop on Interpreters, Virtual Machines and Emulators,
pages 41–49. ACM Press, 2003.
[14] L. H. de Figueiredo, W. Celes, and R. Ierusalimschy.
Programming advanced control mechanisms with Lua
coroutines. In Game Programming Gems 6, pages 357–369.
Charles River Media, 2006.
[15] L. H. de Figueiredo, R. Ierusalimschy, and W. Celes. The
design and implementation of a language for extending
applications. In Proceedings of XXI SEMISH (Brazilian
Seminar on Software and Hardware), pages 273–284, 1994.
[16] L. H. de Figueiredo, R. Ierusalimschy, and W. Celes. Lua:
an extensible embedded language. Dr. Dobb’s Journal,
21(12):26–33, Dec. 1996.
[17] L. H. de Figueiredo, C. S. Souza, M. Gattass, and L. C. G.
Coelho. Gerac¸ ˜ao de interfaces para captura de dados sobre
desenhos. In Proceedings of SIBGRAPI ’92 (Brazilian
Symposium on Computer Graphics and Image Processing),
pages 169–175, 1992.
[18] A. de Moura, N. Rodriguez, and R. Ierusalimschy. Coroutines
in Lua. Journal of Universal Computer Science, 10(7):910–
925, 2004.
[19] A. L. de Moura and R. Ierusalimschy. Revisiting coroutines.
MCC 15/04, PUC-Rio, 2004.
[20] R. K. Dybvig. Three Implementation Models for Scheme.
PhD thesis, Department of Computer Science, University
of North Carolina at Chapel Hill, 1987. Technical Report
#87-011.
[21] M. Feeley and G. Lapalme. Closure generation based on
viewing LAMBDA as EPSILON plus COMPILE. Journal of
Computer Languages, 17(4):251–267, 1992.
[22] T. G. Gorham and R. Ierusalimschy. Um sistema de
depurac¸ ˜ao reflexivo para uma linguagem de extens˜ao.
In Anais do I Simp´osio Brasileiro de Linguagens de
Programac¸ ˜ao, pages 103–114, 1996.
[23] T. Gutschmidt. Game Programming with Python, Lua, and
Ruby. Premier Press, 2003.
[24] M. Harmon. Building Lua into games. In Game Programming
Gems 5, pages 115–128. Charles River Media, 2005.
[25] J. Heiss. Lua Scripting f¨ur Spieleprogrammierer. Hit the
Ground with Lua. Stefan Zerbst, Dec. 2005.
[26] A. Hester, R. Borges, and R. Ierusalimschy. Building flexible
and extensible web applications with Lua. Journal of
Universal Computer Science, 4(9):748–762, 1998.
[27] R. Ierusalimschy. Programming in Lua. Lua.org, 2003.
[28] R. Ierusalimschy. Programming in Lua. Lua.org, 2nd edition,
2006.
[29] R. Ierusalimschy, W. Celes, L. H. de Figueiredo, and
R. de Souza. Lua: uma linguagem para customizac¸ ˜ao de
aplicac¸ ˜oes. In VII Simp´osio Brasileiro de Engenharia de
Software — Caderno de Ferramentas, page 55, 1993.
[30] R. Ierusalimschy, L. H. de Figueiredo, and W. Celes. Lua:
an extensible extension language. Software: Practice &
Experience, 26(6):635–652, 1996.
[31] R. Ierusalimschy, L. H. de Figueiredo, and W. Celes. The
implementation of Lua 5.0. Journal of Universal Computer
Science, 11(7):1159–1176, 2005.
[32] R. Ierusalimschy, L. H. de Figueiredo, and W. Celes. Lua 5.1
Reference Manual. Lua.org, 2006.
[33] K. Jung and A. Brown. Beginning Lua Programming. Wrox,
2007.
[34] L. Lamport. LATEX: A Document Preparation System.
Addison-Wesley, 1986.
[35] M. J. Lima and R. Ierusalimschy. Continuac¸ ˜oes em Lua.
In VI Simp´osio Brasileiro de Linguagens de Programac¸ ˜ao,
pages 218–232, June 2002.
[36] D. McDermott. An efficient environment allocation scheme
in an interpreter for a lexically-scoped LISP. In ACM
conference on LISP and functional programming, pages 154–
162, 1980.
[37] I. Millington. Artificial Intelligence for Games. Morgan
Kaufmann, 2006.
[38] B. Mogilefsky. Lua in Grim Fandango. http://www.
grimfandango.net/?page=articles&pagenumber=2,
May 1999.
[39] Open Software Foundation. OSF/Motif Programmer’s Guide.
Prentice-Hall, Inc., 1991.
[40] J. Ousterhout. Tcl: an embeddable command language. In
Proc. of the Winter 1990 USENIX Technical Conference.
USENIX Association, 1990.
[41] D. Sanchez-Crespo. Core Techniques and Algorithms in
Game Programming. New Riders Games, 2003.
[42] P. Schuytema and M. Manyen. Game Development with Lua.
Delmar Thomson Learning, 2005.
[43] A. van Deursen, P. Klint, and J. Visser. Domain-specific
languages: an annotated bibliography. SIGPLAN Notices,
35(6):26–36, 2000.
[44] A. Varanese. Game Scripting Mastery. Premier Press, 2002.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章