- Lua解釋器構建:從虛擬機到編譯器
- 吳尹杰
- 3229字
- 2023-06-28 15:30:28
1.2.1 虛擬機簡介
前文介紹了Lua解釋器的整體架構和運行機制,本節將會對其內部的一些數據結構進行簡要的介紹。在后續的章節中,會逐漸豐富虛擬機的內容。回顧一下圖1-7,到目前為止,Lua解釋器的虛擬機被看作是一個黑盒子,那么這個黑盒子里有什么東西呢?由于Lua解釋器是用C語言開發的,并沒有什么面向對象的概念,因此也沒有一個虛擬機類對象。但是Lua虛擬機有一個很重要的數據結構,這個結構被稱為global_State。這個結構包含了為虛擬機開辟和釋放內存所需的內存分配函數,保存GC對象和狀態的成員變量,以及一個主線程結構實例、全局注冊表等。
要理解Lua虛擬機,有兩個特別重要的結構要弄清楚,一個是前面說的global_State結構,還有一個是Lua虛擬機自定義的“線程”結構,也被稱為lua_State結構。Lua虛擬機里的“線程”和操作系統的線程是有區別的。操作系統中多條線程之間可以并發(分時間片交替運行)或并行(在同一時刻、不同CPU核心內)執行,而Lua虛擬機的“線程”則不行。Lua虛擬機的“線程”切換,必須等正在運行的“線程”先執行完或者主動調用掛起函數,否則其他“線程”不會被執行。Lua虛擬機的“線程”實際上是運行在操作系統的線程內。在實踐中,一條操作系統線程,在同一時刻往往只會運行Lua虛擬機里其中的一條“線程”。當Lua虛擬機內部存在多個“線程”實例時,除了“主線程”,其他“線程”實際上是協程。
現在先來看一下global_State的整體結構。global_State結構里的成員可以先從大體概念去劃分,而不是過早地關注內部的細節,這樣有利于先建立整體的概念,然后順著概念逐個擊破。圖1-11展示了整個global_State大體的結構,這是Lua虛擬機最核心的數據結構?,F在對global_State結構的幾個部分分別進行解釋和說明。
· Allocator:這是Lua虛擬機的內存分配器,本質上是一個內存分配函數。虛擬機開辟內存和釋放內存均需要通過這個函數。用戶可以自定義內存分配器,也可以使用官方默認的。官方默認的最終會調用realloc和free函數。

·圖1-11
· GC fields:這是包含一系列和GC相關的成員,將在第2章詳細討論它們。
· String Table:這是短字符串的全局緩存。同樣地,對于字符串,將在第2章討論它的設計和實現。
· Registry:這是Lua虛擬機的全局注冊表。它本質上是一個Lua表對象,在全局注冊表Registry中只有數組被用到,并且第一個值是指向“主線程”的指針,而第二個值則是指向全局表(也就是_G)的指針。
· Mainthread:這是Lua虛擬機,指向“主線程”結構的指針。在Lua C層代碼中,可以很輕易地拿到global_State指針。而這個Mainthread指針,可以方便地獲取Mainthread對象。
剩下的部分是和元表、弱表等相關的內容,在第5章會介紹它們。
接下來要來介紹的內容則是Lua虛擬機“線程”結構。前文也已經提到過,Lua虛擬機的“線程”結構實際上是lua_State結構。lua_State結構的內容較多,這里將其抽象成若干個大的模塊,如圖1-12所示。下面對這些模塊分類進行簡要說明。
· GC相關:所謂GC即是Garbage Collection,也就是垃圾回收。所有垃圾回收相關的成員都歸為這類,第2章將詳細討論GC機制。
· Stack相關:每個Lua“線程”實例,都會有自己獨立的??臻g、信息等。這些部分包含在stack相關的域內,Lua的函數會在棧上執行,臨時變量也會暫存在棧上,同時棧的起始地址、大小信息也包含在這里面。此外,Lua虛擬機的虛擬寄存器也是直接使用棧上的空間,第4章將討論編譯器相關的內容。
· status:代表了Lua“線程”實例的狀態,Lua“線程”在初始化階段會被設置為LUA_OK。

·圖1-12
· global_State指針:指向Lua虛擬機中global_State結構的指針。
· CallInfo相關:這是函數調用相關的信息。前文提到過,函數(Lua函數和C函數)要被執行,首先函數實體要被壓入lua_State結構的棧中,然后再進行調用。CallInfo相關的信息則會記錄被調用的函數在棧中的位置。每個被調用的函數都有自己獨立的虛擬棧(lua_State棧中的某個片段),CallInfo信息會記錄獨立虛擬棧的棧頂信息。此外,它還記錄了被調用的函數有幾個返回值、調用的狀態以及當前執行的指令地址等,是和Lua棧一樣具有同等重要性的數據結構。
· 異常處理相關:當Lua棧內函數調用發生異常時,需要這些異常相關的變量協助進行錯誤處理。
到這里就已經完成Lua虛擬機中最重要的兩個數據結構的介紹了。實際上,需要介紹的內容還有很多,包括字符串和表等,這些將在后續章節詳細介紹。此外,相關資料對于虛擬機的定義,語言級別的虛擬機是用來運行獨立于平臺的程序(比如字節碼)。也就是說,Lua虛擬機也需要有解析并運行Lua字節碼的能力。
Lua虛擬機的運行主要是調用函數。在Lua虛擬機中被調用的函數類型主要有兩大類:一類是C函數,另一類是Lua函數。C函數又細分為Light C Function和C閉包(C Closure),Lua函數主要是指Lua閉包。本書將在后續章節介紹閉包的概念,這里僅舉兩個簡單的例子。先來看第一個例子,假設有一個C函數,如下所示。

