Home

Awesome

Google protobuf support for Lua

Build StatusCoverage Status

English | 中文


This project offers a C module for Lua (5.1, 5.2, 5.3, 5.4 and LuaJIT) manipulating Google's protobuf protocol, both for version 2 and 3 syntax and semantics. It splits to the lower-level and the high-level parts for different goals.

For converting between binary protobuf data with Lua tables, using pb.load() loads the compiled protobuf schema content (*.pb file) generated by Google protobuf's compiler named protoc and call pb.encode()/pb.decode().

Or use these modules to manipulate the raw wire format in lower-level way:

If you don't want to depend Google's protobuf compiler, protoc.lua is a pure Lua module translating text-based protobuf schema content into the *.pb binary format.

Install

To install, you could just use luarocks:

luarocks install lua-protobuf

If you want to build it from source, just clone the repo and use luarocks:

git clone https://github.com/starwing/lua-protobuf
luarocks make rockspecs/lua-protobuf-scm-1.rockspec

If you don't have luarocks, use hererocks to install Lua and luarocks:

pip install hererocks
git clone https://github.com/starwing/lua-protobuf
hererocks -j 2.0 -rlatest .
bin/luarocks make lua-protobuf/rockspecs/lua-protobuf-scm-1.rockspec CFLAGS="-fPIC -Wall -Wextra" LIBFLAGS="-shared"
cp protoc.lua pb.so ..

Or you can build it by hand, it only has a pure Lua module protoc.lua and a pair of C source: pb.h and pb.c. Notice that in order to build the pb C module, you need Lua header file and/or libary file installed. replace $LUA_HEADERS and $LUA_LIBS below to real install locations.

To build it on macOS, use your favor compiler:

gcc -O2 -shared -undefined dynamic_lookup -I "$LUA_HEADERS" pb.c -o pb.so

On Linux, use the nearly same command:

gcc -O2 -shared -fPIC -I "$LUA_HEADERS" pb.c -o pb.so

On Windows, you could use MinGW or MSVC, create a *.sln project or build it on the command line (notice the Lua_BUILD_AS_DLL flag):

cl /O2 /LD /Fepb.dll /I "$LUA_HEADERS" /DLUA_BUILD_AS_DLL pb.c "$LUA_LIBS"

Example

local pb = require "pb"
local protoc = require "protoc"

-- load schema from text (just for demo, use protoc.new() in real world)
assert(protoc:load [[
   message Phone {
      optional string name        = 1;
      optional int64  phonenumber = 2;
   }
   message Person {
      optional string name     = 1;
      optional int32  age      = 2;
      optional string address  = 3;
      repeated Phone  contacts = 4;
   } ]])

-- lua table data
local data = {
   name = "ilse",
   age  = 18,
   contacts = {
      { name = "alice", phonenumber = 12312341234 },
      { name = "bob",   phonenumber = 45645674567 }
   }
}

-- encode lua table data into binary format in lua string and return
local bytes = assert(pb.encode("Person", data))
print(pb.tohex(bytes))

-- and decode the binary data back into lua table
local data2 = assert(pb.decode("Person", bytes))
print(require "serpent".block(data2))

Use case

零境交错

Usage

protoc Module

FunctionReturnsDescriptions
protoc.new()Proroc objectcreate a new compiler instance
protoc.reload()truereload all google standard messages into pb module
p:parse(string)tabletransform schema to DescriptorProto table
p:compile(string)stringtransform schema to binary *.pb format data
p:load(string)trueload schema into pb module
p.loadedtablecontains all parsed DescriptorProto table
p.unknown_importsee belowhandle schema import error
p.unknown_typesee belowhandle unknown type in schema
p.include_importsboolauto load imported proto

To parse a text schema content, create a compiler instance first:

local p = protoc.new()

Then, set some options to the compiler, e.g. the unknown handlers:

-- set some hooks
p.unknown_import = function(self, module_name) ... end
p.unknown_type   = function(self, type_name) ... end
-- ... and options
p.include_imports = true

The unknown_import and unknown_type handle could be true, string or a function. Seting it to true means all non-exist modules and types are given a default value without triggering an error; A string means a Lua pattern that indicates whether an unknown module or type should raise an error, e.g.

p.unknown_type = "Foo.*"

means all types prefixed by Foo will be treat as existing type and do not trigger errors.

