Emit the ZAP runtime for C++ instead of porting it per chain

ZAP decides what the bytes mean, so a second hand-written codec in a
second language is a second answer to what a frame is. The C++ chains
had two of them — one per chain, already drifted — because zip could
emit an RPC client SDK from a schema but not the runtime that SDK reads
and writes bytes with.

CppZap renders that runtime: the reader, the builder, and the arithmetic,
in one namespace-parameterised C++23 header. Its constants are imported
from github.com/luxfi/zap rather than retyped, and its accessors are that
runtime name for name, so the two can be checked against each other line
by line.

The promise it makes is the bytes, and the promise is a test result:
TestCppZapKeepsTheWire compiles the emitted header and diffs its buffers
against the Go runtime's over seventeen shapes — scalars, tails, growth,
backward pointers, every list kind, flags, both versions — and
TestCppZapReadsWhatGoWrote closes the loop the other way.
This commit is contained in:
2026-09-05 14:33:24 -07:00
parent b2a8053b8a
commit ddc11c181c
3 changed files with 1360 additions and 0 deletions
+23
View File
@@ -30,6 +30,8 @@ func main() {
runCLI(args)
case "docs":
runDocs(args)
case "zap":
runZap(args)
default:
usage()
os.Exit(1)
@@ -44,6 +46,7 @@ Commands:
mcp Generate native Go, Rust, or C++ MCP server and tools
cli Generate native Go, Rust, or C++ CLI commands and runners
docs Generate @hanzo/docs compatible MDX documentation pages
zap Generate the ZAP wire runtime (reader + builder) for a target language
Run 'zipgen <command> -h' for command options.
`)
@@ -85,6 +88,26 @@ func runSDK(args []string) {
}
}
// runZap emits the ZAP wire runtime itself — the reader and the builder — for a
// target language. This is the codec every generated SDK and every hand-written
// chain reads and writes bytes with, so it is stated once here rather than
// ported per repository.
func runZap(args []string) {
fs := flag.NewFlagSet("zap", flag.ExitOnError)
lang := fs.String("lang", "cpp", "target language: cpp")
ns := fs.String("ns", "lux::zap", "namespace for the emitted runtime")
out := fs.String("o", "", "output directory or file")
fs.Parse(args)
switch strings.ToLower(*lang) {
case "cpp", "c++":
writeOutput(*out, "zap.hpp", zip.CppZap(*ns))
default:
fmt.Fprintf(os.Stderr, "unknown language: %s (choose cpp)\n", *lang)
os.Exit(1)
}
}
func runMCP(args []string) {
fs := flag.NewFlagSet("mcp", flag.ExitOnError)
lang := fs.String("lang", "rust", "target language: rust, cpp, or go")
+731
View File
@@ -0,0 +1,731 @@
package zip
// The ZAP wire runtime, emitted for C++.
//
// ZAP decides what the bytes MEAN. A second hand-written codec in a second
// language is a second answer to "what is this frame", and two answers inside
// one binary is a fork surface — so the runtime is STATED once, here, and
// rendered per language and per namespace.
//
// # Where the definition comes from
//
// github.com/luxfi/zap is the runtime the Go chains link. Its constants are
// imported below rather than retyped, so kHeaderSize, kAlignment, the magic
// and the accepted versions cannot drift from the Go runtime by editing this
// file. The accessor bodies are the same arithmetic on the same layout,
// transliterated name for name: Go's Object.BytesFixedSlice is bytes_fixed_slice,
// List.ObjectPtr is object_ptr, ObjectBuilder.SetBytes is set_bytes. A reader
// holding both open should be able to check one against the other line by line,
// which is the only review that can catch a codec drifting.
//
// # What the emitted header promises
//
// The bytes. A message built by the emitted Builder is byte-identical to one
// built by the Go Builder from the same calls in the same order —
// TestCppZapKeepsTheWire holds that against the Go runtime directly by
// compiling the emitted header and diffing buffers, so the promise is a test
// result and not a comment.
//
// # The two deliberate shape changes
//
// A Go Object holds *Message and the collector keeps the buffer alive; a C++
// Object copied out of a local Message would dangle, so Object and List carry
// (pointer, size, position) and are self-contained views. Second, every
// accessor guards a NEGATIVE position, which Go cannot produce from an
// unsigned wire read but C++ can be handed by a caller passing a negative
// field offset. Neither touches a byte on the wire.
import (
"fmt"
"strings"
luxzap "github.com/luxfi/zap"
)
// CppZap renders the ZAP wire runtime as one self-contained C++23 header in the
// given namespace ("lux::zap"). The namespace is the only knob: two chains that
// want the codec under two names get the SAME arithmetic, not two ports.
func CppZap(namespace string) []byte {
if namespace == "" {
namespace = "zap"
}
guard := strings.ToUpper(strings.ReplaceAll(namespace, "::", "_")) + "_ZAP_HPP_"
var b strings.Builder
fmt.Fprintf(&b, cppZapPreamble, namespace, guard, guard)
fmt.Fprintf(&b, cppZapConstants,
luxzap.HeaderSize,
luxzap.Alignment,
luxzap.Version1,
luxzap.Version2,
luxzap.Version,
cppByteList(luxzap.Magic),
)
b.WriteString(cppZapPrimitives)
b.WriteString(cppZapReader)
b.WriteString(cppZapWriter)
fmt.Fprintf(&b, "} // namespace %s\n\n#endif // %s\n", namespace, guard)
return []byte(b.String())
}
// cppByteList spells a Go string as a C++ char initializer list, so the magic
// is carried as the bytes it is rather than a literal a translator can retype.
func cppByteList(s string) string {
parts := make([]string, 0, len(s))
for i := 0; i < len(s); i++ {
parts = append(parts, fmt.Sprintf("char(0x%02x)", s[i]))
}
return strings.Join(parts, ", ")
}
const cppZapPreamble = `// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// Code generated by zip from the ZAP wire definition. DO NOT EDIT.
// Regenerate with: zipgen zap -lang cpp -ns %s -o <path>
//
// ZAP — the Zero-copy Application Protocol, and the ONLY serialization a Lux
// chain writes. This header is the whole runtime: the reader, the builder, and
// the arithmetic that decides what a frame means.
//
// header 16B: Magic "ZAP\0" | Version u16 | Flags u16 | RootOffset u32 | Size u32
// body: 8-byte-aligned objects, lists and byte runs; every pointer is
// RELATIVE to the position of the pointer field itself.
//
// All multi-byte integers are little-endian.
//
// READ SIDE IS A VIEW. Message, Object and List alias a caller-owned buffer and
// copy nothing; the buffer must outlive them. That is the point of the format,
// and it is why a type that must survive its input caches its own wire bytes.
//
// READS ARE TOTAL. A read that leaves the buffer answers zero or an empty span,
// never a fault, so a hostile buffer can reach a typed accessor without a
// validation pass in front of it. Bounds live here and not in every caller;
// callers still validate MEANING.
#ifndef %s
#define %s
#include <bit>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace %[1]s {
`
const cppZapConstants = `inline constexpr std::int64_t kHeaderSize = %d;
inline constexpr std::int64_t kAlignment = %d;
// Two schema generations share one data-segment encoding and differ only in the
// meaning of the leading struct byte. Both are ACCEPTED on read; kVersion is
// what this runtime's Builder emits.
inline constexpr std::uint16_t kVersion1 = %d;
inline constexpr std::uint16_t kVersion2 = %d;
inline constexpr std::uint16_t kVersion = %d;
inline constexpr char kMagic[4] = {%s};
// Header flags.
inline constexpr std::uint16_t kFlagNone = 0;
inline constexpr std::uint16_t kFlagCompressed = 1 << 0;
inline constexpr std::uint16_t kFlagEncrypted = 1 << 1;
inline constexpr std::uint16_t kFlagSigned = 1 << 2;
`
const cppZapPrimitives = `// ---- little-endian primitives: the ONE place byte order is spelled ----
inline std::uint16_t get_u16(const std::uint8_t* p) {
return std::uint16_t(std::uint16_t(p[0]) | (std::uint16_t(p[1]) << 8));
}
inline std::uint32_t get_u32(const std::uint8_t* p) {
return std::uint32_t(p[0]) | (std::uint32_t(p[1]) << 8) | (std::uint32_t(p[2]) << 16) |
(std::uint32_t(p[3]) << 24);
}
inline std::uint64_t get_u64(const std::uint8_t* p) {
return std::uint64_t(get_u32(p)) | (std::uint64_t(get_u32(p + 4)) << 32);
}
inline void put_u16(std::uint8_t* p, std::uint16_t v) {
p[0] = std::uint8_t(v);
p[1] = std::uint8_t(v >> 8);
}
inline void put_u32(std::uint8_t* p, std::uint32_t v) {
p[0] = std::uint8_t(v);
p[1] = std::uint8_t(v >> 8);
p[2] = std::uint8_t(v >> 16);
p[3] = std::uint8_t(v >> 24);
}
inline void put_u64(std::uint8_t* p, std::uint64_t v) {
put_u32(p, std::uint32_t(v));
put_u32(p + 4, std::uint32_t(v >> 32));
}
// message_length is the size field at offset 12 — the self-delimiting length of
// the leading message in a buffer, and so the split point between the unsigned
// prefix of a signed transaction and the credentials that follow it. It reads
// the header field only; magic and version are the parse's business.
inline std::optional<std::size_t> message_length(std::span<const std::uint8_t> b) {
if (b.size() < std::size_t(kHeaderSize)) return std::nullopt;
const std::size_t n = get_u32(b.data() + 12);
if (n < std::size_t(kHeaderSize) || n > b.size()) return std::nullopt;
return n;
}
`
const cppZapReader = `// ================================ read side ================================
class Object;
// List is a view of one list: where its elements start, and how many the wire
// says there are. The element WIDTH is the accessor's, not the list's — u32(i)
// strides 4, object(i, n) strides n — which is exactly how the wire encodes it.
class List {
public:
List() = default;
List(const std::uint8_t* data, std::size_t size, std::int64_t offset, std::int64_t length)
: d_(data), n_(size), off_(offset), len_(length) {}
std::int64_t len() const { return len_; }
bool is_null() const { return d_ == nullptr; }
std::uint8_t u8(std::int64_t i) const {
if (i < 0 || i >= len_) return 0;
const std::int64_t pos = off_ + i;
if (pos < 0 || std::uint64_t(pos) >= n_) return 0;
return d_[pos];
}
std::uint32_t u32(std::int64_t i) const {
if (i < 0 || i >= len_) return 0;
const std::int64_t pos = off_ + i * 4;
if (pos < 0 || std::uint64_t(pos) + 4 > n_) return 0;
return get_u32(d_ + pos);
}
std::uint64_t u64(std::int64_t i) const {
if (i < 0 || i >= len_) return 0;
const std::int64_t pos = off_ + i * 8;
if (pos < 0 || std::uint64_t(pos) + 8 > n_) return 0;
return get_u64(d_ + pos);
}
// object(i, elem_size) — an INLINE element: a fixed-stride record living in
// the list itself.
inline Object object(std::int64_t i, std::int64_t elem_size) const;
// object_ptr(i) — an OUT-OF-LINE element: a 4-byte SIGNED relative pointer,
// dereferenced exactly as Object::object does. This is how a repeated
// message field crosses, each element carrying its own tail.
inline Object object_ptr(std::int64_t i) const;
// bytes is the whole run, for a list whose elements are bytes.
std::span<const std::uint8_t> bytes() const {
if (d_ == nullptr || off_ < 0 || std::uint64_t(off_ + len_) > n_) return {};
return {d_ + off_, std::size_t(len_)};
}
private:
const std::uint8_t* d_ = nullptr;
std::size_t n_ = 0;
std::int64_t off_ = 0;
std::int64_t len_ = 0;
};
// Object is a view of one record's fixed section. It carries the whole buffer so
// every read can bound itself; an Object at offset 0 is the null object and
// every read through it answers zero.
class Object {
public:
Object() = default;
Object(const std::uint8_t* data, std::size_t size, std::int64_t offset)
: d_(data), n_(size), off_(offset) {}
bool is_null() const { return d_ == nullptr || off_ == 0; }
std::int64_t offset() const { return off_; }
const std::uint8_t* buffer() const { return d_; }
std::size_t buffer_size() const { return n_; }
bool boolean(std::int64_t field) const { return u8(field) != 0; }
std::uint8_t u8(std::int64_t field) const {
const std::int64_t pos = off_ + field;
if (d_ == nullptr || pos < 0 || std::uint64_t(pos) >= n_) return 0;
return d_[pos];
}
std::uint16_t u16(std::int64_t field) const {
const std::int64_t pos = off_ + field;
if (d_ == nullptr || pos < 0 || std::uint64_t(pos) + 2 > n_) return 0;
return get_u16(d_ + pos);
}
std::uint32_t u32(std::int64_t field) const {
const std::int64_t pos = off_ + field;
if (d_ == nullptr || pos < 0 || std::uint64_t(pos) + 4 > n_) return 0;
return get_u32(d_ + pos);
}
std::uint64_t u64(std::int64_t field) const {
const std::int64_t pos = off_ + field;
if (d_ == nullptr || pos < 0 || std::uint64_t(pos) + 8 > n_) return 0;
return get_u64(d_ + pos);
}
std::int8_t i8(std::int64_t field) const { return std::int8_t(u8(field)); }
std::int16_t i16(std::int64_t field) const { return std::int16_t(u16(field)); }
std::int32_t i32(std::int64_t field) const { return std::int32_t(u32(field)); }
std::int64_t i64(std::int64_t field) const { return std::int64_t(u64(field)); }
float f32(std::int64_t field) const { return std::bit_cast<float>(u32(field)); }
double f64(std::int64_t field) const { return std::bit_cast<double>(u64(field)); }
// bytes_fixed_slice: n bytes living IN the fixed section (an id, a public
// key), as opposed to bytes() which follows a pointer to a variable tail.
// The symmetric reader for set_bytes_fixed.
std::span<const std::uint8_t> bytes_fixed_slice(std::int64_t field, std::int64_t n) const {
if (d_ == nullptr || n <= 0) return {};
const std::int64_t pos = off_ + field;
if (pos < 0 || std::uint64_t(pos) + std::uint64_t(n) > n_) return {};
return {d_ + pos, std::size_t(n)};
}
// A variable byte tail: {relOffset u32, length u32}.
//
// Wire-format rule: relOffset is an UNSIGNED forward pointer. A crafted
// high-bit value becomes a huge positive and dies on the end bound rather
// than aliasing backwards, and a target inside the wire header is rejected
// outright — a tail can never legitimately live at offsets 0..kHeaderSize-1.
// That pair of rules is the pointer-escape gate.
std::span<const std::uint8_t> bytes(std::int64_t field) const {
const std::int64_t pos = off_ + field;
if (d_ == nullptr || pos < 0 || std::uint64_t(pos) + 4 > n_) return {};
const std::uint32_t rel = get_u32(d_ + pos);
if (rel == 0) return {}; // null
const std::int64_t len_pos = pos + 4;
if (std::uint64_t(len_pos) + 4 > n_) return {};
const std::uint32_t length = get_u32(d_ + len_pos);
const std::uint64_t abs = std::uint64_t(pos) + rel;
if (abs < std::uint64_t(kHeaderSize)) return {};
if (abs + length > n_) return {};
return {d_ + abs, std::size_t(length)};
}
std::string_view text(std::int64_t field) const {
const auto b = bytes(field);
if (b.empty()) return {};
return std::string_view(reinterpret_cast<const char*>(b.data()), b.size());
}
// A nested object pointer.
//
// Wire-format rule: relOffset is SIGNED — an honest builder that finalized
// the child first points BACKWARDS. A backward pointer could otherwise reach
// the wire header, which carries magic, version, flags, root and size and is
// never a legitimate object payload, so any target below kHeaderSize is
// rejected.
Object object(std::int64_t field) const {
const std::int64_t pos = off_ + field;
if (d_ == nullptr || pos < 0 || std::uint64_t(pos) + 4 > n_) return {};
const std::int32_t rel = std::int32_t(get_u32(d_ + pos));
if (rel == 0) return {}; // null
const std::int64_t abs = pos + rel;
if (abs < kHeaderSize || std::uint64_t(abs) >= n_) return {};
return Object(d_, n_, abs);
}
// A list pointer. relOffset is SIGNED, as for object(). The length is
// clamped to the message size: an attacker-set 0xFFFFFFFF would otherwise
// let a downstream loop over len() run four billion times even though every
// per-element accessor answers zero.
List list(std::int64_t field) const {
const std::int64_t pos = off_ + field;
if (d_ == nullptr || pos < 0 || std::uint64_t(pos) + 8 > n_) return {};
const std::int32_t rel = std::int32_t(get_u32(d_ + pos));
if (rel == 0) return {}; // null
const std::uint32_t length = get_u32(d_ + pos + 4);
if (std::uint64_t(length) > n_) return {};
const std::int64_t abs = pos + rel;
if (abs < kHeaderSize || std::uint64_t(abs) >= n_) return {};
return List(d_, n_, abs, std::int64_t(length));
}
// list_stride is list() with the element width the caller knows. It applies
// the tighter test length*min_stride <= what remains of the buffer, so an
// impossible count is refused once, up front, rather than at every element.
// The wire is unchanged: any list() that would succeed succeeds here too
// when given the true stride.
List list_stride(std::int64_t field, std::uint32_t min_stride) const {
const std::int64_t pos = off_ + field;
if (d_ == nullptr || pos < 0 || std::uint64_t(pos) + 8 > n_) return {};
const std::int32_t rel = std::int32_t(get_u32(d_ + pos));
if (rel == 0) return {}; // null
const std::uint32_t length = get_u32(d_ + pos + 4);
const std::int64_t abs = pos + rel;
if (abs < kHeaderSize || std::uint64_t(abs) >= n_) return {};
const std::uint64_t remaining = n_ - std::uint64_t(abs);
if (min_stride > 0) {
if (std::uint64_t(length) * std::uint64_t(min_stride) > remaining) return {};
} else if (std::uint64_t(length) > n_) {
return {};
}
return List(d_, n_, abs, std::int64_t(length));
}
private:
const std::uint8_t* d_ = nullptr;
std::size_t n_ = 0;
std::int64_t off_ = 0;
};
inline Object List::object(std::int64_t i, std::int64_t elem_size) const {
if (i < 0 || i >= len_) return {};
return Object(d_, n_, off_ + i * elem_size);
}
inline Object List::object_ptr(std::int64_t i) const {
if (i < 0 || i >= len_) return {};
const std::int64_t pos = off_ + i * 4;
if (pos < 0 || std::uint64_t(pos) + 4 > n_) return {};
const std::int32_t rel = std::int32_t(get_u32(d_ + pos));
if (rel == 0) return {}; // null element
const std::int64_t abs = pos + rel;
if (abs < kHeaderSize || std::uint64_t(abs) >= n_) return {};
return Object(d_, n_, abs);
}
// Message is a validated window onto caller-owned bytes. It does not own them:
// whoever holds the buffer holds them, and it must outlive every view taken.
class Message {
public:
Message() = default;
// parse validates magic, version and the declared size, and says WHY on a
// violation rather than returning a half-valid message. Both versions are
// accepted; a caller that requires one schema generation reads version()
// afterwards.
//
// The declared size must be at least the header: a size=0 buffer would
// otherwise pass and then read past an empty window.
static bool parse(std::span<const std::uint8_t> data, Message* out, std::string* err) {
if (data.size() < std::size_t(kHeaderSize)) {
if (err) *err = "zap: buffer too small";
return false;
}
if (std::memcmp(data.data(), kMagic, 4) != 0) {
if (err) *err = "zap: invalid magic bytes";
return false;
}
const std::uint16_t version = get_u16(data.data() + 4);
if (version != kVersion1 && version != kVersion2) {
if (err) *err = "zap: unsupported version";
return false;
}
const std::uint32_t size = get_u32(data.data() + 12);
if (std::int64_t(size) < kHeaderSize || std::size_t(size) > data.size()) {
if (err) *err = "zap: buffer too small";
return false;
}
if (out) *out = Message(data.data(), std::size_t(size));
return true;
}
// The same parse, for a caller that wants the answer and not the reason.
// One implementation, two doors.
static std::optional<Message> parse(std::span<const std::uint8_t> data) {
Message m;
if (!parse(data, &m, nullptr)) return std::nullopt;
return m;
}
bool valid() const { return d_ != nullptr; }
std::uint16_t version() const { return d_ ? get_u16(d_ + 4) : 0; }
std::uint16_t flags() const { return d_ ? get_u16(d_ + 6) : 0; }
std::size_t size() const { return n_; }
std::span<const std::uint8_t> bytes() const { return {d_, n_}; }
const std::uint8_t* data() const { return d_; }
Object root() const {
if (d_ == nullptr) return {};
return Object(d_, n_, std::int64_t(get_u32(d_ + 8)));
}
// root_object_at anchors an Object at a position the caller already knows —
// the offset finish_as_root returned — instead of re-reading the header.
Object root_object_at(std::int64_t off) const { return Object(d_, n_, off); }
private:
Message(const std::uint8_t* d, std::size_t n) : d_(d), n_(n) {}
const std::uint8_t* d_ = nullptr;
std::size_t n_ = 0;
};
`
const cppZapWriter = `// =============================== write side ================================
class ObjectBuilder;
class ListBuilder;
// Builder writes a message. Nothing is deferred: start_object RESERVES the whole
// fixed section, so a variable field appends its tail immediately after it and
// patches its own relative pointer on the spot. There is no intention list to
// replay, and the bytes a set call produces never depend on what a later call
// does.
class Builder {
public:
explicit Builder(std::size_t capacity = 256, std::uint16_t version = kVersion) {
if (capacity < std::size_t(kHeaderSize)) capacity = 256;
buf_.assign(capacity, 0);
pos_ = kHeaderSize;
std::memcpy(buf_.data(), kMagic, 4);
put_u16(buf_.data() + 4, version);
}
void reset() {
pos_ = kHeaderSize;
root_ = 0;
}
inline ObjectBuilder start_object(std::int64_t data_size);
inline ListBuilder start_list(std::int64_t elem_size);
std::int64_t write_bytes(std::span<const std::uint8_t> data) {
if (data.empty()) return 0;
align(kAlignment);
const std::int64_t off = pos_;
grow(data.size());
std::memcpy(buf_.data() + pos_, data.data(), data.size());
pos_ += std::int64_t(data.size());
return off;
}
std::int64_t write_text(std::string_view s) {
return write_bytes({reinterpret_cast<const std::uint8_t*>(s.data()), s.size()});
}
// finish stamps the root offset and the total size, and hands back the
// message.
std::vector<std::uint8_t> finish() {
put_u32(buf_.data() + 8, std::uint32_t(root_));
put_u32(buf_.data() + 12, std::uint32_t(pos_));
return std::vector<std::uint8_t>(buf_.begin(), buf_.begin() + pos_);
}
std::vector<std::uint8_t> finish_with_flags(std::uint16_t flags) {
put_u16(buf_.data() + 6, flags);
return finish();
}
private:
friend class ObjectBuilder;
friend class ListBuilder;
void grow(std::size_t n) {
if (std::size_t(pos_) + n <= buf_.size()) return;
std::size_t cap = buf_.size() * 2;
if (cap < std::size_t(pos_) + n) cap = std::size_t(pos_) + n;
buf_.resize(cap, 0);
}
void align(std::int64_t alignment) {
const std::int64_t padding = (alignment - (pos_ % alignment)) % alignment;
grow(std::size_t(padding));
for (std::int64_t i = 0; i < padding; ++i) buf_[std::size_t(pos_++)] = 0;
}
std::vector<std::uint8_t> buf_;
std::int64_t pos_ = 0;
std::int64_t root_ = 0;
};
// ObjectBuilder writes one object's fixed section, and the tails its pointer
// fields name. It is a value: it holds where the object started, nothing else.
class ObjectBuilder {
public:
ObjectBuilder() = default;
ObjectBuilder(Builder* b, std::int64_t start, std::int64_t data_size)
: b_(b), start_(start), data_size_(data_size) {}
void set_bool(std::int64_t field, bool v) { set_u8(field, v ? 1 : 0); }
void set_u8(std::int64_t field, std::uint8_t v) {
ensure_field(field + 1);
b_->buf_[std::size_t(start_ + field)] = v;
}
void set_u16(std::int64_t field, std::uint16_t v) {
ensure_field(field + 2);
put_u16(b_->buf_.data() + start_ + field, v);
}
void set_u32(std::int64_t field, std::uint32_t v) {
ensure_field(field + 4);
put_u32(b_->buf_.data() + start_ + field, v);
}
void set_u64(std::int64_t field, std::uint64_t v) {
ensure_field(field + 8);
put_u64(b_->buf_.data() + start_ + field, v);
}
void set_i8(std::int64_t field, std::int8_t v) { set_u8(field, std::uint8_t(v)); }
void set_i16(std::int64_t field, std::int16_t v) { set_u16(field, std::uint16_t(v)); }
void set_i32(std::int64_t field, std::int32_t v) { set_u32(field, std::uint32_t(v)); }
void set_i64(std::int64_t field, std::int64_t v) { set_u64(field, std::uint64_t(v)); }
void set_f32(std::int64_t field, float v) { set_u32(field, std::bit_cast<std::uint32_t>(v)); }
void set_f64(std::int64_t field, double v) { set_u64(field, std::bit_cast<std::uint64_t>(v)); }
// set_bytes_fixed copies v IN PLACE inside the fixed section — a declared
// fixed-width byte field. An empty value is a no-op: the slot keeps its
// zeroes.
void set_bytes_fixed(std::int64_t field, std::span<const std::uint8_t> v) {
if (v.empty()) return;
ensure_field(field + std::int64_t(v.size()));
std::memcpy(b_->buf_.data() + start_ + field, v.data(), v.size());
}
// set_bytes writes a variable-length tail: the {relOffset u32, length u32}
// pair in the fixed section, and the payload appended at the cursor. The
// fixed section is already reserved by start_object, so the cursor IS the
// tail and the pointer can be patched immediately.
void set_bytes(std::int64_t field, std::span<const std::uint8_t> v) {
const std::int64_t cell = start_ + field;
if (v.empty()) {
put_u32(b_->buf_.data() + cell, 0);
put_u32(b_->buf_.data() + cell + 4, 0);
return;
}
const std::int64_t data_pos = b_->pos_;
b_->grow(v.size());
std::memcpy(b_->buf_.data() + b_->pos_, v.data(), v.size());
b_->pos_ += std::int64_t(v.size());
// grow may have moved the buffer; derive the cell after it.
std::uint8_t* p = b_->buf_.data() + cell;
put_u32(p, std::uint32_t(std::int32_t(data_pos - cell)));
put_u32(p + 4, std::uint32_t(v.size()));
}
void set_text(std::int64_t field, std::string_view v) {
set_bytes(field, {reinterpret_cast<const std::uint8_t*>(v.data()), v.size()});
}
void set_object(std::int64_t field, std::int64_t obj_offset) {
ensure_field(field + 4);
std::uint8_t* p = b_->buf_.data() + start_ + field;
if (obj_offset == 0) {
put_u32(p, 0);
return;
}
put_u32(p, std::uint32_t(std::int32_t(obj_offset - (start_ + field))));
}
void set_list(std::int64_t field, std::int64_t list_offset, std::int64_t length) {
ensure_field(field + 8);
std::uint8_t* p = b_->buf_.data() + start_ + field;
if (list_offset == 0 || length == 0) {
put_u32(p, 0);
put_u32(p + 4, 0);
return;
}
put_u32(p, std::uint32_t(std::int32_t(list_offset - (start_ + field))));
put_u32(p + 4, std::uint32_t(length));
}
void reserve_fixed(std::int64_t data_size) { ensure_field(data_size); }
std::int64_t finish() const { return start_; }
std::int64_t finish_as_root() const {
b_->root_ = start_;
return start_;
}
std::int64_t data_size() const { return data_size_; }
private:
void ensure_field(std::int64_t end_offset) {
const std::int64_t needed = start_ + end_offset;
if (needed <= b_->pos_) return;
b_->grow(std::size_t(needed - b_->pos_));
for (std::int64_t i = b_->pos_; i < needed; ++i) b_->buf_[std::size_t(i)] = 0;
b_->pos_ = needed;
}
Builder* b_ = nullptr;
std::int64_t start_ = 0;
std::int64_t data_size_ = 0;
};
// ListBuilder appends elements at the cursor. The stride is implicit in which
// add is called.
//
// add_bytes counts BYTES, not elements. That is the wire's own accounting for a
// byte run, and a writer laying down fixed-stride records supplies the real
// element count to set_list itself — keeping the asymmetry is deliberate,
// because "fixing" it here would silently change the length field on the wire.
class ListBuilder {
public:
ListBuilder() = default;
ListBuilder(Builder* b, std::int64_t start) : b_(b), start_(start) {}
void add_u8(std::uint8_t v) {
b_->grow(1);
b_->buf_[std::size_t(b_->pos_++)] = v;
++count_;
}
void add_u32(std::uint32_t v) {
b_->grow(4);
put_u32(b_->buf_.data() + b_->pos_, v);
b_->pos_ += 4;
++count_;
}
void add_u64(std::uint64_t v) {
b_->grow(8);
put_u64(b_->buf_.data() + b_->pos_, v);
b_->pos_ += 8;
++count_;
}
void add_bytes(std::span<const std::uint8_t> data) {
b_->grow(data.size());
if (!data.empty()) std::memcpy(b_->buf_.data() + b_->pos_, data.data(), data.size());
b_->pos_ += std::int64_t(data.size());
count_ += std::int64_t(data.size());
}
// add_object_ptr appends a 4-byte SIGNED relative pointer to an object at
// absolute position target_pos (0 = a null element). The target usually
// PRECEDES the slot, because a builder writes the objects first and the
// pointer array after.
void add_object_ptr(std::int64_t target_pos) {
b_->grow(4);
std::uint8_t* p = b_->buf_.data() + b_->pos_;
if (target_pos == 0) {
put_u32(p, 0);
} else {
put_u32(p, std::uint32_t(std::int32_t(target_pos - b_->pos_)));
}
b_->pos_ += 4;
++count_;
}
// finish returns (offset, count) — the pair set_list takes.
std::pair<std::int64_t, std::int64_t> finish() const { return {start_, count_}; }
std::int64_t offset() const { return start_; }
std::int64_t count() const { return count_; }
private:
Builder* b_ = nullptr;
std::int64_t start_ = 0;
std::int64_t count_ = 0;
};
inline ObjectBuilder Builder::start_object(std::int64_t data_size) {
align(kAlignment);
ObjectBuilder ob(this, pos_, data_size);
ob.reserve_fixed(data_size);
return ob;
}
inline ListBuilder Builder::start_list(std::int64_t /*elem_size*/) {
align(kAlignment);
return ListBuilder(this, pos_);
}
`
+606
View File
@@ -0,0 +1,606 @@
package zip
// The C++ ZAP runtime is only worth generating if it writes the SAME BYTES the
// Go runtime writes. This test holds that directly: it emits the header,
// compiles a C++ driver against it, and compares the driver's buffers with
// buffers the Go runtime builds from the same calls in the same order.
//
// The two scripts below — cppZapScript and the Go cases in zapWireCases — are
// deliberately parallel, statement for statement. Keeping them in sync by hand
// is the point: when someone changes one side of the codec, the diff shows up
// here as a failing vector rather than as a fork on the wire.
import (
"encoding/hex"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
luxzap "github.com/luxfi/zap"
)
func fill(n int, seed byte) []byte {
b := make([]byte, n)
for i := range b {
b[i] = seed + byte(i*7)
}
return b
}
// zapWireCases is the Go half of the differential: one named buffer per case.
func zapWireCases() []struct {
name string
bytes []byte
} {
type kase = struct {
name string
bytes []byte
}
var out []kase
add := func(name string, f func() []byte) { out = append(out, kase{name, f()}) }
add("scalar", func() []byte {
b := luxzap.NewBuilder(256)
ob := b.StartObject(8)
ob.SetUint64(0, 0x0102030405060708)
ob.FinishAsRoot()
return b.Finish()
})
add("mixed", func() []byte {
b := luxzap.NewBuilder(256)
ob := b.StartObject(24)
ob.SetUint8(0, 7)
ob.SetUint16(2, 0xBEEF)
ob.SetUint32(4, 0xDEADBEEF)
ob.SetUint64(8, 0x1122334455667788)
ob.SetBool(16, true)
ob.SetBool(17, false)
ob.FinishAsRoot()
return b.Finish()
})
add("signed_and_float", func() []byte {
b := luxzap.NewBuilder(256)
ob := b.StartObject(32)
ob.SetInt8(0, -3)
ob.SetInt16(2, -300)
ob.SetInt32(4, -70000)
ob.SetInt64(8, -5000000000)
ob.SetFloat32(16, -1.5)
ob.SetFloat64(24, 3.141592653589793)
ob.FinishAsRoot()
return b.Finish()
})
add("bytes_tail", func() []byte {
b := luxzap.NewBuilder(256)
ob := b.StartObject(16)
ob.SetBytes(0, []byte("hello"))
ob.SetBytes(8, nil)
ob.FinishAsRoot()
return b.Finish()
})
add("two_tails_and_fixed", func() []byte {
b := luxzap.NewBuilder(256)
ob := b.StartObject(24)
ob.SetBytes(0, fill(11, 0x40))
ob.SetText(8, "zap")
ob.SetBytesFixed(16, fill(8, 0x90))
ob.FinishAsRoot()
return b.Finish()
})
add("grow", func() []byte {
b := luxzap.NewBuilder(64)
ob := b.StartObject(16)
ob.SetBytes(0, fill(1000, 0x11))
ob.SetUint64(8, 0xFFFFFFFFFFFFFFFF)
ob.FinishAsRoot()
return b.Finish()
})
add("child_first", func() []byte {
b := luxzap.NewBuilder(256)
child := b.StartObject(8)
child.SetUint64(0, 42)
co := child.Finish()
parent := b.StartObject(8)
parent.SetObject(0, co)
parent.SetUint32(4, 9)
parent.FinishAsRoot()
return b.Finish()
})
add("null_pointers", func() []byte {
b := luxzap.NewBuilder(256)
ob := b.StartObject(24)
ob.SetObject(0, 0)
ob.SetList(4, 0, 0)
ob.SetList(12, 64, 0)
ob.SetBytes(16, nil)
ob.FinishAsRoot()
return b.Finish()
})
add("list_u8", func() []byte {
b := luxzap.NewBuilder(256)
lb := b.StartList(1)
for i := 0; i < 5; i++ {
lb.AddUint8(byte(i + 1))
}
off, n := lb.Finish()
ob := b.StartObject(8)
ob.SetList(0, off, n)
ob.FinishAsRoot()
return b.Finish()
})
add("list_u32_u64", func() []byte {
b := luxzap.NewBuilder(256)
l1 := b.StartList(4)
l1.AddUint32(1)
l1.AddUint32(0xFFFFFFFF)
o1, n1 := l1.Finish()
l2 := b.StartList(8)
l2.AddUint64(0x0102030405060708)
l2.AddUint64(7)
l2.AddUint64(0)
o2, n2 := l2.Finish()
ob := b.StartObject(16)
ob.SetList(0, o1, n1)
ob.SetList(8, o2, n2)
ob.FinishAsRoot()
return b.Finish()
})
add("list_bytes", func() []byte {
b := luxzap.NewBuilder(256)
lb := b.StartList(1)
lb.AddBytes(fill(9, 0x21))
lb.AddBytes(fill(4, 0x55))
off, n := lb.Finish()
ob := b.StartObject(8)
ob.SetList(0, off, n)
ob.FinishAsRoot()
return b.Finish()
})
add("list_object_ptr", func() []byte {
b := luxzap.NewBuilder(256)
var pos []int
for i := 0; i < 3; i++ {
ob := b.StartObject(16)
ob.SetUint64(0, uint64(i+1))
ob.SetBytes(8, fill(3+i, byte(0x30+i)))
pos = append(pos, ob.Finish())
}
lb := b.StartList(4)
lb.AddObjectPtr(pos[0])
lb.AddObjectPtr(0)
lb.AddObjectPtr(pos[1])
lb.AddObjectPtr(pos[2])
off, n := lb.Finish()
root := b.StartObject(8)
root.SetList(0, off, n)
root.FinishAsRoot()
return b.Finish()
})
add("write_bytes_alignment", func() []byte {
b := luxzap.NewBuilder(256)
o1 := b.WriteBytes(fill(3, 0x01))
o2 := b.WriteText("abcde")
ob := b.StartObject(16)
ob.SetList(0, o1, 3)
ob.SetList(8, o2, 5)
ob.FinishAsRoot()
return b.Finish()
})
add("reserve_fixed", func() []byte {
b := luxzap.NewBuilder(256)
ob := b.StartObject(4)
ob.SetUint32(0, 0x0A0B0C0D)
ob.ReserveFixed(32)
ob.SetUint64(24, 5)
ob.FinishAsRoot()
return b.Finish()
})
add("flags", func() []byte {
b := luxzap.NewBuilder(256)
ob := b.StartObject(8)
ob.SetUint64(0, 1)
ob.FinishAsRoot()
return b.FinishWithFlags(luxzap.FlagSigned | luxzap.FlagCompressed)
})
add("version1", func() []byte {
b := luxzap.NewBuilderV1(256)
ob := b.StartObject(8)
ob.SetUint64(0, 0x0908070605040302)
ob.FinishAsRoot()
return b.Finish()
})
add("no_root", func() []byte {
b := luxzap.NewBuilder(256)
ob := b.StartObject(8)
ob.SetUint64(0, 3)
ob.Finish()
return b.Finish()
})
return out
}
// cppZapScript is the C++ half: the same calls, in the same order, against the
// emitted header.
const cppZapScript = `
#include "zap.hpp"
#include <cstdio>
#include <span>
#include <string>
#include <vector>
using namespace lux::zap;
static std::vector<std::uint8_t> fill(int n, std::uint8_t seed) {
std::vector<std::uint8_t> v(std::size_t(n), std::uint8_t(0));
for (int i = 0; i < n; ++i) v[std::size_t(i)] = std::uint8_t(seed + std::uint8_t(i * 7));
return v;
}
static std::span<const std::uint8_t> sp(const std::vector<std::uint8_t>& v) { return {v.data(), v.size()}; }
static void emit(const char* name, const std::vector<std::uint8_t>& b) {
static const char* d = "0123456789abcdef";
std::string h;
h.reserve(b.size() * 2);
for (std::uint8_t c : b) { h.push_back(d[c >> 4]); h.push_back(d[c & 0xF]); }
std::printf("%s\t%s\n", name, h.c_str());
}
int main() {
{
Builder b(256);
auto ob = b.start_object(8);
ob.set_u64(0, 0x0102030405060708ULL);
ob.finish_as_root();
emit("scalar", b.finish());
}
{
Builder b(256);
auto ob = b.start_object(24);
ob.set_u8(0, 7);
ob.set_u16(2, 0xBEEF);
ob.set_u32(4, 0xDEADBEEF);
ob.set_u64(8, 0x1122334455667788ULL);
ob.set_bool(16, true);
ob.set_bool(17, false);
ob.finish_as_root();
emit("mixed", b.finish());
}
{
Builder b(256);
auto ob = b.start_object(32);
ob.set_i8(0, -3);
ob.set_i16(2, -300);
ob.set_i32(4, -70000);
ob.set_i64(8, -5000000000LL);
ob.set_f32(16, -1.5f);
ob.set_f64(24, 3.141592653589793);
ob.finish_as_root();
emit("signed_and_float", b.finish());
}
{
Builder b(256);
auto ob = b.start_object(16);
const char* hello = "hello";
ob.set_bytes(0, {reinterpret_cast<const std::uint8_t*>(hello), 5});
ob.set_bytes(8, {});
ob.finish_as_root();
emit("bytes_tail", b.finish());
}
{
Builder b(256);
auto ob = b.start_object(24);
auto a = fill(11, 0x40);
auto c = fill(8, 0x90);
ob.set_bytes(0, sp(a));
ob.set_text(8, "zap");
ob.set_bytes_fixed(16, sp(c));
ob.finish_as_root();
emit("two_tails_and_fixed", b.finish());
}
{
Builder b(64);
auto ob = b.start_object(16);
auto a = fill(1000, 0x11);
ob.set_bytes(0, sp(a));
ob.set_u64(8, 0xFFFFFFFFFFFFFFFFULL);
ob.finish_as_root();
emit("grow", b.finish());
}
{
Builder b(256);
auto child = b.start_object(8);
child.set_u64(0, 42);
auto co = child.finish();
auto parent = b.start_object(8);
parent.set_object(0, co);
parent.set_u32(4, 9);
parent.finish_as_root();
emit("child_first", b.finish());
}
{
Builder b(256);
auto ob = b.start_object(24);
ob.set_object(0, 0);
ob.set_list(4, 0, 0);
ob.set_list(12, 64, 0);
ob.set_bytes(16, {});
ob.finish_as_root();
emit("null_pointers", b.finish());
}
{
Builder b(256);
auto lb = b.start_list(1);
for (int i = 0; i < 5; ++i) lb.add_u8(std::uint8_t(i + 1));
auto [off, n] = lb.finish();
auto ob = b.start_object(8);
ob.set_list(0, off, n);
ob.finish_as_root();
emit("list_u8", b.finish());
}
{
Builder b(256);
auto l1 = b.start_list(4);
l1.add_u32(1);
l1.add_u32(0xFFFFFFFF);
auto [o1, n1] = l1.finish();
auto l2 = b.start_list(8);
l2.add_u64(0x0102030405060708ULL);
l2.add_u64(7);
l2.add_u64(0);
auto [o2, n2] = l2.finish();
auto ob = b.start_object(16);
ob.set_list(0, o1, n1);
ob.set_list(8, o2, n2);
ob.finish_as_root();
emit("list_u32_u64", b.finish());
}
{
Builder b(256);
auto lb = b.start_list(1);
auto a = fill(9, 0x21);
auto c = fill(4, 0x55);
lb.add_bytes(sp(a));
lb.add_bytes(sp(c));
auto [off, n] = lb.finish();
auto ob = b.start_object(8);
ob.set_list(0, off, n);
ob.finish_as_root();
emit("list_bytes", b.finish());
}
{
Builder b(256);
std::vector<std::int64_t> pos;
std::vector<std::vector<std::uint8_t>> keep;
for (int i = 0; i < 3; ++i) {
auto ob = b.start_object(16);
ob.set_u64(0, std::uint64_t(i + 1));
keep.push_back(fill(3 + i, std::uint8_t(0x30 + i)));
ob.set_bytes(8, sp(keep.back()));
pos.push_back(ob.finish());
}
auto lb = b.start_list(4);
lb.add_object_ptr(pos[0]);
lb.add_object_ptr(0);
lb.add_object_ptr(pos[1]);
lb.add_object_ptr(pos[2]);
auto [off, n] = lb.finish();
auto root = b.start_object(8);
root.set_list(0, off, n);
root.finish_as_root();
emit("list_object_ptr", b.finish());
}
{
Builder b(256);
auto a = fill(3, 0x01);
auto o1 = b.write_bytes(sp(a));
auto o2 = b.write_text("abcde");
auto ob = b.start_object(16);
ob.set_list(0, o1, 3);
ob.set_list(8, o2, 5);
ob.finish_as_root();
emit("write_bytes_alignment", b.finish());
}
{
Builder b(256);
auto ob = b.start_object(4);
ob.set_u32(0, 0x0A0B0C0D);
ob.reserve_fixed(32);
ob.set_u64(24, 5);
ob.finish_as_root();
emit("reserve_fixed", b.finish());
}
{
Builder b(256);
auto ob = b.start_object(8);
ob.set_u64(0, 1);
ob.finish_as_root();
emit("flags", b.finish_with_flags(kFlagSigned | kFlagCompressed));
}
{
Builder b(256, kVersion1);
auto ob = b.start_object(8);
ob.set_u64(0, 0x0908070605040302ULL);
ob.finish_as_root();
emit("version1", b.finish());
}
{
Builder b(256);
auto ob = b.start_object(8);
ob.set_u64(0, 3);
ob.finish();
emit("no_root", b.finish());
}
return 0;
}
`
// TestCppZapKeepsTheWire is the promise the generator makes: what it emits
// writes the same bytes the Go runtime writes.
func TestCppZapKeepsTheWire(t *testing.T) {
cxx := cppCompiler(t)
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "zap.hpp"), CppZap("lux::zap"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "driver.cpp"), []byte(cppZapScript), 0o644); err != nil {
t.Fatal(err)
}
bin := filepath.Join(dir, "driver")
build := exec.Command(cxx, "-std=c++23", "-O1", "-Wall", "-Wextra", "-Werror",
"-I", dir, "-o", bin, filepath.Join(dir, "driver.cpp"))
if out, err := build.CombinedOutput(); err != nil {
t.Fatalf("compiling the emitted header failed: %v\n%s", err, out)
}
out, err := exec.Command(bin).Output()
if err != nil {
t.Fatalf("running the C++ driver failed: %v", err)
}
got := map[string]string{}
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
name, hexed, ok := strings.Cut(line, "\t")
if !ok {
t.Fatalf("driver printed an unparsable line: %q", line)
}
got[name] = hexed
}
cases := zapWireCases()
if len(got) != len(cases) {
t.Fatalf("driver emitted %d vectors, Go built %d", len(got), len(cases))
}
for _, c := range cases {
want := hex.EncodeToString(c.bytes)
if got[c.name] != want {
t.Errorf("%s: the C++ runtime and the Go runtime disagree on the wire\n go %s\n cpp %s",
c.name, want, got[c.name])
}
}
}
// TestCppZapReadsWhatGoWrote closes the loop the other way: the emitted reader
// must answer the same values over a buffer the Go builder produced.
func TestCppZapReadsWhatGoWrote(t *testing.T) {
cxx := cppCompiler(t)
b := luxzap.NewBuilder(256)
tail := fill(11, 0x40)
child := b.StartObject(8)
child.SetUint64(0, 0xCAFEBABEDEADBEEF)
co := child.Finish()
lb := b.StartList(4)
lb.AddUint32(3)
lb.AddUint32(0xFFFFFFFF)
lo, ln := lb.Finish()
ob := b.StartObject(48)
ob.SetUint8(0, 9)
ob.SetUint64(8, 0x1122334455667788)
ob.SetBytes(16, tail)
ob.SetText(24, "zap")
ob.SetObject(32, co)
ob.SetList(36, lo, ln)
ob.SetBytesFixed(44, []byte{1, 2, 3, 4})
ob.FinishAsRoot()
msg := b.Finish()
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "zap.hpp"), CppZap("lux::zap"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "reader.cpp"), []byte(cppZapReadScript), 0o644); err != nil {
t.Fatal(err)
}
bin := filepath.Join(dir, "reader")
if out, err := exec.Command(cxx, "-std=c++23", "-O1", "-Wall", "-Wextra", "-Werror",
"-I", dir, "-o", bin, filepath.Join(dir, "reader.cpp")).CombinedOutput(); err != nil {
t.Fatalf("compiling the reader failed: %v\n%s", err, out)
}
out, err := exec.Command(bin, hex.EncodeToString(msg)).Output()
if err != nil {
t.Fatalf("running the reader failed: %v", err)
}
// The same reads, answered by the Go runtime the buffer came from.
gm, err := luxzap.Parse(msg)
if err != nil {
t.Fatal(err)
}
r := gm.Root()
gl := r.ListStride(36, 4)
u := func(v uint64) string { return strconv.FormatUint(v, 10) }
want := strings.Join([]string{
"version " + u(uint64(gm.Version())),
"u8 " + u(uint64(r.Uint8(0))),
"u64 " + u(r.Uint64(8)),
"bytes " + hex.EncodeToString(r.Bytes(16)),
"text " + r.Text(24),
"child " + u(r.Object(32).Uint64(0)),
"list " + u(uint64(gl.Len())) + " " + u(uint64(gl.Uint32(0))) + " " + u(uint64(gl.Uint32(1))),
"fixed " + hex.EncodeToString(r.BytesFixedSlice(44, 4)),
}, "\n") + "\n"
if string(out) != want {
t.Errorf("the emitted reader answered differently than the Go runtime\n got %q\n want %q", out, want)
}
}
const cppZapReadScript = `
#include "zap.hpp"
#include <cstdio>
#include <string>
#include <vector>
using namespace lux::zap;
static std::string hexed(std::span<const std::uint8_t> b) {
static const char* d = "0123456789abcdef";
std::string h;
for (std::uint8_t c : b) { h.push_back(d[c >> 4]); h.push_back(d[c & 0xF]); }
return h;
}
int main(int argc, char** argv) {
if (argc < 2) return 2;
std::vector<std::uint8_t> buf;
auto nib = [](char c) { return c <= '9' ? c - '0' : (c | 32) - 'a' + 10; };
for (const char* p = argv[1]; p[0] && p[1]; p += 2) {
buf.push_back(std::uint8_t(nib(p[0]) * 16 + nib(p[1])));
}
Message m;
std::string err;
if (!Message::parse({buf.data(), buf.size()}, &m, &err)) {
std::printf("parse failed: %s\n", err.c_str());
return 1;
}
const auto r = m.root();
const auto l = r.list_stride(36, 4);
std::printf("version %u\n", unsigned(m.version()));
std::printf("u8 %u\n", unsigned(r.u8(0)));
std::printf("u64 %llu\n", static_cast<unsigned long long>(r.u64(8)));
std::printf("bytes %s\n", hexed(r.bytes(16)).c_str());
std::printf("text %.*s\n", int(r.text(24).size()), r.text(24).data());
std::printf("child %llu\n", static_cast<unsigned long long>(r.object(32).u64(0)));
std::printf("list %lld %u %u\n", static_cast<long long>(l.len()), l.u32(0), l.u32(1));
std::printf("fixed %s\n", hexed(r.bytes_fixed_slice(44, 4)).c_str());
return 0;
}
`
func cppCompiler(t *testing.T) string {
t.Helper()
for _, c := range []string{"g++", "clang++"} {
if p, err := exec.LookPath(c); err == nil {
return p
}
}
t.Skip("no C++ compiler on this machine; the emitted runtime cannot be checked here")
return ""
}