pb runtime API referenceEverything require('pb') exposes, organized by what you call from
application code.
For generated per-message functions (M.Foo_encode, M.Foo_text,
etc.) see generated-api.md. For the descriptor
table shape see ../codegen.md.
local pb = require('pb')
local bytes = pb.encode(desc, t) -- table -> wire bytes
local t = pb.decode(desc, bytes) -- wire bytes -> table
local view = pb.decode_lazy(desc, bytes) -- wire bytes -> MessageView
local mod = pb.parse(proto_source) -- .proto text -> module
local set = pb.from_pb(descset_bytes) -- FileDescriptorSet -> module set
pb.json.encode(desc, t) -- proto3 JSON encode
pb.json.decode(desc, s [, opts]) -- proto3 JSON decode
pb.text.encode(desc, t [, opts]) -- text format encode
pb.text.decode(desc, s [, opts]) -- text format decode
pb.NULL -- canonical null sentinel
pb.to_uint64(v) / pb.to_int64(v) -- coerce to 64-bit cdata
pb.any.pack(desc, t [, prefix]) -- build google.protobuf.Any
pb.any.unpack(any_t [, desc]) -- unpack to {desc, t}
pb.register(desc) / pb.lookup(name) -- type registry for Any
pb.grpc.loopback(server) -- in-process gRPC transport
pb.grpc.multiplex({srv1, srv2, ...}) -- fan multiple servers
pb.field_names(t) -- strict field-name table (codegen)
pb.enum(name, {RED=0, ...}) -- enum descriptor (codegen)
pb.finalize_message(desc) -- finalize a hand-rolled descriptor
pb.encode(desc, t) -> stringEncode t against desc to protobuf wire bytes. Same input shape as
generated M.Foo_encode(t); runs through descriptor dispatch instead of
inline code. ~5-15% slower than full-mode Foo_encode on the
microbenchmark; same allocation profile.
pb.decode(desc, bytes) -> tableDecode wire bytes against desc. Returns a plain Lua table whose keys
match field names. Defaults are filled in per proto3 semantics; absent
explicit-optional fields stay nil. Unknown fields are concatenated
into t._unknown_fields (raw bytes, re-emitted on encode).
pb.decode_lazy(desc, bytes) -> MessageViewBuild a zero-copy view over the bytes. Nothing past the field index is
decoded until you call :get, :has, :iter, etc. See
api-modes.md → lazy for the full
surface and when it wins.
pb.lazyThe lazy module itself (pb.lazy.build, the MessageView /
ArrayView / MapView classes). Most callers use pb.decode_lazy;
this is for advanced consumers that need to construct views by hand.
Two ways to produce a descriptor module at runtime, both yielding the
same shape generated code emits in mode=runtime.
pb.parse(source) -> moduleParse a .proto source string and return a module-shaped table
{<MessageName>_descriptor = ..., <MessageName>_encode = ..., ...}. The
parser handles proto3 syntax including services, options, imports,
nested messages, oneofs, maps, explicit-optional. WKT imports
(google/protobuf/*.proto) resolve to the pb.wkt descriptors
automatically.
local hello = pb.parse(io.open('hello.proto'):read('*a'))
local bytes = hello.Person_encode({name = 'Alice'})
pb.from_pb(descset_bytes) -> {files, order, lookup}Parse a binary FileDescriptorSet (the output of
protoc --descriptor_set_out=...) and return:
files[name] — per-file module, indexed by the original .proto
filename.order — array of filenames in dependency order.lookup(fqn) -> descriptor — find any message or enum by its fully-
qualified name (e.g. 'hello.Person').local set = pb.from_pb(io.open('build/all.pb', 'rb'):read('*a'))
local desc = set.lookup('hello.Person')
local bytes = pb.encode(desc, {name = 'Alice'})
pb.parser, pb.dynamic, pb.filesetThe submodules behind pb.parse / pb.from_pb. Exposed for callers
that want the AST step (pb.parser.parse(text) -> ast) or to build
descriptors by hand (pb.dynamic.build(ast)).
pb.jsonStrict proto3 JSON. See runtime/pb/json.lua for the canonical-mapping
details (camelCase field names, base64 for bytes, RFC 3339 for
Timestamp, etc.).
pb.json.encode(desc, t) -> stringpb.json.decode(desc, s [, opts]) -> table —
opts.ignore_unknown_fields = true accepts JSON with extra keys
(matches the conformance suite's JSON_IGNORE_UNKNOWN_PARSING_TEST
category).pb.textMainline-protoc text format, both directions.
pb.text.encode(desc, t [, opts]) -> string — opts.single_line = true collapses to one space-separated line; opts.indent = '<string>' overrides the default two-space indent.pb.text.decode(desc, s [, opts]) -> table — handles every grammar
bucket the conformance text suite exercises (decimal/hex/octal int
literals, float specials, C-style and \u/\U escapes, {} /
<> aggregates, repeated short-form [a, b, c], map entries, the
inline Any form, enum-by-name-or-number, reserved-name drop, and
numeric-field-ID tolerance).Both directions support _unknown_fields passthrough.
pb.wktHand-rolled descriptors for google.protobuf.*. Generated code that
references a WKT field routes to these automatically — you only touch
pb.wkt directly when packing/unpacking by hand or registering a type
for Any.
| Symbol | Notes |
|---|---|
pb.wkt.Timestamp_descriptor |
Lua-side value: datetime cdata when in spec, or {seconds, nanos} table when out of spec. |
pb.wkt.Duration_descriptor |
Lua-side value: {seconds, nanos} table. |
pb.wkt.Empty_descriptor |
Lua-side value: {}. |
pb.wkt.<T>Value_descriptor (Int32, Int64, UInt32, UInt64, Bool, String, Bytes, Float, Double) |
Auto-wrap/unwrap: pass the scalar directly, decode returns the scalar. |
pb.wkt.Struct_descriptor |
Lua-side value: a table tagged via pb.wkt.struct(t) if disambiguation is needed (Struct vs Value vs ListValue). |
pb.wkt.Value_descriptor |
Lua-side value: native Lua of matching shape; use pb.NULL for null. |
pb.wkt.ListValue_descriptor |
Lua-side value: array; use pb.wkt.list(t) to disambiguate. |
pb.wkt.FieldMask_descriptor |
Lua-side value: {'foo.bar', 'baz', ...}. |
pb.wkt.Any_descriptor |
Opaque {type_url, value} table by default; see Any below. |
pb.wkt.NullValue_descriptor |
Enum with single value NULL_VALUE = 0. |
local s = pb.wkt.struct({a = 1, b = 'x'}) -- table tagged as Struct
local l = pb.wkt.list({1, 2, 3}) -- table tagged as ListValue
Use these when stashing a value into google.protobuf.Value (or a
Struct field) and the codec can't infer whether you mean a Struct,
a ListValue, or a primitive map/array.
Anypb.any.pack(desc, t [, prefix]) -> any_table
Encode t against desc, wrap as
{type_url = '<prefix>/<full.name>', value = <bytes>}. Default prefix
is type.googleapis.com.
pb.any.unpack(any_t [, desc]) -> {desc, t} or (t, desc)
Decode the value bytes against the supplied descriptor, or
auto-resolve via the registry if desc is nil.
pb.register(desc) / pb.lookup(name_or_url) -> desc
Type registry. Generated code does not auto-register messages —
call pb.register(M.Foo_descriptor) once per type you want to round-
trip through Any by type_url alone.
pb.grpc| Symbol | Notes |
|---|---|
pb.grpc.loopback(server) |
In-process transport. server is the table returned by M.<Service>_server(impl). Suitable for tests and same-process apps; uses fiber.channel. |
pb.grpc.multiplex({srv1, srv2, ...}) |
Fan multiple _server results onto one transport. Errors on duplicate paths. |
pb.grpc.new_stream_pair(buf_size) |
Build a paired (client_stream, server_stream) over a fiber.channel. Used internally by loopback; exposed for custom transports. |
pb.grpc.wrap_* |
Helpers that wrap a raw stream/call with input/output codecs. Used by generated client/server code. |
The transport contract (:unary, :server_stream, :client_stream,
:bidi) is documented in
../specs/grpc_transports.md. Any table
implementing those four methods plugs into a generated client.
pb.NULLCanonical null sentinel used by google.protobuf.Value and proto3
JSON. Equal to box.NULL — preferred form is pb.NULL so application
code doesn't have to depend on box being available.
pb.to_uint64(v) -> uint64_t cdatapb.to_int64(v) -> int64_t cdataCoerce a Lua number, cdata, or numeric string into the matching LuaJIT cdata. Use at any boundary where the input type isn't already cdata (JSON, text format, net.box arguments, user input). The codec otherwise requires cdata for the five 64-bit-typed fields and will error on bare Lua numbers past 2^53.
local id = pb.to_uint64('18446744073709551615') -- max uint64
local t = {user_id = id}
_pb.lua)Three helpers application code rarely calls directly — they're how generated modules and hand-rolled descriptors get built:
pb.field_names(tbl) -> tblWrap a {field_name = field_name, ...} table with a strict
__index / __newindex so unknown keys error at the read site.
Generated code emits M.<Type>_fields = pb.field_names({...}) for use
with the lazy view (view:get(F.user_id)).
pb.enum(name, {NAME = number, ...}) -> enum_descriptorBuild an enum descriptor with reversible by_name / by_value maps:
local Color = pb.enum('app.Color', {RED = 0, GREEN = 1, BLUE = 2})
Color.by_name.RED -- 0
Color.by_value[1] -- 'GREEN'
pb.finalize_message(desc) -> descFill in field_by_id / field_by_name / oneofs_list, mark cdata-
keyed maps for pointer-vs-value dedup on decode, and attach
per-field _writer / _reader specializations for the hot path.
Call after constructing desc.fields[]. Idempotent.
pb.wireLow-level encode/decode for individual wire types. Generated full-mode
code inlines calls to these; application code rarely needs them
directly. The full surface is in runtime/pb/wire.lua. Highlights:
pb.WIRE_VARINT, pb.WIRE_I64, pb.WIRE_LEN,
pb.WIRE_I32 (also under pb.wire.WIRE_*).pb.wire.encode_int32, decode_string, etc.
for all 15 scalar types.pb.wire.encode_tag(id, wt), decode_tag(buf, pos).encode_varint, decode_varint, the four zigzag
variants.pb.wire.is_valid_utf8(s) — ICU-backed, matches
every proto3 UTF-8 rejection rule.Adding a new scalar means touching wire.lua (primitives +
TYPE_INFO), types.go (Kind mapping), and inline.go (emission).
See codegen.md → adding a new wire type.
pb.codecExposed for generated inline code that wants to share helpers (e.g.
pb.codec.merge_message for sub-message merging on repeated decode).
Not intended as a stable application-facing surface; reach for the
high-level pb.encode / pb.decode instead.