摘 要: 通過嵌入腳本引擎為應用程序提供腳本支持是實現應用程序可定制和可擴展的有效方法,但現存的腳本語言難于掌握,引擎龐大使應用程序的效率降低。為了解決該問題,設計了語法簡單易學的腳本語言Vblet,實現了Vblet的輕量級腳本引擎。該引擎支持腳本無縫地使用應用程序實現的類和函數,并具有很好的性能。
關鍵詞: 腳本引擎;腳本語言;可擴展;可定制;二次開發
腳本語言憑借強大的描述能力和靈活的語法結構,使得為應用程序提供腳本支持從而進行混合語言開發成為實現可擴展和可定制的有效方案[1]。出于穩定性和開發時間限制的考慮,開發人員傾向于嵌入現有腳本引擎的方法為應用程序提供腳本支持,如嵌入Python引擎為應用程序提供Python腳本支持,或使用Microsoft提供的ActiveX Scripting技術為應用程序嵌入VBScript引擎或JavaScript引擎提供相應的腳本支持。但是這樣方法靈活性較差,應用程序必須接受現有腳本引擎的體積和性能要求,這對運行在低硬件條件下的應用程序,或者是只要求進行簡單規則計算的小型應用程序來說,這種方法在效率上沒有優勢[2]。而且有些現有腳本語言比較難學,使得用戶把太多時間花在語言的學習上。因此,需要一個輕型的腳本引擎,能夠解釋運行一門語法簡單易學的腳本語言,該腳本語言對于工程應用領域的非正式程序員,可以經過短時間的學習培訓或者不經過學習就能掌握并使用。
針對以上問題,本文在自行設計的腳本語言Vblet的基礎上,開發實現了Vblet的輕型腳本引擎,支持腳本引擎被嵌入在C++實現的應用程序上。Vblet語言語法簡單,繼承了在非專業程序員中具有較高聲譽的VBA語言,并且借鑒了Python語言的部分功能,使得用戶能夠專注于問題的解決而不是語法的學習上。
1 腳本引擎概述
腳本引擎[3]是一個加載、解釋執行腳本,并負責與外界進行交互的程序。腳本引擎一般很少獨立存在,而是要嵌入應用程序中以擴展應用程序的行為,這個被嵌入腳本引擎的應用程序稱為宿主程序。
嵌入的腳本引擎如圖1所示。圖1中,腳本引擎通過某種交互接口,根據腳本源程序描述的邏輯來控制應用系統。根據宿主程序和腳本引擎之間的緊密層次不同,可將通信方式分為:(1)基于二進制接口的通信。(2)基于公共運行時環境的通信。(3)基于源碼接口的通信。本文實現的基于源碼的交互接口,即通信雙方基于共同的實現語言,在源代碼級上相互調用。
除了交互接口,作為腳本的解釋運行平臺,腳本引擎包含了一個編譯器前端程序,前端程序負責將腳本源代碼經過詞法分析、語法語義分析后生成字節碼格式的指令序列,然而這些指令序列是不能在目標機器上執行的。因此,在腳本引擎的最底層還要一個執行字節碼指令的程序,這個程序即稱為虛擬機。
2 腳本語言的設計
在開發實現腳本引擎之前,先要確定引擎要解釋執行的對象,即腳本語言。本文設計的腳本語言Vblet是VBA(Visual Basic for Applications)的子集,而VBA的語法簡單易學,在非專業程序員中有很大的用戶量,享有很高的聲譽。Vblet簡化了VBA語法,去掉了VBA語法中的一些限制,此外還根據需要擴展了部分功能。下面是Vblet不同于VBA的一些重要的語法特性:
(1)交互執行。這是借鑒了Python語言交互執行的語法特點,使得程序員可以單行執行語句或計算表達式,而不限于一定要把代碼封裝在代碼塊中。
(2)不需要變量和參數聲明。VBlet是動態語言,變量的類型由腳本引擎從上下文中確定,變量可以不經過聲明就可以使用。
(3)去掉了部分運算符,比如冒號運算符和逗號運算符。
為了提高性能,Vblet去除了VBA中一些庫函數的支持,只保留一些在工程應用領域比較常用的數學計算函數。
3 腳本引擎實現方案
Vblet引擎除了IDE的開發使用了MFC類庫之外,其他模塊的實現都是使用標準C++編寫的,這使得Vblet引擎只需要重新編寫IDE,或者修改小部分的核心代碼就能夠移植到其他平臺上。
3.1 前端編譯程序的實現
前端編譯程序將腳本源程序的字符流經過詞法分析、語法分析和語義分析后,生成字節碼表示的指令流,同時進行語法檢查,對語法錯誤給出提示信息。另外,為了支持斷點調試和異常信息顯示,每行源程序和生成的字節碼指令的對應關系也要在這里建立。
前端編譯器一般可以通過一些自動生成工具生成,但是這些自動生成的代碼效率都不夠高或者不好閱讀,因此本文采用手寫的方式實現前端編譯程序。前端程序由Scanner類和Compiler類兩大主要模塊組成。Scanner類主要負責源程序的詞法分析,它根據規定的詞法規則把源程序拆分成詞法單元,并進行詞法檢查。Compiler類則充當語法分析、語義分析和字節碼生成,而且這三者一步完成,中間不產生任何數據。另外,語法分析和語義分析出現的錯誤由類Parse_error負責處理。前端編譯器序列圖如圖2所示。
Vblet語法分析采用自頂向下的預測分析法,驅動Scanner對象的token()成員函數為其產生一個詞法單元,當需要后退時使用Scanner對象的stoken()方法保存一個不符合當前產生式規則的詞法單元,以便下一個產生式規則的分析。類Compiler只包含一個public權限的成員函數Compile(),當Compile()被調用時,將生成的對應編譯單元的字節碼序列和符號信息、常量數據封裝在ByteCode對象中,并返回給被調用者。
3.2 虛擬機的實現
Vblet虛擬機是一個模擬的運行時環境,是對Vblet腳本邏輯做出響應的地方。根據體系結構的不同,虛擬機分為如下兩種類型:(1)寄存器虛擬機;(2)堆棧虛擬機。寄存器虛擬機具有相對較高的執行效率,但實現機制復雜。而堆棧虛擬機實現起來則相對比較簡單,但是需要付出一定的性能代價。堆棧虛擬機由于Java和Python的成功而被證明它在模擬計算平臺上的優勢[4-5]。因此,本文也將采用堆棧虛擬機作為Vblet腳本引擎的計算平臺。
如圖3所示,除了模擬處理器執行字節碼指令外,Vblet虛擬機還包含了1個堆棧和PC、SP、FP 3個寄存器。堆棧是虛擬機的運行時棧,是函數調用和保存中間變量的地方,是整個虛擬機最核心的數據結構。程序計數器寄存器PC是記錄下一條要執行指令的序號;棧頂寄存器SP是在堆棧變化的過程中保存堆棧的頂部位置;幀指針寄存器FP則相當于真實處理機的基址寄存器BX,保存當前函數工作棧的棧底位置。這些數據結構表示了整個Vblet虛擬機的運行時環境。
Vblet是一種動態語言,所有數據類型的數據值在Vblet虛擬機中只以一種數據結構VALUE存在,VALUE的定義如下:
struct VALUE
{
short int v_type;//數據類型
union V
{
bool v_bool;
int v_integer;
double v_float;
string* v_string;//指向字符串類型數據
ARRAY v_array;
//指向數組數據,ARRAY是封裝VALUE數組的類
CODE* v_code;
//指向字節碼;CODE是封裝字節碼的類
VlExtObject* v_extobj;
//指向用戶注冊類的對象
} v;
…//操作VALUE和各種數據類型間轉換的函數
}
在VALUE結構體中,成員v_type表示了當前VALUE對象保存的數據類型。VALUE不僅封裝了Vblet的所有基本數據類型,也封裝了數組和字節碼指令流等。實際上,虛擬機的堆棧和SP、FP寄存器所保存的就是VALUE對象或是VALUE對象的引用。
Vblet虛擬機在執行字節碼指令的過程中,要經常讀寫堆棧數據。因此,為了提高堆棧讀寫操作的速度,從而提高虛擬機性能,在實現過程中,定義了如下宏來進行堆棧操作:
#define PUSH(v) (*(++sp))=v //將值v壓入棧頂
#define POP (--sp) //彈s出棧頂
#define PUSHN(n) (sp+=(n)) //將n個未定義的值壓棧
#define POPN(n) (sp-=(n))//從棧中彈出n個元素
#define SP(n) (*(sp-(n)))
//取棧頂一個第n個元素的值
#define FP(n) (*(fp-(n)))
//取幀基地址以下第n個元素的值
Vitual虛擬機指令系統共有16條數據傳輸指令、21條運算指令、5條轉移指令和5條支持調試、異常和錯誤處理的指令,而執行指令的機構——處理器則由函數interpret()來模擬。interpret()函數從指令序列中逐條取得操作碼指令,根據操作碼的不同調用各自的處理函數。整個虛擬機的實現封裝在VVM類中。
3.3 集成接口的實現
腳本引擎的集成接口是指將腳本引擎嵌入應用程序中擴展后者功能時,負責兩者之間通信的API。所謂腳本引擎與應用程序的通信,是指腳本引擎以動態庫或靜態庫的形式被加載進應用程序中,應用程序向腳本引擎開放特定的全局函數和類及其屬性、方法,使腳本引擎可以調用這些全局函數、創建這些類的實例,并且通過該實例實現屬性的訪問和方法的調用。
為了實現全局函數和類的注冊,需要一些數據結構來表示注冊對象的信息,以便腳本引擎能夠識別并使用注冊對象。相對來說,描述函數的信息比較簡單,只需要一個函數名和一個函數指針,函數指針的定義如下:
#define VALUE(*extfuncptr)(ARRAY)
可見,extfuncptr是指向以VALUE數組為參數、返回一個VALUE值的函數,extfuncptr函數指針統一了參數類型、個數和返回值類型不同的所有函數聲明。因此,需要把用一個能夠被extfuncptr指向的全局函數將注冊函數“包裝”起來。在包裝函數中,需要將Vblet腳本傳遞過來的VALUE參數轉換為C++數據類型的參數,并調用注冊函數取得C++數據類型的返回值,再將返回值轉換成VALUE值返回給腳本引擎。
一個類的類信息包括類名、大小、初始化函數指針、類注冊的方法和屬性,用結構體VLEXTCLASSINFO表示,如圖4所示。
此外,還需要一些機制使得腳本引擎能夠引用腳本創建的注冊類的實例對象。把這個表示所有注冊類對象的“始祖”稱為VlExtObject。VlExtObject包含一個表示引用計數的成員變量refcnt和兩個純虛函數GetExtClassInfo()和RegisterConstructor()。任何應用程序需要向腳本引擎注冊的自定義類都必須繼承自類VlExtObject,每個注冊類包含一個靜態的VLEXTCLASSINFO實例,函數GetExtClassInfo()返回該VLEXTCLASSINFO實例,函數RegisterConstructor()將向該VLEXTCLASSINFO實例指定類實例的初始化函數——構造器的包裝函數。此外,應用程序注冊類還可以有自己的函數來向VLEXTCLASSINFO實例對象添加自己的方法和屬性。在腳本引擎內部,通過使用指針來操作注冊類的實例。因此,在腳本中當這樣的對象被復制時,實際上復制的是對象的指針,并且該對象的引用計數refcnt加1,而當對象的引用在腳本中離開其作用域時,并不立即銷毀對象,而是將refcnt減1后如果為零才會使用delete將其銷毀。
整個腳本引擎被封裝成單件模式的CVbletEngine類中,該類包含了腳本引擎的啟動、初始化、腳本的運行和停止等操作。CVbletEngine和集成接口的聲明對應用程序可見,而集成接口始終與腳本引擎的虛擬機部分連接,在虛擬機指令集足夠完善的情況下,前端編譯器和集成接口的分離使得前端編譯器對應用程序是透明的,這樣,當需要增加腳本功能的時候,應用程序可以不做修改。
4 與Python的性能比較
為了測試Vblet腳本引擎是否達到輕量級的要求,在Intel 586 PC機上用該腳本引擎解釋執行如下這段Vblet腳本代碼:
function main(void)
i=0
a=121
b=212
while i<100000000
a=a+b
a=a-b
a=a*b
a=a/b
i=i+1
ewhile
end funtion
同時在同一平臺上用Cpython2.6解釋執行對應的Python程序,最后通過對兩者的初始化時間、編譯時間、運行時間、內存使用量和生成字節碼個數進行了比較,結果如表1所示。
從表1可以看出,雖然Vblet的編譯時間高于CPython,但是初始化時間要遠遠優于Cpython。這是因為Python包含了強大的功能模塊,引擎運行前需要加載這些模塊,并初始化復雜的運行時環境和類型環境。另外,由于Vblet的類型機制比Python簡單,虛擬機在數據的存取上比CPython更快,即使生成的字節碼個數略多于CPython,也能達到更優的運行時間。再加上Vblet在內存上的優勢,表明Vblet完全可以作為一個輕型的腳本引擎嵌入在應用程序中。
基于嵌入或擴展腳本的混合語言編程是實現可定制工程應用系統的有效方法。本文通過借鑒VBA和Python的語法特點設計了語法簡單易學的腳本語言Vblet,設計并實現了Vblet的基于堆棧虛擬機的輕量級腳本引擎。測試結果表明,腳本引擎能夠正確運行Vblet代碼,占有較小的內存空間,在進行簡單的規則計算時具有明顯的執行效率。同時,腳本引擎對被嵌入C++應用程序的支持,使得腳本能夠透明地使用應用程序注冊的類和函數,從而達到增強應用系統靈活性、可定制性和擴展性的目的。
參考文獻
[1] JOHN K. Ousterhout scripting: higher-level programming for the 21st century[J]. IEEE Computer Magazine,1998,31(3).
[2] XIE Q, LIU J, CHOU P H. Tapper: a lightweight scripting engine for highly constrained wireless sensor nodes[C].Information Processing in Sensor Networks, 2006. IPSN 2006. The Fifth International Conference on, 2006:342-349.
[3] Alex Varanese.Game Scripting Mastery[M]. Premier Press,2003.
[4] LINDHOLM T, YELLIN F. Java virtual machine specification[M].Boston, MA, USA:Addison-Wesley Longman Publishing Co., Inc,1999.
[5] ALFRED R S, AHO V, JEFFREY D. Ullman. compilers: principles, techniques, and tools[M]. 2rd. Boston: Pearson/Addison Wesley, 2007.