If these are functions, the unknown type and module name will be passed to functions. For module handler, it should return a DescriptorProto Table produced by p:load() functions, for type handler, it should return a type name and type, such as message or enum, e.g.

function p:unknown_import(name)
  -- if can not find "foo.proto", load "my_foo.proto" instead
  return p:parsefile("my_"..name)
end

function p:unknown_type(name)
  -- if cannot find "Type", treat it as ".MyType" and is a message type return ".My"..name, "message"
end

After setting options, use load() or compile() or parse() function to get result.

pb Module

pb module has high-level routines to manipulate protobuf messages.

In below table of functions, we have several types that have special means:

NOTICE: Only pb.load() returns error on failure, do check the result it returns. Other routines raise a error when failure for convenience.

FunctionReturnsDescription
pb.clear()Noneclear all types
pb.clear(type)Nonedelete specific type
pb.load(data)boolean,integerload a binary schema data into pb module
pb.encode(type, table)stringencode a message table into binary form
pb.encode(type, table, b)bufferencode a message table into binary form to buffer
pb.decode(type, data)tabledecode a binary message into Lua table
pb.decode(type, data, table)tabledecode a binary message into a given Lua table
pb.pack(fmt, ...)stringsame as buffer.pack() but return string
pb.unpack(data, fmt, ...)values...same as slice.unpack() but accept data
pb.types()iteratoriterate all types in pb module
pb.type(type)see belowreturn informations for specific type
pb.fields(type)iteratoriterate all fields in a message
pb.field(type, string)see belowreturn informations for specific field of type
pb.typefmt(type)Stringtransform type name of field into pack/unpack formatter
pb.enum(type, string)numberget the value of a enum by name
pb.enum(type, number)stringget the name of a enum by value
pb.defaults(type[, table/nil])tableget the default table of type
pb.hook(type[, function])functionget or set hook functions
pb.option(string)stringset options to decoder/encoder
pb.state()pb.Stateretrieve current pb state
pb.state(newstate | nil)pb.Stateset new pb state and retrieve the old one

Schema loading

pb.load() accepts the schema binary data and returns a boolean indicates the result of loading, success or failure, and a offset reading in schema so far that is useful to figure out the reason of failure.

Type mapping

Protobuf TypesLua Types
double, floatnumber
int32, uint32, fixed32, sfixed32, sint32number or integer in Lua 5.3+
int64, uint64, fixed64, sfixed64, sint64number or "#" prefixed string or integer in Lua 5.3+
boolboolean
string, bytesstring
messagetable
enumstring or number

Type Information

Using pb.(type|field)[s]() functions retrieve type information for loaded messages.

pb.type() returns multiple informations for specified type:

pb.types() returns a iterators, behavior like call pb.type() on every types of all messages.

print(pb.type "MyType")

-- list all types that loaded into pb
for name, basename, type in pb.types() do
  print(name, basename, type)
end

pb.field() returns information of the specified field for one type:

And pb.fields() iterates all fields in a message:

print(pb.field("MyType", "the_first_field"))

-- notice that you needn't receive all return values from iterator
for name, number, type in pb.fields "MyType" do
  print(name, number, type)
end

pb.enum() maps from enum name and value:

protoc:load [[
enum Color { Red = 1; Green = 2; Blue = 3 }
]]
print(pb.enum("Color", "Red")) --> 1
print(pb.enum("Color", 2)) --> "Green"

Default Values

Using pb.defaults() to get or set a table with all default values from a message. this table will be used as the metatable of the corresponding decoded message table when setting use_default_metatable option.

You could also call pb.defaults with "*map" or "*array" to get the default metatable for map and array when decoding a message. These settings will bypass use_default_metatable option.

To clear a default metatable, just pass nil as second argument to pb.defaults().

   check_load [[
      message TestDefault {
         optional int32 defaulted_int = 10 [ default = 777 ];
         optional bool defaulted_bool = 11 [ default = true ];
         optional string defaulted_str = 12 [ default = "foo" ];
         optional float defaulted_num = 13 [ default = 0.125 ];
      } ]]
   print(require "serpent".block(pb.defaults "TestDefault"))
-- output:
-- {
--   defaulted_bool = true,
--   defaulted_int = 777,
--   defaulted_num = 0.125,
--   defaulted_str = "foo"
-- } --[[table: 0x7f8c1e52b050]]

Hooks

