~bigbes/tarantool

tarantool-protobuf

ref: 46045da1d9330db68d0e8210ff0ba4ae2c0885db tarantool-protobuf/README.md -rw-r--r-- 6.1 KiB
46045da1 — Eugene Blikh codec: precompute per-field readers (runtime decode +10–17%) 3 months ago

#tarantool-protobuf

A protoc plugin and pure-Lua runtime for using Protocol Buffers (proto3) and gRPC service stubs from Tarantool.

Tarantool ships an in-tree require('protobuf') module, but it is encode-only and has no support for map, oneof, services, or any decode path. This project fills those gaps with:

  • protoc-gen-tarantool — a protoc plugin (Go) that turns .proto files into Lua modules.
  • runtime/pb — a pure Lua + LuaJIT-FFI runtime the generated code uses for the wire format. Named pb rather than protobuf to avoid colliding with Tarantool's built-in module.

#Status

MVP — proto3 messages and enums, end-to-end round-trip verified.

Feature State
proto3 scalars (all 15 types)
Repeated, packed by default
Nested messages, self-reference
Cross-file imports
Enums (open semantics)
64-bit integers as LuaJIT cdata
Two codegen modes (full + runtime)
map<K,V> (scalar/message values)
oneof
proto3 explicit optional + has_/clear_
gRPC service stubs (unary)
gRPC streaming (server / client / bidi)
Loopback / multiplex transport
WKT: Timestamp ↔ datetime
WKT: Duration, Empty, wrappers
WKT: Struct, Value, ListValue
WKT: Any (opaque + registry pack/unpack)
WKT: FieldMask
Byte-for-byte interop with protoc ✅ (18 fixtures)
Google conformance runner (cmd/conformance-runner.lua)
Runtime .proto parsing (pb.parse)
proto3 JSON (pb.json.encode/.decode)
Unknown-field passthrough (_unknown_fields)
Microbenchmark + alloc regression gate (make bench)
proto2 / editions ❌ out of scope

#Quick start

# 1. Build the plugin and generate the example.
make gen

# 2. Run the round-trip test in Tarantool.
make test

The plugin emits one .lua file per .proto. By default the output path mirrors the proto package (package foo.bar; baz.protofoo/bar/baz_pb.lua), required as foo.bar.baz_pb. Override the Lua module path with a file option:

import "tarantool/tarantool.proto";
option (tarantool.lua_package) = "myapp.proto.foo";

#Generated API

For each message Foo the plugin emits:

local M = require('myapp.proto.foo')

M.Foo_descriptor   -- the descriptor table consumed by the runtime
M.Foo_new(t)       -- returns t (or {}); placeholder for future validation
M.Foo_encode(t)    -- table -> wire bytes (string)
M.Foo_decode(b)    -- wire bytes (string) -> table

For each enum Color:

M.Color_descriptor          -- { name, by_name, by_value }
M.Color                     -- alias for by_name: M.Color.RED -> 0

Repeated fields are Lua arrays (1-based, contiguous). 64-bit integers (int64, uint64, fixed64, sfixed64, sint64) are LuaJIT int64_t / uint64_t cdata — lossless and the same convention used by Tarantool's net.box, msgpack, and built-in protobuf modules.

#Layout

cmd/protoc-gen-tarantool/   # Go plugin
  main.go                   # reads CodeGeneratorRequest, hands off to gen
  internal/gen/             # codegen package
runtime/pb/                 # Lua runtime (require('pb'))
  init.lua                  # public surface
  wire.lua                  # varint / zigzag / fixed / float / LEN
  codec.lua                 # descriptor-driven encode/decode
options/tarantool/          # custom proto options
  tarantool.proto           # (tarantool.lua_package) file option
examples/proto/             # demo .proto inputs
examples/expected/          # generated output (committed for inspection)
test/                       # roundtrip test

#Conformance

cmd/conformance-runner.lua speaks the Google protobuf conformance protocol on stdin/stdout. Drive it with the canonical conformance_test_runner binary like so:

make gen
conformance_test_runner --enforce_recommended \
    tarantool cmd/conformance-runner.lua

The runner currently supports protobuf_test_messages.proto3.TestAllTypesProto3 in both protobuf and JSON formats; proto2 / editions / JSPB / text-format cases return skipped. The self-test in test/conformance_test.lua exercises the runner with crafted requests on every make test run.

#Benchmarks

make bench           # print throughput + alloc per op (5 sizes × 2 modes)
make bench-baseline  # overwrite bench/baseline.json (run on a quiet machine)
make bench-compare   # exit 1 if any alloc-per-op regressed >5% vs baseline

The committed bench/baseline.json tracks only allocation per op — that's reproducible across machines because it counts bytes, not time. Throughput in stderr is informational; it swings 30%+ on a contended CPU.

Current baseline (LuaJIT 2.1, hello.Person):

Payload encode alloc (full / runtime) decode alloc (full / runtime)
10 B 0.37 / 0.63 KB 0.51 / 0.51 KB
100 B 0.37 / 0.63 KB 0.51 / 0.51 KB
1 KB 5.98 / 7.04 KB 7.75 / 7.88 KB
10 KB 47.5 / 48.6 KB 59.5 / 59.6 KB
100 KB 444 / 445 KB 568 / 568 KB

#Why named pb instead of protobuf?

Tarantool's loader prefers the built-in require('protobuf') over any filesystem module of the same name. Trying to override it would break code that uses the built-in's encode API. pb is short, unambiguous, and lives alongside the built-in.

#License

TBD.