摘 要: 通過(guò)嵌入腳本引擎為應(yīng)用程序提供腳本支持是實(shí)現(xiàn)應(yīng)用程序可定制和可擴(kuò)展的有效方法,但現(xiàn)存的腳本語(yǔ)言難于掌握,引擎龐大使應(yīng)用程序的效率降低。為了解決該問(wèn)題,設(shè)計(jì)了語(yǔ)法簡(jiǎn)單易學(xué)的腳本語(yǔ)言Vblet,實(shí)現(xiàn)了Vblet的輕量級(jí)腳本引擎。該引擎支持腳本無(wú)縫地使用應(yīng)用程序?qū)崿F(xiàn)的類(lèi)和函數(shù),并具有很好的性能。
關(guān)鍵詞: 腳本引擎;腳本語(yǔ)言;可擴(kuò)展;可定制;二次開(kāi)發(fā)
腳本語(yǔ)言憑借強(qiáng)大的描述能力和靈活的語(yǔ)法結(jié)構(gòu),使得為應(yīng)用程序提供腳本支持從而進(jìn)行混合語(yǔ)言開(kāi)發(fā)成為實(shí)現(xiàn)可擴(kuò)展和可定制的有效方案[1]。出于穩(wěn)定性和開(kāi)發(fā)時(shí)間限制的考慮,開(kāi)發(fā)人員傾向于嵌入現(xiàn)有腳本引擎的方法為應(yīng)用程序提供腳本支持,如嵌入Python引擎為應(yīng)用程序提供Python腳本支持,或使用Microsoft提供的ActiveX Scripting技術(shù)為應(yīng)用程序嵌入VBScript引擎或JavaScript引擎提供相應(yīng)的腳本支持。但是這樣方法靈活性較差,應(yīng)用程序必須接受現(xiàn)有腳本引擎的體積和性能要求,這對(duì)運(yùn)行在低硬件條件下的應(yīng)用程序,或者是只要求進(jìn)行簡(jiǎn)單規(guī)則計(jì)算的小型應(yīng)用程序來(lái)說(shuō),這種方法在效率上沒(méi)有優(yōu)勢(shì)[2]。而且有些現(xiàn)有腳本語(yǔ)言比較難學(xué),使得用戶(hù)把太多時(shí)間花在語(yǔ)言的學(xué)習(xí)上。因此,需要一個(gè)輕型的腳本引擎,能夠解釋運(yùn)行一門(mén)語(yǔ)法簡(jiǎn)單易學(xué)的腳本語(yǔ)言,該腳本語(yǔ)言對(duì)于工程應(yīng)用領(lǐng)域的非正式程序員,可以經(jīng)過(guò)短時(shí)間的學(xué)習(xí)培訓(xùn)或者不經(jīng)過(guò)學(xué)習(xí)就能掌握并使用。
針對(duì)以上問(wèn)題,本文在自行設(shè)計(jì)的腳本語(yǔ)言Vblet的基礎(chǔ)上,開(kāi)發(fā)實(shí)現(xiàn)了Vblet的輕型腳本引擎,支持腳本引擎被嵌入在C++實(shí)現(xiàn)的應(yīng)用程序上。Vblet語(yǔ)言語(yǔ)法簡(jiǎn)單,繼承了在非專(zhuān)業(yè)程序員中具有較高聲譽(yù)的VBA語(yǔ)言,并且借鑒了Python語(yǔ)言的部分功能,使得用戶(hù)能夠?qū)W⒂趩?wèn)題的解決而不是語(yǔ)法的學(xué)習(xí)上。
1 腳本引擎概述
腳本引擎[3]是一個(gè)加載、解釋執(zhí)行腳本,并負(fù)責(zé)與外界進(jìn)行交互的程序。腳本引擎一般很少獨(dú)立存在,而是要嵌入應(yīng)用程序中以擴(kuò)展應(yīng)用程序的行為,這個(gè)被嵌入腳本引擎的應(yīng)用程序稱(chēng)為宿主程序。
嵌入的腳本引擎如圖1所示。圖1中,腳本引擎通過(guò)某種交互接口,根據(jù)腳本源程序描述的邏輯來(lái)控制應(yīng)用系統(tǒng)。根據(jù)宿主程序和腳本引擎之間的緊密層次不同,可將通信方式分為:(1)基于二進(jìn)制接口的通信。(2)基于公共運(yùn)行時(shí)環(huán)境的通信。(3)基于源碼接口的通信。本文實(shí)現(xiàn)的基于源碼的交互接口,即通信雙方基于共同的實(shí)現(xiàn)語(yǔ)言,在源代碼級(jí)上相互調(diào)用。
除了交互接口,作為腳本的解釋運(yùn)行平臺(tái),腳本引擎包含了一個(gè)編譯器前端程序,前端程序負(fù)責(zé)將腳本源代碼經(jīng)過(guò)詞法分析、語(yǔ)法語(yǔ)義分析后生成字節(jié)碼格式的指令序列,然而這些指令序列是不能在目標(biāo)機(jī)器上執(zhí)行的。因此,在腳本引擎的最底層還要一個(gè)執(zhí)行字節(jié)碼指令的程序,這個(gè)程序即稱(chēng)為虛擬機(jī)。
2 腳本語(yǔ)言的設(shè)計(jì)
在開(kāi)發(fā)實(shí)現(xiàn)腳本引擎之前,先要確定引擎要解釋執(zhí)行的對(duì)象,即腳本語(yǔ)言。本文設(shè)計(jì)的腳本語(yǔ)言Vblet是VBA(Visual Basic for Applications)的子集,而VBA的語(yǔ)法簡(jiǎn)單易學(xué),在非專(zhuān)業(yè)程序員中有很大的用戶(hù)量,享有很高的聲譽(yù)。Vblet簡(jiǎn)化了VBA語(yǔ)法,去掉了VBA語(yǔ)法中的一些限制,此外還根據(jù)需要擴(kuò)展了部分功能。下面是Vblet不同于VBA的一些重要的語(yǔ)法特性:
(1)交互執(zhí)行。這是借鑒了Python語(yǔ)言交互執(zhí)行的語(yǔ)法特點(diǎn),使得程序員可以單行執(zhí)行語(yǔ)句或計(jì)算表達(dá)式,而不限于一定要把代碼封裝在代碼塊中。
(2)不需要變量和參數(shù)聲明。VBlet是動(dòng)態(tài)語(yǔ)言,變量的類(lèi)型由腳本引擎從上下文中確定,變量可以不經(jīng)過(guò)聲明就可以使用。
(3)去掉了部分運(yùn)算符,比如冒號(hào)運(yùn)算符和逗號(hào)運(yùn)算符。
為了提高性能,Vblet去除了VBA中一些庫(kù)函數(shù)的支持,只保留一些在工程應(yīng)用領(lǐng)域比較常用的數(shù)學(xué)計(jì)算函數(shù)。
3 腳本引擎實(shí)現(xiàn)方案
Vblet引擎除了IDE的開(kāi)發(fā)使用了MFC類(lèi)庫(kù)之外,其他模塊的實(shí)現(xiàn)都是使用標(biāo)準(zhǔn)C++編寫(xiě)的,這使得Vblet引擎只需要重新編寫(xiě)IDE,或者修改小部分的核心代碼就能夠移植到其他平臺(tái)上。
3.1 前端編譯程序的實(shí)現(xiàn)
前端編譯程序?qū)⒛_本源程序的字符流經(jīng)過(guò)詞法分析、語(yǔ)法分析和語(yǔ)義分析后,生成字節(jié)碼表示的指令流,同時(shí)進(jìn)行語(yǔ)法檢查,對(duì)語(yǔ)法錯(cuò)誤給出提示信息。另外,為了支持?jǐn)帱c(diǎn)調(diào)試和異常信息顯示,每行源程序和生成的字節(jié)碼指令的對(duì)應(yīng)關(guān)系也要在這里建立。
前端編譯器一般可以通過(guò)一些自動(dòng)生成工具生成,但是這些自動(dòng)生成的代碼效率都不夠高或者不好閱讀,因此本文采用手寫(xiě)的方式實(shí)現(xiàn)前端編譯程序。前端程序由Scanner類(lèi)和Compiler類(lèi)兩大主要模塊組成。Scanner類(lèi)主要負(fù)責(zé)源程序的詞法分析,它根據(jù)規(guī)定的詞法規(guī)則把源程序拆分成詞法單元,并進(jìn)行詞法檢查。Compiler類(lèi)則充當(dāng)語(yǔ)法分析、語(yǔ)義分析和字節(jié)碼生成,而且這三者一步完成,中間不產(chǎn)生任何數(shù)據(jù)。另外,語(yǔ)法分析和語(yǔ)義分析出現(xiàn)的錯(cuò)誤由類(lèi)Parse_error負(fù)責(zé)處理。前端編譯器序列圖如圖2所示。
Vblet語(yǔ)法分析采用自頂向下的預(yù)測(cè)分析法,驅(qū)動(dòng)Scanner對(duì)象的token()成員函數(shù)為其產(chǎn)生一個(gè)詞法單元,當(dāng)需要后退時(shí)使用Scanner對(duì)象的stoken()方法保存一個(gè)不符合當(dāng)前產(chǎn)生式規(guī)則的詞法單元,以便下一個(gè)產(chǎn)生式規(guī)則的分析。類(lèi)Compiler只包含一個(gè)public權(quán)限的成員函數(shù)Compile(),當(dāng)Compile()被調(diào)用時(shí),將生成的對(duì)應(yīng)編譯單元的字節(jié)碼序列和符號(hào)信息、常量數(shù)據(jù)封裝在ByteCode對(duì)象中,并返回給被調(diào)用者。
3.2 虛擬機(jī)的實(shí)現(xiàn)
Vblet虛擬機(jī)是一個(gè)模擬的運(yùn)行時(shí)環(huán)境,是對(duì)Vblet腳本邏輯做出響應(yīng)的地方。根據(jù)體系結(jié)構(gòu)的不同,虛擬機(jī)分為如下兩種類(lèi)型:(1)寄存器虛擬機(jī);(2)堆棧虛擬機(jī)。寄存器虛擬機(jī)具有相對(duì)較高的執(zhí)行效率,但實(shí)現(xiàn)機(jī)制復(fù)雜。而堆棧虛擬機(jī)實(shí)現(xiàn)起來(lái)則相對(duì)比較簡(jiǎn)單,但是需要付出一定的性能代價(jià)。堆棧虛擬機(jī)由于Java和Python的成功而被證明它在模擬計(jì)算平臺(tái)上的優(yōu)勢(shì)[4-5]。因此,本文也將采用堆棧虛擬機(jī)作為Vblet腳本引擎的計(jì)算平臺(tái)。
如圖3所示,除了模擬處理器執(zhí)行字節(jié)碼指令外,Vblet虛擬機(jī)還包含了1個(gè)堆棧和PC、SP、FP 3個(gè)寄存器。堆棧是虛擬機(jī)的運(yùn)行時(shí)棧,是函數(shù)調(diào)用和保存中間變量的地方,是整個(gè)虛擬機(jī)最核心的數(shù)據(jù)結(jié)構(gòu)。程序計(jì)數(shù)器寄存器PC是記錄下一條要執(zhí)行指令的序號(hào);棧頂寄存器SP是在堆棧變化的過(guò)程中保存堆棧的頂部位置;幀指針寄存器FP則相當(dāng)于真實(shí)處理機(jī)的基址寄存器BX,保存當(dāng)前函數(shù)工作棧的棧底位置。這些數(shù)據(jù)結(jié)構(gòu)表示了整個(gè)Vblet虛擬機(jī)的運(yùn)行時(shí)環(huán)境。
Vblet是一種動(dòng)態(tài)語(yǔ)言,所有數(shù)據(jù)類(lèi)型的數(shù)據(jù)值在Vblet虛擬機(jī)中只以一種數(shù)據(jù)結(jié)構(gòu)VALUE存在,VALUE的定義如下:
struct VALUE
{
short int v_type;//數(shù)據(jù)類(lèi)型
union V
{
bool v_bool;
int v_integer;
double v_float;
string* v_string;//指向字符串類(lèi)型數(shù)據(jù)
ARRAY v_array;
//指向數(shù)組數(shù)據(jù),ARRAY是封裝VALUE數(shù)組的類(lèi)
CODE* v_code;
//指向字節(jié)碼;CODE是封裝字節(jié)碼的類(lèi)
VlExtObject* v_extobj;
//指向用戶(hù)注冊(cè)類(lèi)的對(duì)象
} v;
…//操作VALUE和各種數(shù)據(jù)類(lèi)型間轉(zhuǎn)換的函數(shù)
}
在VALUE結(jié)構(gòu)體中,成員v_type表示了當(dāng)前VALUE對(duì)象保存的數(shù)據(jù)類(lèi)型。VALUE不僅封裝了Vblet的所有基本數(shù)據(jù)類(lèi)型,也封裝了數(shù)組和字節(jié)碼指令流等。實(shí)際上,虛擬機(jī)的堆棧和SP、FP寄存器所保存的就是VALUE對(duì)象或是VALUE對(duì)象的引用。
Vblet虛擬機(jī)在執(zhí)行字節(jié)碼指令的過(guò)程中,要經(jīng)常讀寫(xiě)堆棧數(shù)據(jù)。因此,為了提高堆棧讀寫(xiě)操作的速度,從而提高虛擬機(jī)性能,在實(shí)現(xiàn)過(guò)程中,定義了如下宏來(lái)進(jìn)行堆棧操作:
#define PUSH(v) (*(++sp))=v //將值v壓入棧頂
#define POP (--sp) //彈s出棧頂
#define PUSHN(n) (sp+=(n)) //將n個(gè)未定義的值壓棧
#define POPN(n) (sp-=(n))//從棧中彈出n個(gè)元素
#define SP(n) (*(sp-(n)))
//取棧頂一個(gè)第n個(gè)元素的值
#define FP(n) (*(fp-(n)))
//取幀基地址以下第n個(gè)元素的值
Vitual虛擬機(jī)指令系統(tǒng)共有16條數(shù)據(jù)傳輸指令、21條運(yùn)算指令、5條轉(zhuǎn)移指令和5條支持調(diào)試、異常和錯(cuò)誤處理的指令,而執(zhí)行指令的機(jī)構(gòu)——處理器則由函數(shù)interpret()來(lái)模擬。interpret()函數(shù)從指令序列中逐條取得操作碼指令,根據(jù)操作碼的不同調(diào)用各自的處理函數(shù)。整個(gè)虛擬機(jī)的實(shí)現(xiàn)封裝在VVM類(lèi)中。
3.3 集成接口的實(shí)現(xiàn)
腳本引擎的集成接口是指將腳本引擎嵌入應(yīng)用程序中擴(kuò)展后者功能時(shí),負(fù)責(zé)兩者之間通信的API。所謂腳本引擎與應(yīng)用程序的通信,是指腳本引擎以動(dòng)態(tài)庫(kù)或靜態(tài)庫(kù)的形式被加載進(jìn)應(yīng)用程序中,應(yīng)用程序向腳本引擎開(kāi)放特定的全局函數(shù)和類(lèi)及其屬性、方法,使腳本引擎可以調(diào)用這些全局函數(shù)、創(chuàng)建這些類(lèi)的實(shí)例,并且通過(guò)該實(shí)例實(shí)現(xiàn)屬性的訪(fǎng)問(wèn)和方法的調(diào)用。
為了實(shí)現(xiàn)全局函數(shù)和類(lèi)的注冊(cè),需要一些數(shù)據(jù)結(jié)構(gòu)來(lái)表示注冊(cè)對(duì)象的信息,以便腳本引擎能夠識(shí)別并使用注冊(cè)對(duì)象。相對(duì)來(lái)說(shuō),描述函數(shù)的信息比較簡(jiǎn)單,只需要一個(gè)函數(shù)名和一個(gè)函數(shù)指針,函數(shù)指針的定義如下:
#define VALUE(*extfuncptr)(ARRAY)
可見(jiàn),extfuncptr是指向以VALUE數(shù)組為參數(shù)、返回一個(gè)VALUE值的函數(shù),extfuncptr函數(shù)指針統(tǒng)一了參數(shù)類(lèi)型、個(gè)數(shù)和返回值類(lèi)型不同的所有函數(shù)聲明。因此,需要把用一個(gè)能夠被extfuncptr指向的全局函數(shù)將注冊(cè)函數(shù)“包裝”起來(lái)。在包裝函數(shù)中,需要將Vblet腳本傳遞過(guò)來(lái)的VALUE參數(shù)轉(zhuǎn)換為C++數(shù)據(jù)類(lèi)型的參數(shù),并調(diào)用注冊(cè)函數(shù)取得C++數(shù)據(jù)類(lèi)型的返回值,再將返回值轉(zhuǎn)換成VALUE值返回給腳本引擎。
一個(gè)類(lèi)的類(lèi)信息包括類(lèi)名、大小、初始化函數(shù)指針、類(lèi)注冊(cè)的方法和屬性,用結(jié)構(gòu)體VLEXTCLASSINFO表示,如圖4所示。
此外,還需要一些機(jī)制使得腳本引擎能夠引用腳本創(chuàng)建的注冊(cè)類(lèi)的實(shí)例對(duì)象。把這個(gè)表示所有注冊(cè)類(lèi)對(duì)象的“始祖”稱(chēng)為VlExtObject。VlExtObject包含一個(gè)表示引用計(jì)數(shù)的成員變量refcnt和兩個(gè)純虛函數(shù)GetExtClassInfo()和RegisterConstructor()。任何應(yīng)用程序需要向腳本引擎注冊(cè)的自定義類(lèi)都必須繼承自類(lèi)VlExtObject,每個(gè)注冊(cè)類(lèi)包含一個(gè)靜態(tài)的VLEXTCLASSINFO實(shí)例,函數(shù)GetExtClassInfo()返回該VLEXTCLASSINFO實(shí)例,函數(shù)RegisterConstructor()將向該VLEXTCLASSINFO實(shí)例指定類(lèi)實(shí)例的初始化函數(shù)——構(gòu)造器的包裝函數(shù)。此外,應(yīng)用程序注冊(cè)類(lèi)還可以有自己的函數(shù)來(lái)向VLEXTCLASSINFO實(shí)例對(duì)象添加自己的方法和屬性。在腳本引擎內(nèi)部,通過(guò)使用指針來(lái)操作注冊(cè)類(lèi)的實(shí)例。因此,在腳本中當(dāng)這樣的對(duì)象被復(fù)制時(shí),實(shí)際上復(fù)制的是對(duì)象的指針,并且該對(duì)象的引用計(jì)數(shù)refcnt加1,而當(dāng)對(duì)象的引用在腳本中離開(kāi)其作用域時(shí),并不立即銷(xiāo)毀對(duì)象,而是將refcnt減1后如果為零才會(huì)使用delete將其銷(xiāo)毀。
整個(gè)腳本引擎被封裝成單件模式的CVbletEngine類(lèi)中,該類(lèi)包含了腳本引擎的啟動(dòng)、初始化、腳本的運(yùn)行和停止等操作。CVbletEngine和集成接口的聲明對(duì)應(yīng)用程序可見(jiàn),而集成接口始終與腳本引擎的虛擬機(jī)部分連接,在虛擬機(jī)指令集足夠完善的情況下,前端編譯器和集成接口的分離使得前端編譯器對(duì)應(yīng)用程序是透明的,這樣,當(dāng)需要增加腳本功能的時(shí)候,應(yīng)用程序可以不做修改。
4 與Python的性能比較
為了測(cè)試Vblet腳本引擎是否達(dá)到輕量級(jí)的要求,在Intel 586 PC機(jī)上用該腳本引擎解釋執(zhí)行如下這段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
同時(shí)在同一平臺(tái)上用Cpython2.6解釋執(zhí)行對(duì)應(yīng)的Python程序,最后通過(guò)對(duì)兩者的初始化時(shí)間、編譯時(shí)間、運(yùn)行時(shí)間、內(nèi)存使用量和生成字節(jié)碼個(gè)數(shù)進(jìn)行了比較,結(jié)果如表1所示。
從表1可以看出,雖然Vblet的編譯時(shí)間高于CPython,但是初始化時(shí)間要遠(yuǎn)遠(yuǎn)優(yōu)于Cpython。這是因?yàn)镻ython包含了強(qiáng)大的功能模塊,引擎運(yùn)行前需要加載這些模塊,并初始化復(fù)雜的運(yùn)行時(shí)環(huán)境和類(lèi)型環(huán)境。另外,由于Vblet的類(lèi)型機(jī)制比Python簡(jiǎn)單,虛擬機(jī)在數(shù)據(jù)的存取上比CPython更快,即使生成的字節(jié)碼個(gè)數(shù)略多于CPython,也能達(dá)到更優(yōu)的運(yùn)行時(shí)間。再加上Vblet在內(nèi)存上的優(yōu)勢(shì),表明Vblet完全可以作為一個(gè)輕型的腳本引擎嵌入在應(yīng)用程序中。
基于嵌入或擴(kuò)展腳本的混合語(yǔ)言編程是實(shí)現(xiàn)可定制工程應(yīng)用系統(tǒng)的有效方法。本文通過(guò)借鑒VBA和Python的語(yǔ)法特點(diǎn)設(shè)計(jì)了語(yǔ)法簡(jiǎn)單易學(xué)的腳本語(yǔ)言Vblet,設(shè)計(jì)并實(shí)現(xiàn)了Vblet的基于堆棧虛擬機(jī)的輕量級(jí)腳本引擎。測(cè)試結(jié)果表明,腳本引擎能夠正確運(yùn)行Vblet代碼,占有較小的內(nèi)存空間,在進(jìn)行簡(jiǎn)單的規(guī)則計(jì)算時(shí)具有明顯的執(zhí)行效率。同時(shí),腳本引擎對(duì)被嵌入C++應(yīng)用程序的支持,使得腳本能夠透明地使用應(yīng)用程序注冊(cè)的類(lèi)和函數(shù),從而達(dá)到增強(qiáng)應(yīng)用系統(tǒng)靈活性、可定制性和擴(kuò)展性的目的。
參考文獻(xiàn)
[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.