If set pb.option "enable_hooks", the hook function will be enabled. you could use pb.hook() and pb.encode_hook to set or get a decode or encode hook function, respectively: call it with type name directly get current setted hook; call it with two arguments to set a hook; and call it with nil as the second argument to remove the hook. in all case, the original one will be returned.

After the hook function setted and hook enabled, the decode function will be called after a message get decoded and encode functions will be called before the message is encoded. So you could get all values in the table passed to hook function. That's the only argument of hook.

If you need type name in hook functions, use this helper:

local function make_hook(name, func)
  return pb.hook(name, function(t)
    return func(name, t)
  end)
end

Options

Setting options to change the behavior of other routines. These options are supported currently:

OptionDescription
enum_as_nameset value to enum name when decode a enum (default)
enum_as_valueset value to enum value when decode a enum
int64_as_numberset value to integer when it fit into uint32, otherwise return a number (default)
int64_as_stringsame as above, but return a string instead
int64_as_hexstringsame as above, but return a hexadigit string instead
auto_default_valuesact as use_default_values for proto3 and act as no_default_values for the others (default)
no_default_valuesdo not default values for decoded message table
use_default_valuesset default values by copy values from default table before decode
use_default_metatableset default values by set table from pb.default() as the metatable
enable_hookspb.decode will call pb.hooks() hook functions
disable_hookspb.decode do not call hooks (default)
encode_default_valuesdefault values also encode
no_encode_default_valuesdo not encode default values (default)
decode_default_arraywork with no_default_values,decode null to empty table for array
no_decode_default_arraywork with no_default_values,decode null to nil for array (default)
encode_orderguarantees the same message will be encoded into the same result with the same schema and the same data (but the order itself is not specified)
no_encode_orderdo not have guarantees about encode orders (default)
decode_default_messagedecode null message to default message table
no_decode_default_messagedecode null message to null (default)

Note: The string returned by int64_as_string or int64_as_hexstring will prefix a '#' character. Because Lua may convert between string with number, prefix a '#' makes Lua return the string as-is.

all routines in all module accepts '#' prefix string/hex string as arguments regardless of the option setting.

Multiple State

pb module support multiple states. A state is a database that contains all type information of registered messages. You can retrieve current state by pb.state(), or set new state by pb.state(newstate).

Use pb.state(nil) to discard current state, but not to set a new one (the following routines call that use the state will create a new default state automatedly). Use pb.state() to retrieve current state without setting a new one. e.g.

local old = pb.state(nil)
-- if you use protoc.lua, call protoc.reload() here.
assert(pb.load(...))
-- do someting ...
pb.state(old)

Notice that if you use protoc.lua module, it will register some message to the state, so you should call proto.reload() after setting a new state.

pb.io Module

pb.io module reads binary data from a file or stdin/stdout, pb.io.read() reads binary data from a file, or stdin if no file name given as the first parameter.

pb.io.write() and pb.io.dump() are same as Lua's io.write() except they write binary data. the former writes data to stdout, and the latter writes data to a file specified by the first parameter as the file name.

All these functions return a true value when success, and return nil, errmsg when an error occurs.

FunctionReturnsDescription
io.read()stringread all binary data from stdin
io.read(string)stringread all binary data from file name
io.write(...)truewrite binary data to stdout
io.dump(string, ...)stringwrite binary data to file name

pb.conv Module

pb.conv provide functions to convert between numbers.

Encode FunctionDecode Function
conv.encode_int32()conv.decode_int32()
conv.encode_uint32()conv.decode_uint32()
conv.encode_sint32()conv.decode_sint32()
conv.encode_sint64()conv.decode_sint64()
conv.encode_float()conv.decode_float()
conv.encode_double()conv.decode_double()

pb.slice Module

Slice object parse binary protobuf data in a low-level way. Use slice.new() to create a slice object, with the optional offset i and j to access a subpart of the original data (named a view).

As protobuf usually nest sub message with in a range of slice, a slice object has a stack itself to support this. Calling s:enter(i, j) saves current position and enters next level with the optional offset i and j just as slice.new(). calling s:leave() restore the prior view. s:level() returns the current level, and s:level(n) returns the current position, the start and the end position information of the nth level. calling s:enter() without parameter will read a length delimited type value from the slice and enter the view in reading value. Using #a to get the count of bytes remains in current view.

