lua-protobuf 使用說明

應某小夥伴邀請,寫一篇關於我的lua-protobuf庫的介紹文章。

lua-protobuf實際上是一個純C的protobuf協議實現,和對應的Lua綁定。協議實現在一個single-header文件里,即pb.h,這個文件實現了完整的protobuf協議。pb.c是針對Lua的綁定。lua-protobuf支持Lua5.1/LuaJIT, Lua5.2和Lua5.3。庫本身是平台無關的,可以在PC或者移動設備上使用。

為啥重複發明輪子?

首先實現這個庫的時候(其實到現在也是這樣),protobuf的Lua支持比較好的只有pbc,而pbc採用了懶惰載入的方式,返回的並不是純粹的Lua表(plain lua table),而是讀取什麼解析什麼,這就給實際使用數據造成了困難:你必須完全知道協議里有什麼域,如果不知道,你必須直接使用pairs遍歷這張表。如果直接訪問了不存在的field,那麼pbc會直接產生錯誤消息。

另外一個原因是pbc是Lua和C的混合庫,很多事情是在Lua庫里實現的,實際上pbc的C庫起到一個底層解析和消息資料庫的作用。lua-protobuf所有的解析代碼都在C裡面,只需要編譯pb.c一個文件就有全部功能了,使用起來更加方便。

lua-protobuf的實現也更為簡單,使用了一個同時支持string和integer的哈希表實現,而不是兩個哈希表;直接用C代碼解析了pb文件,不依賴元數據,這些都讓lua-protobuf的代碼更加直觀。

lua-protobuf天然分成三個模塊,利用三種不同的類型來區分:pb_State提供了類型信息,pb_Slice專門負責解析二進位數據,而pb_Buffer專門負責編碼二進位數據。代碼上非常清晰。

構建lua-protobuf

首先看看如何編譯。如果只是使用C介面,那麼不需要編譯,直接把pb.h拷貝進你的項目即可。如果需要使用Lua介面(大多數是這樣)那麼有兩個方法:自己編譯或者用luarocks。

如果你的luarocks是在Linux下面或者在Windows下使用MinGW,那麼安裝lua-protobuf是很簡單的:

luarocks install lua-protobuf

然而,因為Lua的綁定實際上除了pb庫以外還導出了四個庫,而luarocks並不知道這一點,所以如果你的luarocks是用VS編譯的,則編譯之後的pb.dll文件只會導出pb庫,雖然不影響使用,但是也比較不爽……這時候可以考慮下載代碼手動編譯:

cl /O2 /LD /MT /DLUA_BUILD_AS_DLL /I/path/to/lua/include pb.c path/to/lua/lib

其中,O2指定最大化優化速度,LD表明生成dll文件,MT指定生成的dll不依賴運行時庫。聲明LUA_BUILD_AS_DLL宏是為了導出pb.c里所有的符號,pb.c是唯一一個需要編譯的.c文件,而Lua提供的頭文件和導入庫需要手動制定位置。

高層介面

pb.dll 提供四個模塊:

  • pb模塊:高層介面,提供和pbc兼容的encode/decode介面。
  • pb.conv:這是一個轉換工具庫,負責在Lua里方便地在protobuf提供的各種類型和Lua原生類型之間轉換。
  • pb.slice:提供了底層的protobuf協議解析能力,能夠在不知道message的情況下解析協議二進位數據。
  • pb.buffer:提供了底層的protobuf的協議序列化能力,能夠在不知道message的情況下序列化信息。
  • pb.io:這個主要是為寫protoc插件使用的。protoc會把pb二進位文件通過stdin傳遞給插件,然而stdin在Windows下默認是用文本模式打開的,這就會導致解析錯誤。因此pb.io提供了二進位模式下的IO讀寫功能。

其中pb模塊是最簡單的。如果要使用基本的protobuf的解析/序列化功能,那麼首先你需要Google的protoc.exe,這可以通過編譯官方的protobuf項目得到。然後,你需要手寫一個proto文件,我們以標準的addressbook.proto為例:

// See README.txt for information and build instructions.package tutorial;option java_package = "com.example.tutorial";option java_outer_classname = "AddressBookProtos";message Person { required string name = 1; required int32 id = 2; // Unique ID number for this person. optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4; repeated int32 test = 5 [packed=true]; extensions 10 to max; }message Ext { extend Person { optional int32 test = 10; }}// Our address book file is just one of these.message AddressBook { repeated Person person = 1;}

首先我們生成pb文件:

protoc -o addressbook.pb addressbook.proto

接著,在Lua里我們就可以通過pb文件來讀寫protobuf協議了:

local pb = require "pb" -- 載入 pb.dllassert(pb.loadfile "addressbook.pb") -- 載入剛才編譯的pb文件local person = { -- 我們定義一個addressbook里的 Person 消息 name = "Alice", id = 12345, phone = { { number = "1301234567" }, { number = "87654321", type = "WORK" }, }}-- 序列化成二進位數據local data = assert(pb.encode("tutorial.Person", person))-- 從二進位數據解析出實際消息local msg = assert(pb.decode("tutorial.Person", data))-- 列印消息內容(使用了serpent開源庫)print(require "serpent".block(msg))

這裡列印消息我們使用了serpent庫,這是一個開源的表格序列化庫,就只有一個文件serpent.lua,可以在lua-protobuf的test目錄下找到這個文件。使用這個庫可以很輕鬆地列印表格的結構。

我這裡的輸出是這樣的:

{ id = 12345, name = "Alice", phone = { { number = "1301234567" } --[[table: 00816578]], { number = "87654321", type = "WORK" } --[[table: 008165F0]] } --[[table: 008165A0]]} --[[table: 00816550]]

注意phone的第一個項並沒有加上默認值 type = "HOME",這個主要是Lua介面的問題。C模塊是讀了默認值的,但是Lua這邊暫時並沒有將默認值寫進Lua表裡,如果要寫的話可能會影響解析性能,所以如果有小夥伴需要這個功能,我再考慮加上,我們暫時是不需要這個功能的。

基本上和pbc不同的地方只是載入消息用loadfile函數而不是register函數了。如果一定要完全兼容pbc,那麼加一行 pb.register = pb.loadfile 也是可以的。

高層介面還提供了這些函數:

  • pb.clear(),清除之前註冊的所有消息
  • pb.clear(msgName),清除某個之前註冊的消息
  • pb.load(chunk),直接解析字元串/Slice格式的二進位pb數據註冊消息。

底層介面

底層介面和C介面主要的功能是在沒有/不知道pb數據的情況下,解析二進位的protobuf數據。通常情況下是用不上的,如果有需求的話後續會在這裡更新使用說明。


推薦閱讀:

unity開發像獵天使魔女,忍龍,鬼泣這樣的高速動作類遊戲有多難?
Unreal Engine 虛幻引擎:若深愛,請讓它自由。世界強大的遊戲引擎開源免費了。
Unity 性能優化總結—CPU篇
Simcity這個遊戲的宗旨是什麼,開發這個遊戲的人是不是城市規劃相關人員?
知乎遊戲界有哪些值得關注的用戶?

TAG:Lua | protobuf | 游戏开发 |