介紹
如果用戶能夠通過一些腳本語言來修改應用本身的行為,那么許多應用可以變得更適合用戶使用。一些商業應用就提供了此類便利。例如 Microsoft Office 的 VBA 腳本編程或在視頻游戲 World of Warcraft 中使用 Lua 。腳本語言把應用作為一個平臺提供一系列終端用戶可以獲得并操控的服務。
做為嵌入到程序中的語言,我們有很多可用的選擇:開源和不開源的腳本引擎,或者可以從頭開始創建一個。現在,最為熟知的腳本語言是JavaScript,Lua和Python,還有很多其它的語言等等。在Microsoft Windows平臺上 嵌入腳本引擎的一般方法是使用一個 包含腳本引擎動態鏈接庫(DLL)中,然后使用一系列的函數調用訪問引擎中的服務。
在這篇文章里面我們將看到如何使用lua5.2腳本引擎嵌入到C++的代碼中,我們的測試用例使用的開發工具是Visual Studio 2005.這開始講解之前我們要從lua官方網站下載一些必要的組件,lua5.2中包含有已經編譯好的dll以及頭文件和鏈接用的導入庫。需要說明的是當你使用lua嵌入到C++代碼的時候,編譯出來的可執行文件必須包含有lua的DLL即動態鏈接庫,否則會提示運行出錯,缺少必要的dll。這些例子都是用Visual Studio 2005編寫,當然對于VS的后續版本是兼容的。
這篇文章向您描述了一個使用lua5.2作為腳本引擎的特定實現,在lua的官方文檔(lua.org)以及一些網站中有許多關于這方面的信息,這里僅僅是為你提供一些測試例子,作為你開始lua旅程之前的引導
對于產品開發而言,開發一個完整專業應用程序跟開發單塊應用程序有不同的策略和方法。譬如說:開發完整專業應用平臺需要對終端用戶提供大量豐富的插件工具和服務去實現他們自己特定的領域需求。插件程序的策略和方法被應用在許多的軟件中,例如微軟的Office,Visual Studio以及集成開發環境Eclipse,甚至是圖片處理軟件Adobe Photoshop. 網游魔獸世界就提供大量的add-on(插件)給用戶,一些其他的游戲提供類似的工具,讓玩家可以增加額外的內容從而創造一個與眾不同的社區。
作為商業應用程序,GenPOS,有一些特性也是極具工具集的策略的:
不管怎樣,有些模塊是被源代碼實際控制的,這部分模塊的更改需要通過開發部門的協作才能完成軟件行為的改變。例如說,打印收據(一些顯示內容是從參數或者助記符的輸入中獲取的)。簡短列舉這些限制可能包含:
與其試圖加強和改進當前的非常簡單的控制字符串功能,以提供包含額外腳本功能的銷售終端,我們最終決定尋找到其它可能的方案,不需要大量開發到改進,且提供更多的產品改進能力。就像我們引入的布局管理器使客戶能夠設計自己的屏幕布置和工作流程一樣,我們希望有一個相當靈活的機制讓經銷商的銷售終端通過腳本為他們的客戶提供增值服務。我們還打算提供足夠的應用服務訪問權限,讓經銷商和最終客戶將能夠修改自己的應用程序的行為。最后,我們要使用有一定程度到用戶社區的語言,感興趣到人們可以使用社區的資源。
我們已經用5.2版本的Lua腳本引擎在POS源代碼上做了一些簡單的實驗來觀察向程序中添加功能的難度。從實驗結果來看,我們想通過程序服務來展現的各種功能已經是可用的了,并且可以通過對Lua腳本引擎做一些修改就可以使用起來。
使用 Lua 5.2 腳本引擎
Lua 腳本引擎本身是由 C 語言寫成的,在 C 或 C++ 中使用 Lua 腳本也相當簡單。你在網上也可以找到很多集成了 Lua 5.2 腳本引擎的的程序或程序片段,并且 Lua 5.2 的程序接口和先前 Lua 版本只有在初始化和啟動等接口上存在少量變化,所以舊的 Lua 程序可以不做修改或只做很小的修改就可以移植到 Lua 5.2環境下。
基本的初始化步驟如下:
一旦初始化了 Lua 腳本引擎,你可以通過如下步驟執行一段 Lua 腳本:
如果想在應用程序中加載Lua腳本并執行其中的函數,你必須執行被加載的Lua程序塊(chunk)。剛剛加載的程序塊只是編譯后存放于Lua的腳本引擎中,并沒有被執行。只有在程序塊被執行后,Lua中的全局變量和函數才會被創建,在這之前這些任何全局變量和函數對于應用程序來說都不可用。作為Lua引擎的環境由應用程序提供給Lua腳本引擎的任何全局變量和函數也不可用。應用程序必須首先創建變量和函數,并使用函數lua_setglobal()讓它們可用。在文件 UtilityFunctions.cpp 中的函數int LuaSimpleWrapper::TriggerGlobalCall()定義了一個例子,它在Lua虛擬棧上動態創建一個Lua函數調用,并用Lua腳本引擎中的lua_pcall()函數,以給定的參數來執行此函數。
所有Lua腳本引擎函數或服務,都用到一個包含Lua腳本引擎狀態信息的句柄或數據結構指針。這個句柄被指定為lua_State*或者指向lua_State變量的指針。每次成功的lua_newstate()的調用都會返回一個lua_State指針,失敗時返回NULL。這個數據結構是用來指示特定會話的變量,它允許Lua腳本引擎同時有多個會話并行。當應用程序用到Lua腳本引擎中的函數,或應用程序提供給Lua腳本引擎的服務被調用時,lua_State結構提供的會話環境唯一指示了某個Lua會話狀態。這意味著,應用程序提供給Lua腳本引擎的函數應該是完全可重入的,或者提供某種監視,比如信號量和臨界區使得非共享服務能夠提供線程安全的訪問。
使用代碼
在Visual Studio 2005工程目錄中包含三個C++文件和一個測試基本功能的Lua例子。主體部分在Parser01.cpp中。在這里,加載了特定的Lua文件,然后又調用了一些別的函數。UtilityFunctions.cpp包含了LuaSimpleWrapper方法的源代碼。InitEnviron則包含了我們希望提供給Lua環境的的函數。
我們正考慮使用的這個方法是, 在銷售點應用程序啟動一個包含Lua腳本的文件被指定。作為啟動,銷售點將啟動一個初始化Lua腳本引擎并且加載和執行指定的Lua源文件的線程。Lua塊將一直保留在內存中,并且在銷售點應用程序中,隨著事件的進行一些事件會被轉移到Lua腳本進行處理。這個示例程序中的測試工具是一個對我們正在考慮的這種方法的探索。
在下面提供的lua源碼的函數中,我們使用了幾個在文件InitEnviron.cpp中提供的幾個函數,處理非標準Lua字符串的‘寬字符串'。標準的Lua字符串是char字符串(C風格的單字節字符串)。這些附加函數為Lua處理這些寬字符串提供了方法,例如字符串的連接,比較。
下面展示一個含有三個參數并執行一系列操作的Lua函數。函數名xxfunc是一個全局名字,應用程序可以通過調用函數lua_getglobal()從Lua全局字典中檢索出來并在Lua虛擬堆棧上把句柄傳給lua_getglobal。然后函數的參數可以通過調用像一個lua_pushstring()的函數推送到Lua虛擬堆棧上,接著調用Lua腳本引擎函數lua_pcall()執行xxfunc函數。
以上在Lua腳本中已經被加載的Lua函數可以被應用程序調用。使用來自LuaSimpleWrapper類中的一個助手函數,我們可以調用帶有以下C++代碼行的Lua函數。方法TriggerGlobalCall()需要一個標識全局Lua函數(函數或賦給一個變量或表實例的函數)的描述性字符串來調用帶有描述的參數類型。這些參數遵循描述性字符串。這種類型的變量函數調用會成為一個運行時錯誤的根源,因為它包含幾個單獨的必須匹配的源:
1. TriggerGlobalCall()中的描述性字符串;
2. theTriggerGlobalCall()提供的實際參數;
3. 被調用的Lua函數。
富有表達力的字符串使用了由逗號分隔的列表來區分參數的類型,列表中每個字母代表著各自的類型,如一個ANSI字符串,一個長字符串,一個浮點型,一個整型,一個函數的地址。在 TriggerGlobalCall() 方法的代碼中,字母中間的逗號被忽略了,實際上他們存在的作用只是為了使這個富有表達力的字符串更易識別和理解。
上面的方法TriggerGlobalCall()調用了Lua的xxfunc()函數會產生如下的結果輸出。這輸出由Lua函數使用TriggerGlobalCall()里定義的參數列表的參數生成。
我們在LuaSimpleWrapper 類中提供的TriggerGlobalCall()方法允許指定一個可訪問一個函數的表值,這個函數已經在Lua表中分配了一個鍵值。描述性字符串的格式是:tablename.key,這里“tablename”是一個Lua表的全局名而“key”是訪問函數所需的鍵值。
將C/C++函數導出到Lua引擎中
為了創建一個要在Lua腳本中使用的C或者C++輔助函數,C/C++應用程序就必需提供該函數體(the function body)并使用適當的Lua引擎函數讓該新函數變為在Lua引擎中可用。在應用程序里將一個函數提供給在Lua引擎中進行使用所需的函數調用要將若干值壓入Lua的虛擬堆棧之中,然后調用lua_setglobal()函數,就可以把應用程序中的函數作為全局函數提供給在Lua腳本引擎中使用。
Lua為函數提供閉包概念。當一個Lua腳本引擎調用應用函數時,一個閉包允許一個應用指定一個或多個提供給應用函數的值。這些值可以被應用函數更新,這個例子使用的一個特性是通過一個與應用函數關聯的計數器增量提供一個惟一值。C++關于這方面的源代碼的一個例子可以在方法int LuaSimpleWrapper::InitLuaEnvironment()中找到,這個方法為Lua腳本引擎提供CreateFrame()函數。
// create the C closure with the above two arguments,
lua_pushcclosure (m_luaState, ParserLuaCreateGlobalFrame, 2);
lua_setglobal (m_luaState, "CreateFrame");
要導出的C++函數的源代碼應該具有如下所示的形式。函數concatMultiWideStrings ()使用了一系列的Lua引擎里的函數處理LUa虛擬堆棧之中的一些數值并將處理結果返回給Lua引擎。以下所示函數說明了要使用lua_State *這個參數提供給該函數相關的session環境。這個函數用以將多個字符串拼接到一起。Lua腳本引擎提供了位于Lua虛擬堆棧之中的參數的個數信息。我們還可以使用Lua引擎提供的lua_type()函數判斷出參數的數據類型,從而可以跳過那些不是正確類型的參數。
然而,實際上在我們只想使用LUA_STRING這種類型的情況下,Lua腳本引擎會執行相應的類型轉換,但Lua所做的從其它類型到字符串類型的轉換的結果是C風格的單字節字符所組成的字符串串而不是我們所預期的雙字節寬度的字符組成的字符串。
關注點
該測試工具(test harness)的第一個版本只是個非常簡單的開頭,只是簡單的裝載一個簡單Lua腳本,在運行后也只是使用Lua的輸出函數在控制臺(console)中輸出一個“Hello World”。隨著在調查嵌入式Lua所具有的潛在能力時,腳本和測試工具會變得越來越復雜,很快就會發現明顯需要有某種方式將Lua虛擬堆棧中所有內容打印出來,才能理解Lua引擎同應用程序之間到底是如何進行通信的。 要多次在運行突然中斷,使用調試器單步進入C++源代碼時使用堆棧內容輸出函數查看堆棧中內容,只有這樣才能理解到底發生了什么問題并找出問題的修復辦法。
Lua語言的動態特性,像JavaScript這類的松散類型語言一樣,會鼓勵有冒險精神的程序員寫出一些非常有趣的代碼,但是也會產生一些非常難以調試和測試的代碼。這個難度主要來自于使用了兩種語言,而在Visual Studio中又缺乏對Lua調試的支持。
在實現方法int LuaSimpleWrapper::TriggerGlobalCall ()時,在Visual Studio debugger里運行的情況下我們遇到了導致Lua的腳步引擎(script engine)在Windows錯誤對話框中要執行應用程序關閉或退出操作的測試案例(test case)。我們認為,之所以出現這個問題,是由于在出現了錯誤的情況下,有些特定的值被壓入了Lua的虛擬堆棧之中,從而未被恰當處理所導致的。為了解決該問題,在發現錯誤的情況下,我們要用下面所示的C++代碼對Lua的虛擬堆棧進行清理。在此測試套件(test harness)中,我們有兩個不同的測試,一個用來對指定的全局變量是否存在進行測試,另一個測試的是,如果通過使用“global.key”語法指定了一個關鍵值(key value),那么其中的全局變量必定為一個表,否則便為錯誤。
新聞熱點
疑難解答