local s = slice.new("<data here>")
local tag = s:unpack "v"
if tag%8 == 2 then -- tag has a type of string/bytes? maybe it's a sub-message.
  s:enter() -- read following bytes value, and enter the view of bytes value.
  -- do something with bytes value, e.g. reads a lot of fixed32 integers from bytes.
  local t = {}
  while #s > 0 do
    t[#t+1] = s:unpack "d"
  end
  s:leave() -- after done, leave bytes value and ready to read next value.
end

To read values from slice, use slice.unpack(), it use a format string to control how to read into a slice as below table (same format character are also used in buffer.pack()). Notice that you can use pb.typefmt() to convert between format and protobuf type names (returned from pb.field()).

FormatDescription
vvariable Int value
d4 bytes fixed32 value
q8 bytes fixed64 value
slength delimited value, usually a string, bytes or message in protobuf.
creceive a extra number parameter count after the format, and reads count bytes in slice.
bvariable int value as a Lua boolean value.
f4 bytes fixed32 value as floating point number value.
F8 bytes fixed64 value as floating point number value.
ivariable int value as signed int value, i.e. int32
jvariable int value as zig-zad encoded signed int value, i.e.sint32
uvariable int value as unsigned int value, i.e. uint32
x4 bytes fixed32 value as unsigned fixed32 value, i.e.fixed32
y4 bytes fixed32 value as signed fixed32 value, i.e. sfixed32
Ivariable int value as signed int value, i.e.int64
Jvariable int value as zig-zad encoded signed int value, i.e. sint64
Uvariable int value and treat it as uint64
X8 bytes fixed64 value as unsigned fixed64 value, i.e. fixed64
Y8 bytes fixed64 value as signed fixed64 value, i.e. sfixed64

And extra format can be used to control the read cursor in one slice.unpack() process:

FormatDescription
@returns current cursor position in the slice, related with the beginning of the current view.
*set the current cursor position to the extra parameter after format string.
+set the relate cursor position, i.e. add the extra parameter to the current position.

e.g. If you want to read a varint value twice, you can write it as:

local v1, v2 = s:unpack("v*v", 1)
-- v: reads a `varint` value
-- *: receive the second parameter 1 and set it to the current cursor position, i.e. restore the cursor to the head of the view
-- v: reads the first `varint` value again

All routines in pb.slice module:

FunctionReturnsDescription
slice.new(data[,i[,j]])Slice objectcreate a new slice object
s:delete()nonesame as s:reset(), free it's content
tostring(s)stringreturn the string repr of the object
#snumberreturns the count of bytes can read in current view
s:result([i[, j]])Stringreturn the remaining bytes in current view
s:reset([...])selfreset object to another data
s:level()numberreturns the count of stored state
s:level(number)p, i, jreturns the informations of the nth stored state
s:enter()selfreads a bytes value, and enter it's view
s:enter(i[, j])selfenter a view start at i and ends at j, includes
s:leave([number])self, nleave the number count of level (default 1) and return current level
s:unpack(fmt, ...)values...reads values of current view from slice

pb.buffer Module

Buffer module used to construct a protobuf data format stream in a low-level way. It's just a bytes data buffer. using buffer.pack() to append values to the buffer, and buffer.result() to get the encoded raw data, or buffer.tohex() to get the human-readable hex digit value of data.

buffer.pack() use the same format syntax with slice.unpack(), and support '()' format means the inner value will be encoded as a length delimited value, i.e. a message value encoded format.

parenthesis can be nested.

e.g.

b:pack("(vvv)", 1, 2, 3) -- get a bytes value that contains three varint value.

buffer.pack() also support '#' format, it means prepends a length into the buffer.

e.g.

b:pack("#", 5) -- prepends a varint length #b-5+1 at offset 5

All routines in pb.buffer module:

FunctionReturnsDescription
buffer.new([...])Buffer objectcreate a new buffer object, extra args will passed to b:reset()
b:delete()nonesame as b:reset(), free it's content
tostring(b)stringreturns the string repr of the object
#bnumberreturns the encoded count of bytes in buffer
b:reset()selfreset to a empty buffer
b:reset([...])selfresets the buffer and set its content as the concat of it's args
b:tohex([i[, j]])stringreturn the string of hexadigit represent of the data, i and j are ranges in encoded data, includes. Omit it means the whole range
b:result([i[,j]])stringreturn the raw data, i and j are ranges in encoded data, includes,. Omit it means the whole range
b:pack(fmt, ...)selfencode the values passed to b:pack(), use fmt to indicate how to encode value