2016年4月24日 星期日

RS232/422/485 Anaylzer DIY PART2

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]。


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

To Be Continued...

4 則留言:

  1. 感謝分享,自己的工具自己造,讚啦。
    您的文章相當豐富、生動,期待下篇文章^^。

    回覆刪除
  2. 建議可以把通訊資料收集與通訊協定分析瀏覽分開開發.
    UART通訊資料不像USB or Ethernet 有明確封包區分, 資料記錄最好有精準的時間(每個Byte的接收時間).

    回覆刪除
    回覆
    1. 請問是新竹X公司的林前輩嗎?的確是有人這樣做的,像是BACnet MSTP就有人寫成wireshark外掛,小弟試用過,效果真的很棒

      刪除