Context
CBOR encoding is on the hot path for every FFI request and event emission. Current implementations in all wrapper languages do unnecessary allocations and/or copies.
C++ — no-alloc preliminary size counting
cborEncodedSize / cborEncodeInto in the generated .hpp wrappers currently construct a basic_cbor_encoder<CountingSink> without a custom allocator. jsoncons's encoder holds an internal nesting stack (std::vector<stack_item>) that heap-allocates on first use.
Fix: supply a std::pmr::polymorphic_allocator backed by a std::pmr::monotonic_buffer_resource over a stack-local arena. With null_memory_resource() as the upstream (fail-loud on overflow rather than silently falling back to heap), the counting pass becomes truly allocation-free for typical nesting depths.
alignas(std::max_align_t) std::byte arena[512]; // ~21 nesting levels
std::pmr::monotonic_buffer_resource mbr{arena, sizeof(arena),
std::pmr::null_memory_resource()};
std::pmr::polymorphic_allocator<char> alloc{&mbr};
jsoncons::cbor::basic_cbor_encoder<CountingSink,
std::pmr::polymorphic_allocator<char>>
enc{CountingSink{n}, jsoncons::cbor_encode_options{}, alloc};
The arena size covers the encoder's nesting stack only — field count and string data are not relevant, only the maximum structural nesting depth of the encoded type.
Apply to both cborEncodedSize and cborEncodeInto in api_codegen_cbor_hpp.nim.
Nim — reduce allocations and copies in cborEncodeShared
Current cborEncodeShared chain:
memoryOutput() # alloc 1: OutputStream ref + PageBuffers (paged heap)
writeValue(w, v) # streaming writes into pages
s.getOutput(seq[byte]) # alloc 2: consolidates all pages into a new seq[byte]
allocShared0(buf.len) # alloc 3: shared heap destination
copyMem(p, buf[0], n) # copy 2 (pages→seq was copy 1)
3 allocations, 2 copies. The intermediate seq[byte] is a pure intermediary that exists only to reveal the total length before the allocShared0.
Option A — skip the consolidation seq: walk the PageBuffers directly after encoding, sum the page lengths for the total, allocShared0 once, then copy each page span into the shared buffer in a single pass. Eliminates alloc 2 and copy 1.
Option B — unsafeMemoryOutput with a conservative upper bound: compute a compile-time or type-level upper bound on the encoded size, allocShared0(bound) upfront, encode directly via unsafeMemoryOutput(p, bound), trim to actual length. Eliminates alloc 2 and both copies, at the cost of potentially over-allocating.
faststreams already exposes unsafeMemoryOutput(pointer, len) which encodes directly into a caller-supplied buffer — the missing piece for Option B is a safe size bound per type.
Relevant code: brokers/internal/api_cbor_codec.nim — cborEncodeShared.
Other wrapper languages
Check whether the same double-copy / over-allocation pattern exists in:
- Python (
api_codegen_cbor_py.nim): cbor2.dumps() always returns a new bytes object; the copy into the FFI buffer is unavoidable, but verify no extra intermediate is created.
- Rust (
api_codegen_cbor_rust.nim): ciborium serializes into a Vec<u8>; check whether encode_into or a writer-to-slice API exists.
- Go (
api_codegen_cbor_go.nim): fxamacker/cbor encodes into a []byte; check whether NewEncoder(w) with a pre-allocated bytes.Buffer or a direct-write path avoids the intermediate copy.
Context
CBOR encoding is on the hot path for every FFI request and event emission. Current implementations in all wrapper languages do unnecessary allocations and/or copies.
C++ — no-alloc preliminary size counting
cborEncodedSize/cborEncodeIntoin the generated.hppwrappers currently construct abasic_cbor_encoder<CountingSink>without a custom allocator. jsoncons's encoder holds an internal nesting stack (std::vector<stack_item>) that heap-allocates on first use.Fix: supply a
std::pmr::polymorphic_allocatorbacked by astd::pmr::monotonic_buffer_resourceover a stack-local arena. Withnull_memory_resource()as the upstream (fail-loud on overflow rather than silently falling back to heap), the counting pass becomes truly allocation-free for typical nesting depths.The arena size covers the encoder's nesting stack only — field count and string data are not relevant, only the maximum structural nesting depth of the encoded type.
Apply to both
cborEncodedSizeandcborEncodeIntoinapi_codegen_cbor_hpp.nim.Nim — reduce allocations and copies in
cborEncodeSharedCurrent
cborEncodeSharedchain:3 allocations, 2 copies. The intermediate
seq[byte]is a pure intermediary that exists only to reveal the total length before theallocShared0.Option A — skip the consolidation seq: walk the
PageBuffersdirectly after encoding, sum the page lengths for the total,allocShared0once, then copy each page span into the shared buffer in a single pass. Eliminates alloc 2 and copy 1.Option B —
unsafeMemoryOutputwith a conservative upper bound: compute a compile-time or type-level upper bound on the encoded size,allocShared0(bound)upfront, encode directly viaunsafeMemoryOutput(p, bound), trim to actual length. Eliminates alloc 2 and both copies, at the cost of potentially over-allocating.faststreamsalready exposesunsafeMemoryOutput(pointer, len)which encodes directly into a caller-supplied buffer — the missing piece for Option B is a safe size bound per type.Relevant code:
brokers/internal/api_cbor_codec.nim—cborEncodeShared.Other wrapper languages
Check whether the same double-copy / over-allocation pattern exists in:
api_codegen_cbor_py.nim):cbor2.dumps()always returns a newbytesobject; the copy into the FFI buffer is unavoidable, but verify no extra intermediate is created.api_codegen_cbor_rust.nim):ciboriumserializes into aVec<u8>; check whetherencode_intoor a writer-to-slice API exists.api_codegen_cbor_go.nim):fxamacker/cborencodes into a[]byte; check whetherNewEncoder(w)with a pre-allocatedbytes.Bufferor a direct-write path avoids the intermediate copy.