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.dll

assert(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数据。通常情况下是用不上的,如果有需求的话后续会在这里更新使用说明。

编辑于 2017-03-27