~bigbes/tarantool

tarantool-protobuf

ref: 43f7b869d1b39ad485f37df3e554a5f485795378 tarantool-protobuf/runtime/pb/wire.lua -rw-r--r-- 20.8 KiB
43f7b869 — Eugene Blikh 3 months ago
wire: reject invalid UTF-8 in proto3 string fields

Per the proto3 spec, a string field's bytes must form valid UTF-8.
decode_string was aliased to decode_len, so any byte sequence was
accepted and round-tripped. Adds a pure-Lua RFC 3629 validator —
is_valid_utf8 — and routes M.decode_string through it. The bytes
type keeps the raw decode_len path so binary payloads still pass.

The validator covers stray continuation bytes, truncated sequences,
overlong encodings, UTF-16 surrogates (U+D800..U+DFFF), and code
points above U+10FFFF.

Codec, lazy, dynamic, and the generated inline code all consume the
same M.decode_string, so singular / repeated / oneof / map-key /
map-value string fields are all covered.

Drops 5 entries from test/conformance/known_failures.txt
(RejectInvalidUtf8.String.*) and adds 7 regression tests covering
each invalid form plus a positive multi-byte string round-trip and
a bytes-field control.
2ed0f4b5 — Eugene Blikh 3 months ago
wire: validate field number and minimal encoding in decode_tag

Three checks added on the multi-byte path:
1. Field number must be > 0 (IllegalZeroFieldNum_Case_0/1/3).
2. Field number must fit in 29 bits per the protobuf spec
   (BadTag_FieldNumberSlightlyTooHigh, BadTag_FieldNumberTooHigh).
3. Tag varint must be minimally encoded — a trailing 0 byte with more
   than one byte read is overlong (BadTag_OverlongVarint).

The field-number check runs on bit ops over the uint64 cdata returned
by decode_varint rather than after tonumber, otherwise field numbers
above 2^32 alias into the valid range (e.g., fn=2^31+1 was being
recovered as fn=1).

The single-byte fast path picks up the field-zero check directly via
the b >> 3 == 0 condition.

Drops 6 entries from test/conformance/known_failures.txt and adds 4
regression tests pinning each rejection path.
1712192b — Eugene Blikh 3 months ago
wire: bounds-check skip_field for I32/I64/LEN truncation

skip_field advanced pos by a fixed or length-prefixed amount with no
check against #buf, so a truncated unknown field was silently consumed:
the outer decode loop's `while pos <= len` exited without raising,
making the parser accept payloads it should have rejected.

Validates that the new position never exceeds #buf+1 for WIRE_I64,
WIRE_I32, and both WIRE_LEN fast and slow paths. WIRE_VARINT already
errored correctly via decode_varint's per-byte check.

Drops 15 entries from test/conformance/known_failures.txt
(PrematureEofBeforeUnknownValue.*, PrematureEofInsideUnknownValue.*,
PrematureEofInDelimitedDataForUnknownValue.*).
7442ea2b — Eugene Blikh 3 months ago
wire: reject illegal wire types 6/7 in decode_tag

Wire types 6 and 7 are never assigned in the protobuf wire format; only
0 (VARINT), 1 (I64), 2 (LEN), 3/4 (SGROUP/EGROUP, proto2-only), and 5
(I32) are valid. skip_field already rejected them for unknown fields,
but the decode loop in codec.lua and inline.go dispatched to a typed
reader whenever the field ID was known — the reader ignored wt and
called the value-specific decoder anyway, accepting bytes that should
have been a parse error.

Moves the check into decode_tag so every parser path — codec, generated
inline code, lazy, and map sub-fields — rejects 6/7 uniformly.

Drops 24 entries from test/conformance/known_failures.txt.
0c13fd73 — Eugene Blikh 3 months ago
wire: truncate varints to 32 bits in int32/uint32/sint32/enum decode

decode_int32/uint32/sint32 returned full uint64 values when the wire input
carried bits above bit 31, corrupting re-encode on 51 conformance tests that
exercise overlong or over-range varints. Per the proto3 spec the decoder
must keep only the low 32 bits (and sign-extend for signed types).

Adds wire.varint_to_int32 / varint_to_uint32 and routes every enum-varint
decode site through them: wire.lua typed decoders, codec.lua (5 enum
sites), lazy.lua (3 sites), and the generated code via inline.go (3 sites).

Drops 51 entries from test/conformance/known_failures.txt.
add2f224 — Eugene Blikh 3 months ago
wire: document why encode_varint's fast path stays 1-byte only

Adds a comment explaining the failed experiment with 2/3/4-byte fast
paths: extending the function past the LuaJIT inline budget makes
parent traces stop inlining it, costing ~30% on 1-byte-dominant
workloads (and the bench payload is 1-byte-dominant since most
proto field tags, enum ordinals, and short-string length prefixes
fit in 7 bits). Future maintainers should resist the temptation.