在調用test_main04函數之前,首先要調用luaL_newstate函數創建一個Lua虛擬機實例,內存中將會得到一個global_State和lua_State實例。此時,虛擬機將test_main04函數壓入虛擬機“主線程”的棧中,然后壓入兩個整型參數:1和2,得到圖1-13所示的結果。
現在可以清晰地看到棧頂函數和參數的位置。那么此時,要在Lua虛擬機中運行test_main04函數,需要調用隨書代碼中的lua_pcall函數,其聲明如下所示。

·圖1-13

函數的第一個參數L是表示Lua“線程”;第二個參數narg表示要在Lua“線程”里執行的函數有多少個參數;第三個參數nresult表示在Lua“線程”里執行的函數有多少個返回值。
可以看到,test_main04函數是將兩個輸入參數相加再返回,那么要在虛擬機調用這個函數需要調用如下代碼。

這里需要說明的是,lua_pcall函數是隨書工程自定義的一個函數,為了方便敘述,它省略了官方同名函數中的最后一個參數。上面這行代碼代表的含義是,調用圖1-13中Lua“主線程”棧上位于top-(2+1)位置上的test_main04函數,其中參數有兩個,返回值是1個。接下來,就會執行test_main04函數,于是可以得到圖1-14所示的結果。
從圖1-14中,可以看到原來函數的位置被計算結果覆蓋了。而top指針則指向了計算結果的上方。
以上就是Lua虛擬機調用C函數的一個簡單例子。既然Lua虛擬機的作用是運行Lua腳本編譯出來的指令,為什么還要保留調用C函數的功能呢?這樣會不會多此一舉?答案是不會的。因為在Lua腳本中調用C函數,C庫可以作為Lua語言的高性能拓展,這也是為什么Lua能作為膠水語言的原因。

·圖1-14
現在來看第二個例子。假設有一個Lua腳本test.lua,腳本里的代碼只是一行“print("hello world")”的代碼,那么加載并運行這段腳本的流程又是怎樣的呢?首先,通過luaL_loadfile函數加載test.lua腳本,并進行編譯,此時會生成一個LClosure類型的實例(腳本被編譯之后的結果,被虛擬機運行前,要創建這樣一個實例來存放這些編譯結果和執行狀態)。它包含了一個Proto結構,Proto結構里包含了編譯好的指令和其他一些信息,這種狀態可以通過圖1-15來表示。在圖中讀者可以看到,編譯后的結果會被存放在Proto結構中,其中指令存放在code列表中,而腳本中的常量則被存放到常量表k中。圖中的Proto結構是被壓入棧中LClosure實例的一個成員。目前圖1-15所展示的是經過luaL_loadfile編譯后的狀態,接下來就是要讓虛擬機去運行LClosure實例中的代碼了。
調用lua_pcall(L,0,0)就能夠讓虛擬機找到LClosure函數,并且去執行它。虛擬機的執行函數,會逐個運行Proto結構中code列表的指令。
首先執行的是“OP_GETTABUP 0 0 256”指令。這個指令的等式表達為R(A)=_ENV[RK(C)](R表示寄存器,K表示常量表)。指令的第一個0表示目標寄存器的位置,在本例中就是stack上被標記為0的位置。第二個0表示從LClosure實例的第0個上值(upvalue)找RK(256)的變量。在Lua-5.3中,每個函數實例的第一個上值都是_ENV,而它默認指向_G。RK(C)的含義是:當C的值<256時,就去棧中找變量;當C≥256時,就到常量表k[C-256]中找值。結合起來,就是
R(A)=_ENV[RK(C)]==>R(0)=_ENV[k[C-256]]==>R(0)=_ENV[k[0]]==>R(0)=_G[“print”]
含義就是,從全局表_G中找到名為print的值,并且將其設置到stack上被標記為0的位置上。
接下來要執行的指令則是OP_LOADK,它的等式表達為R(A)=k[B]。結合本例的例子,可以得到如下推導:

·圖1-15
R(A)=k[B]==>R(1)=k[1]==>R(1)=“hello world”
往后就是執行OP_CALL指令(見附錄A),它可以用公式“Call R(A)B-1 C-1”表示。將當前情景代入,則是Call R(0)1 0,表示調用位于棧0的函數——print函數,輸入一個參數“hello world”,并返回0個值。完成調用后,屏幕輸出“hello world”,同時棧中0和1標記的位置就相當于清空狀態了。
對Lua虛擬機的介紹就到此為止了。本節首先對虛擬機兩個重要的數據結構global_State和lua_State進行簡要說明,然后對虛擬機運行C函數和Lua函數的流程也進行了簡要的說明,后面將介紹Lua虛擬機的指令編碼方式和指令集。
- Web應用系統開發實踐(C#)
- ASP.NET MVC4框架揭秘
- Python機器學習:數據分析與評分卡建模(微課版)
- Redis Applied Design Patterns
- Getting Started with ResearchKit
- MongoDB for Java Developers
- Developing Middleware in Java EE 8
- 云原生Spring實戰
- PLC編程及應用實戰
- Visual C++應用開發
- Android開發案例教程與項目實戰(在線實驗+在線自測)
- Python算法詳解
- Python全棧數據工程師養成攻略(視頻講解版)
- Vue.js 2 Web Development Projects
- Python從入門到精通