1. Embedded Lua to C/C++
最後小弟選擇了 Lua,因為他本來就是被設計成嵌入其他應用程式,而且 100% ANSI C 無雜質不需要安裝其他有得沒得,我們的開發工具就定調為 C/C++ mix Lua。順帶一提,在 RTOS 界知名度極高的 WindRiver 最近新推出的 IoT 平台也支援了 Lua(WindRiver 現為 Intel 子公司)。
C/C++ mix Lua 需要面對 3 項挑戰:
- Lua call C/C++ function
- Lua call C++ object
- C/C++ call Lua
Lua call C/C++ function 不是問題,因為 Lua 本來就提供 API 讓 C/C++ function 可以被註冊到 Lua state(Lua state 可以視為 Java 的 Virtual Machine)內。
Lua call C++ object 的問題是 C++ object 肯定與 Lua object 不相容,而且 Lua 也沒有提供 API 用來註冊 C++ object。
C/C++ call Lua 的挑戰是我們希望當收到資料時,對 Lua script function 進行 callback。
接下來就來解釋 1-3 要如何實做。
註:本篇文章非 Lua 教學,若您對 Lua 很陌生建議先通讀參考資料 [1]。
- Lua call C/C++ function
- Lua call C++ object
- C/C++ call Lua
Lua call C/C++ function 不是問題,因為 Lua 本來就提供 API 讓 C/C++ function 可以被註冊到 Lua state(Lua state 可以視為 Java 的 Virtual Machine)內。
Lua call C++ object 的問題是 C++ object 肯定與 Lua object 不相容,而且 Lua 也沒有提供 API 用來註冊 C++ object。
C/C++ call Lua 的挑戰是我們希望當收到資料時,對 Lua script function 進行 callback。
註:本篇文章非 Lua 教學,若您對 Lua 很陌生建議先通讀參考資料 [1]。
2. Lua call C/C++ function
這個基本上沒什麼技術含量,透過 Lua 提供的 C API - lua_register() 就可以將 C function 註冊到 Lua state,到是可以看看他的 prototype:
//lua.h #define lua_register(L,n,f) (lua_pushcfunction(L, (f)), lua_setglobal(L, (n)))
n 代表函數在 Lua state 中的全域變數名稱(記得 Lua 中 function 為 first class),lua_pushcfunction() 將 C function push 到 Lua state stack,lua_setglobal() 則從 Lua state stack 中 pop 函數位址給變數 n。例如 lua_register(L, "foo", foo); 將會把 int foo(lua_State *L) 設定給變數 "foo",接著在 Lua script 中就可以用 foo() 呼叫 int foo(lua_State *L)。值得一提的是也可以註冊 C++ static class function,因為 Lua 是認函數位址而不是認 C/C++ 的語法定義。
這部份請參考 [1] Chapter 26
3. Lua call C++ object
很多人可能認為這是最難的部份。事實上,如果您真的要「從 Lua script 產生 C++ object,使用完畢後讓 Lua 垃圾收集器回收」,一般的方法是用 lua_newuserdata() 產生一個指向 C++ object 的指標,然後與 meta table 關聯,利用 meta table 中的 function 模擬 C++ object 呼叫 meber function,如果您有興趣的話可以參考 [1] Ch 28,[2] [3]。
小弟認為不需要的原因是當 Lua script 需要呼叫 C++ object 時,基本上碰到的 C++ object 在整個 Lua state 生命週期裡都只有一份,比方說 COM Port 只有一個,輸出的畫面只有一個。所以我們可以用個偷懶的方式,就是先建立好這些 C++ object,然後 Lua call C function 時能夠存取到這些 C++ object 的指標就好。
最簡單的方式是在 C/C++ 內用全域變數(指標)或靜態變數儲存這些 C++ object 的位址,然後 用 lua_register() 註冊的 C 函數就可以存取他們,但這是最糟的方式,有幾個原因:
- 用 lua_register() 註冊的 C 函數生命週期應該與 Lua state 的生命週期綁定(雖然無法真的消滅這些 C 函數)。當 state 結束(lua_close),這些 C 函數不應該有能力繼續存取這些 C++ object。
- 無法跨越不同的 Lua state,如同 Lua 本身內建的 package,A Lua state 的 B package 在運作時不應該影響到 C Lua state 的 B package,除非有特殊需求。
- 使用 multi-thread 執行不同的 Lua state 時會發生 race condition。
那我們有什麼解決方案呢?
3.1 Registry
這裡的 Registry 跟 Windows Registry 沒有一毛錢關係,而是指 Lua Registry。在 [1] Ch 27.3 Storing State in C Functions 裡面對 Registry 的說明是:
The registry is a global table that can be accessed only by C code.
也就是這個 table 不會被 Lua script 存取到,不必擔心被 Lua 垃圾收集,也是我們藏資料的好地方,以下是相關 source code:
bool cLua::InsertRegistry(const char *key, void *data) { lua_pushlightuserdata(m_pScriptContext, data); lua_setfield(m_pScriptContext, LUA_REGISTRYINDEX, key); return true; } void* cLua::FindRegistry(lua_State *state, const char *key) { lua_getfield(state, LUA_REGISTRYINDEX, key); void *p = lua_touserdata(state, -1); lua_pop(state, 1); return p; } void* cLua::FindRegistry(const char *key) { lua_getfield(m_pScriptContext, LUA_REGISTRYINDEX, key); void *p = lua_touserdata(m_pScriptContext, -1); lua_pop(m_pScriptContext, 1); return p; }
cLua 僅是 Lua state 很薄的一層封裝(取自參考資料[5]),讓 cLua object 被摧毀時一併銷毀 Lua state,如此而已。lua_pushlightuserdata(Line#3)是 Lua 提供用來儲存 C 指標的 API。cLua::InsertRegistry 與 cLua::FindRegistry() 使用範例:
bool EventManager::init(cLua& lua) { for(int i = 0; i < sizeof(EventManagerLib)/sizeof(EventManagerLib[0]); ++i) { if(!lua.AddFunction(EventManagerLib[i].name, EventManagerLib[i].func)) return false; } return lua.InsertRegistry("EventManager", (void*)this); } //...... int SetOnReadEventHandler(lua_State *state) { EventManager *em = (EventManager*)cLua::FindRegistry(state, "EventManager"); assert(em != 0); if(!em->setOnReadEventHandler(luaL_optstring(state, 1, 0))) luaL_error(state, "failed to call %s()", __FUNC__); return 0; }
由上面的例子不難看出來,我們在 C++ object 建立時把物件註冊進去(Line#9),當 Lua script call C/C++ function 時用 "EventManager" (Line#16)就可以反查回原物件。
3.2 upvalue
這個方法小弟也沒試過,不過光看到書上花好幾頁解釋就先放在一邊了。小弟的見解是,如果一開始就滲入複雜的方案,那很高的機率就是帶來滿滿的挫折,尤其小弟並非天生神力,先讓整個程式動起來,再慢慢改善對我而言是比較好的作法。
4. C/C++ call Lua
直接複製一段原始碼解釋比較快:
//...... cnt_of_rx_bytes = sio.recv(&buf[0], buf.size()); if(cnt_of_rx_bytes > 0) { if(!em.sendOnReadEvent(lua, &buf[0], cnt_of_rx_bytes)) { memSIO->Lines->Add(lua.getErrorAnsiStr()); stop_ = true; } } //......
當 sio 從 COM port 收到資料(Line#2),我們就立刻對 Lua script function 做 callback(Line#5),這部份由 em.sendOnReadEvent() 負責:
bool EventManager::sendOnReadEvent(cLua& lua, const unsigned char *data, int num_of_bytes) { lua.GetGlobal(on_read_event_handler_name_.c_str()); //lua_getglobal() lua.PushLString((const char*)data, num_of_bytes); //lua_pushlstring() return lua.PCall(1, 0); //lua_pcall() }
這部份不清楚的地方可以參考 [1]P.224。另外,Lua script callback function 是怎麼註冊進 EventManager 的?到了這個時候來看看完整的 script 吧!
SetOnReadEventHandler("OnRead") SetOnTimeoutEventHandler("OnTimeout") LF = 0x0A rx_data = "" function OnRead(data) rx_data = rx_data .. data if string.byte(rx_data, -1) == LF then PrintAsciiString(rx_data) rx_data = "" -- clear string end end function OnTimeout() -- clear rx_data = "" end
Line#1-2 我們把 Lua script callback function 註冊到 C++ object,以前面的 C++ 例子來看,Line#1 就是把 "OnRead" 存進 on_read_event_handler_name_,"OnTimeout" 以此類推。
5. 參考資料
[1] Lua 程序設計第 2 版,ISBN 978-7-121-06187-5
[2] Illustrative C++ Lua binding example/tutorial
[3] Luna Wrapper
[4] 为 Lua 绑定 C/C++ 对象
[5] Lua游戏开发实践指南,ISBN 9787111403357
[2] Illustrative C++ Lua binding example/tutorial
[3] Luna Wrapper
[4] 为 Lua 绑定 C/C++ 对象
[5] Lua游戏开发实践指南,ISBN 9787111403357
感謝分享,自己的工具自己造,讚啦。
回覆刪除您的文章相當豐富、生動,期待下篇文章^^。
謝謝您的稱讚
刪除建議可以把通訊資料收集與通訊協定分析瀏覽分開開發.
回覆刪除UART通訊資料不像USB or Ethernet 有明確封包區分, 資料記錄最好有精準的時間(每個Byte的接收時間).
請問是新竹X公司的林前輩嗎?的確是有人這樣做的,像是BACnet MSTP就有人寫成wireshark外掛,小弟試用過,效果真的很棒
刪除