No code change beyond the comment.
a33dbecb — Eugene Blikh 3 months ago
wire: encode_varint 1-byte fast path (up to 3.8× encode throughput)

For non-negative Lua numbers under 0x80, return string.char(n) directly
— no `to_uint64` cdata allocation, no `out` table, no table.concat.
Covers the dominant case for typical payloads: length prefixes for
strings <128 bytes, small int field values, enum ordinals, and most
tag bytes when codegen-precomputation isn't available.

Symmetric to the decode_varint 1-byte fast path in 9f3bfb8 but the
payoff is much bigger because the encode path was paying for both
a cdata allocation and a list-builder per call, not just a varint
loop.

Effect (bench/bench.lua, hello.Person):
  full/10B   encode:  13 →  30 MB/s (2.3×)
  full/100B  encode: 125 → 278 MB/s (2.2×)
  full/1KB   encode:  69 → 210 MB/s (3.0×)
  full/10KB  encode:  98 → 352 MB/s (3.6×)
  full/100KB encode: 108 → 398 MB/s (3.7×)
  runtime/10B   encode:  9 →  16 MB/s (1.8×)
  runtime/100B  encode: 78 → 151 MB/s (1.9×)
  runtime/1KB   encode: 68 → 183 MB/s (2.7×)
  runtime/10KB  encode: 99 → 352 MB/s (3.5×)
  runtime/100KB encode: 109 → 411 MB/s (3.8×)
  decode: unchanged
  alloc/op: unchanged (bench-compare clean)
9f3bfb8f — Eugene Blikh 3 months ago
wire: inline decode_varint 1-byte fast path at all decoders (~2× decode)

decode_tag, decode_len, decode_int32/uint32/int64/uint64/sint32/sint64/
bool, and the VARINT/LEN branches of skip_field each now read the first
byte directly, handle 0..127 in straight-line code, and call into
decode_varint only for multi-byte values. The duplicated 3 lines per
call site are the cost of avoiding LuaJIT 2.1's side-trace-returning-
from-inlined-call limitation: with the fast path inlined, side traces
off the parent decoder's hot guard stay in the caller's own frame and
stitch back cleanly instead of bridging to interpreter dispatch.

Effect (bench/bench.lua, hello.Person across 5 sizes):
  full mode decode:    2.1×–2.4× throughput (104→220 .. 14→33 MB/s)
  runtime mode decode: 2.0×–2.1× throughput (90→175 .. 12→25 MB/s)
  encode: unchanged (only decode paths were touched)
  alloc/op: unchanged (bench-compare clean)
  bridges: 27 → 7 across 10 jit-trace runs (-74%);
           remaining are encoder-side (codec.lua:41/110 in runtime mode)
aff3ee42 — Eugene Blikh 3 months ago
M6: trace stability gate + two fixes

Add `make jit-trace` (`bench/jit_trace.lua`) — a standalone tarantool
script that attaches a `jit.attach('trace')` listener over each hot
encode/decode path and asserts no aborts in our source files fall into
the fatal set (NYI bytecode, blacklisting, persistent type instability).
Runs outside luatest because on macOS arm64 the test framework exhausts
JIT mcode pages before the test body runs, masking real abort reasons.

Two fixes shipped to make all 13 scenarios pass:

  - `decode_varint` grew a 1-byte fast path. Before, calling it from a
    hot decode loop pulled an inner `while true do` into the caller's
    root trace, which got blacklisted after enough retries.

  - `pb.finalize_message` now precomputes `desc.oneofs_list` (array
    form) and the runtime-mode codec iterates it with ipairs instead
    of `pairs(desc.oneofs)`. `pairs()` over a hash-keyed table compiles
    to bytecode ISNEXT, which is NYI in LuaJIT 2.1.

The gate also reports interpreter-bridge counts as a benchmark-quality
metric. Decoders show 0-4 bridges per run depending on JIT timing —
caused by side traces returning from inlined `decode_varint` calls,
which LuaJIT 2.1 can't stitch back cleanly. Small per-call overhead on
the multi-byte slow path, structural to the engine.

Scope caveat: map fields encode via `pairs()` and remain off-trace —
pinned by the gate's last scenario so we notice if upstream lifts the
restriction.
784dea4d — Eugene Blikh 3 months ago
Initial commit: protoc-gen-tarantool plugin + pb runtime

A protoc plugin (Go) and a pure-Lua + LuaJIT-FFI runtime that give
Tarantool a complete proto3 + gRPC stack. Two codegen modes (full
inline / runtime descriptor), 226-test luatest suite, 18-fixture
mainline-protoc interop corpus, JSON codec, well-known types,
gRPC client/server factories, runtime .proto parser, microbench
harness with allocation regression gate.

Covers PLAN.md M1-M5. Module is `pb` (not `protobuf`) to avoid
colliding with Tarantool's built-in encode-only `protobuf` module.