1. 程式人生 > >雲風的 BLOG: Lua 虛擬機器的封裝

雲風的 BLOG: Lua 虛擬機器的封裝

我們面臨的需求是:建立一個(或幾個)Lua 虛擬機器,定期驅動它執行。重點在,定期驅動它執行。這可視為向虛擬機發送訊息,虛擬機器本質上是訊息驅動的(其實 Windows 程式也是)。通常,有一個時鐘驅動的行為,或者有一個渲染幀驅動的行為;然後,有外部輸入訊息,例如觸控式螢幕訊息等。

由於整個業務邏輯都是在 Lua 中完成,所以我們並不需要從 Lua VM 中獲取什麼東西。如果作業系統需要些什麼,更多的是傳入一個外部庫,由 Lua 程式碼把內部的資訊通過庫傳遞出去,而不是外部去獲取。裁剪掉後者這個需求,Lua 的 C API 中絕大多數 API 都是不必要的。

我最後設計出來的封裝介面只有 5 個 C API :

struct luavm * luavm_new();
const char * luavm_init(struct luavm *L, const char * source, const char *format, ...);
void luavm_close(struct luavm * L);
const char * luavm_register(struct luavm * L, const char * source, const char *chunkname, int *handle);
const char * luavm_call(struct luavm *L, int handle, const char *format, ...);

new init close 是 VM 的構建銷燬 API ,我們可以在 init 時傳入初始化指令碼,從外部注入 Lua 的擴充套件模組。注意:通過 Lua 的原生 require 機制,在部分平臺(如 ios)上,是無法取得外部的 C 模組的。

我的解決方案是寫一個符合 lua package searcher 的函式,在初始化的時候替換掉原生的 C 模組 searcher :

static int
preload_searcher(lua_State *L) {
    const char * modname = luaL_checkstring(L,1);
    int i;
    for (i=0;preload[i].name != NULL;i++) {
        if (strcmp(modname, preload[i].name) == 0) {
            lua_pushcfunction(L, preload[i].func);
            return 1;
        }
    }
    lua_pushfstring(L, "\n\tno preload C module '%s'", modname);
    return 1;
}

然後我們就可以把所需的 C 模組的 luaopen_xxx 靜態連結到程式中,放在 preload 數組裡就可以讓 Lua VM 順利 require 到。

這裡,只提供了一對 register/call API 讓外部可以呼叫一個 VM 中的方法。

我們可以在 register 時注入一段簡單的指令碼,返回一個 Lua 函式。框架給它繫結一個數字 id ,之後 call 就可以呼叫這個 handle 對應的函數了。在引擎中,需要暴露到從原生程式碼直接呼叫的函式並不會太多,可能只有 update draw message 等寥寥幾個。

所有 Lua 呼叫都有可能丟擲 error ,在介面上,我全部用 const char * 來示意是否有 error ,方便原生程式碼在使用時處理。

但這只是一個後備方案。我更傾向於提供第二種途徑,讓 Lua VM 內部也可以拿到這些錯誤資訊,這樣可以做的更完備。例如,可以把錯誤 log 通過網路途徑傳送走,或做更復雜的過濾等。我的方案是,在 init 的時候,框架構建出一個 table ,傳給 init 程式碼。這程式碼可以把這個 table 記錄下來,例如是放在全域性變數中。然後每次 VM 的入口函式在執行前,都可以檢查這個 table 中有沒有新的內容,那就是最後發生的錯誤資訊,處理它再將 table 清空。框架則在每次有新的錯誤資訊後,追加在這個 table 末尾。

讓 C 程式碼向原生程式碼傳遞引數,我才用了 C 傳統的 format ... 的可變引數形式。format 串中,n 表示 double ,i 表示 integer ,b 表示 boolean ,s 表示 const char * , p 表示 void * ,f 表示 lua_CFunction

另外,我還支援了接收 call 方法從 Lua 中返回一些簡單資料型別,用對應的大寫字母即可。call 的時候傳遞指標。比如,向接收一個 int 返回值,就傳一個 int * ,這和 scanf 的風格一致。

最後值得一提的時,雖然這個小的 lua vm 封裝模組只完成了很微小的工作,但要把事情做對,還是需要謹慎實現的 ,需要考慮呼叫 lua c api 時可能發生的任何錯誤。比如很容易被忽略掉的記憶體不足的 error 。( lua_pushstring 就是有可能丟擲異常的,不能裸調)

從 lua 中返回 const char * 也要警惕,避免把可能 gc 的字串指標返回。我的解決方案是,在虛擬機器啟動後,建立一個獨立的 coroutine 專門作為和外界交換資料的區域。所有可能返回到外界的字串,但暫時放在這裡。所以直到下次在呼叫 lua 虛擬機器前,都可以認為上次返回的字串是安全的。

最後,放上我的程式碼做參考。注意,這段程式碼是從我們開發中的引擎中抽出來的片段,僅作參考,該程式碼片段不會(因為 bug )持續維護。