diff --git a/examples/echo/cpp_bindings/echo.hpp b/examples/echo/cpp_bindings/echo.hpp index efcfa2e..c99b799 100644 --- a/examples/echo/cpp_bindings/echo.hpp +++ b/examples/echo/cpp_bindings/echo.hpp @@ -129,6 +129,14 @@ inline CborError encode_cbor(CborEncoder& e, const std::vector& v) { return cbor_encoder_close_container(&e, &arr); } +// `seq[byte]` rides the wire as a CBOR byte string (major type 2), matching +// Nim's cbor_serialization. This non-template overload beats the std::vector +// template in overload resolution, so std::vector fields use it +// automatically. +inline CborError encode_cbor(CborEncoder& e, const std::vector& v) { + return cbor_encode_byte_string(&e, v.data(), v.size()); +} + template inline CborError encode_cbor(CborEncoder& e, const std::optional& v) { if (!v) return cbor_encode_null(&e); @@ -206,6 +214,20 @@ inline CborError decode_cbor(CborValue& it, std::vector& out) { return cbor_value_leave_container(&it, &inner); } +// Counterpart to the byte-string encoder above: decode a CBOR byte string +// (major type 2) back into std::vector. +inline CborError decode_cbor(CborValue& it, std::vector& out) { + if (!cbor_value_is_byte_string(&it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_string_length(&it, &len); + if (err) return err; + out.resize(len); + err = cbor_value_copy_byte_string( + &it, out.empty() ? nullptr : out.data(), &len, nullptr); + if (err) return err; + return cbor_value_advance(&it); +} + template inline CborError decode_cbor(CborValue& it, std::optional& out) { if (cbor_value_is_null(&it)) { diff --git a/examples/timer/cpp_bindings/my_timer.hpp b/examples/timer/cpp_bindings/my_timer.hpp index 8fa440b..19630a8 100644 --- a/examples/timer/cpp_bindings/my_timer.hpp +++ b/examples/timer/cpp_bindings/my_timer.hpp @@ -130,6 +130,14 @@ inline CborError encode_cbor(CborEncoder& e, const std::vector& v) { return cbor_encoder_close_container(&e, &arr); } +// `seq[byte]` rides the wire as a CBOR byte string (major type 2), matching +// Nim's cbor_serialization. This non-template overload beats the std::vector +// template in overload resolution, so std::vector fields use it +// automatically. +inline CborError encode_cbor(CborEncoder& e, const std::vector& v) { + return cbor_encode_byte_string(&e, v.data(), v.size()); +} + template inline CborError encode_cbor(CborEncoder& e, const std::optional& v) { if (!v) return cbor_encode_null(&e); @@ -207,6 +215,20 @@ inline CborError decode_cbor(CborValue& it, std::vector& out) { return cbor_value_leave_container(&it, &inner); } +// Counterpart to the byte-string encoder above: decode a CBOR byte string +// (major type 2) back into std::vector. +inline CborError decode_cbor(CborValue& it, std::vector& out) { + if (!cbor_value_is_byte_string(&it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_string_length(&it, &len); + if (err) return err; + out.resize(len); + err = cbor_value_copy_byte_string( + &it, out.empty() ? nullptr : out.data(), &len, nullptr); + if (err) return err; + return cbor_value_advance(&it); +} + template inline CborError decode_cbor(CborValue& it, std::optional& out) { if (cbor_value_is_null(&it)) { diff --git a/ffi/codegen/cddl.nim b/ffi/codegen/cddl.nim index dece15e..5fa2914 100644 --- a/ffi/codegen/cddl.nim +++ b/ffi/codegen/cddl.nim @@ -31,7 +31,12 @@ proc nimTypeToCddl*(typeName: string): string = let t = typeName.strip() let seqI = innerOf(t, "seq[") if seqI.len > 0: - return "[* " & nimTypeToCddl(seqI) & "]" + let inner = seqI.strip() + if inner == "byte" or inner == "uint8": + # `seq[byte]` rides the wire as a CBOR byte string, matching the Nim + # cbor_serialization writer — reflect that in the schema. + return "bytes" + return "[* " & nimTypeToCddl(inner) & "]" let arrI = innerOf(t, "array[") if arrI.len > 0: # CDDL has no fixed-length array literal as ergonomic as Nim's array; emit @@ -52,7 +57,7 @@ proc nimTypeToCddl*(typeName: string): string = case t of "bool": "bool" of "int", "int64", "int32", "int16", "int8": "int" - of "uint", "uint64", "uint32", "uint16", "uint8": "uint" + of "uint", "uint64", "uint32", "uint16", "uint8", "byte": "uint" of "string", "cstring": "tstr" of "float", "float64": "float64" of "float32": "float32" diff --git a/ffi/codegen/cpp.nim b/ffi/codegen/cpp.nim index d0454c2..65bb325 100644 --- a/ffi/codegen/cpp.nim +++ b/ffi/codegen/cpp.nim @@ -46,8 +46,14 @@ proc nimTypeToCpp*(typeName: string): string = of "string", "cstring": "std::string" of "int", "int64": "int64_t" of "int32": "int32_t" + of "int16": "int16_t" + of "int8": "int8_t" + of "uint", "uint64": "uint64_t" + of "uint32": "uint32_t" + of "uint16": "uint16_t" + of "uint8", "byte": "uint8_t" of "bool": "bool" - of "float": "float" + of "float", "float32": "float" of "float64": "double" of "pointer": CppPtrType else: trimmed diff --git a/ffi/codegen/templates/cpp/cbor_helpers.hpp.tpl b/ffi/codegen/templates/cpp/cbor_helpers.hpp.tpl index 595cc17..ea02efd 100644 --- a/ffi/codegen/templates/cpp/cbor_helpers.hpp.tpl +++ b/ffi/codegen/templates/cpp/cbor_helpers.hpp.tpl @@ -36,6 +36,14 @@ inline CborError encode_cbor(CborEncoder& e, const std::vector& v) { return cbor_encoder_close_container(&e, &arr); } +// `seq[byte]` rides the wire as a CBOR byte string (major type 2), matching +// Nim's cbor_serialization. This non-template overload beats the std::vector +// template in overload resolution, so std::vector fields use it +// automatically. +inline CborError encode_cbor(CborEncoder& e, const std::vector& v) { + return cbor_encode_byte_string(&e, v.data(), v.size()); +} + template inline CborError encode_cbor(CborEncoder& e, const std::optional& v) { if (!v) return cbor_encode_null(&e); @@ -44,17 +52,20 @@ inline CborError encode_cbor(CborEncoder& e, const std::optional& v) { // ── decode_cbor overloads ─────────────────────────────────────────────── -inline CborError decode_cbor(CborValue& it, bool& out) { - if (!cbor_value_is_boolean(&it)) return CborErrorImproperValue; - CborError err = cbor_value_get_boolean(&it, &out); +// After reading a leaf value, the parser must advance past it; both steps +// short-circuit on the same CborError, so they always travel together. +inline CborError advance_if_ok(CborValue& it, CborError err) { if (err) return err; return cbor_value_advance(&it); } + +inline CborError decode_cbor(CborValue& it, bool& out) { + if (!cbor_value_is_boolean(&it)) return CborErrorImproperValue; + return advance_if_ok(it, cbor_value_get_boolean(&it, &out)); +} inline CborError decode_cbor(CborValue& it, int64_t& out) { if (!cbor_value_is_integer(&it)) return CborErrorImproperValue; - CborError err = cbor_value_get_int64_checked(&it, &out); - if (err) return err; - return cbor_value_advance(&it); + return advance_if_ok(it, cbor_value_get_int64_checked(&it, &out)); } inline CborError decode_cbor(CborValue& it, int32_t& out) { int64_t tmp = 0; @@ -65,15 +76,11 @@ inline CborError decode_cbor(CborValue& it, int32_t& out) { } inline CborError decode_cbor(CborValue& it, uint64_t& out) { if (!cbor_value_is_unsigned_integer(&it)) return CborErrorImproperValue; - CborError err = cbor_value_get_uint64(&it, &out); - if (err) return err; - return cbor_value_advance(&it); + return advance_if_ok(it, cbor_value_get_uint64(&it, &out)); } inline CborError decode_cbor(CborValue& it, double& out) { if (cbor_value_is_double(&it)) { - CborError err = cbor_value_get_double(&it, &out); - if (err) return err; - return cbor_value_advance(&it); + return advance_if_ok(it, cbor_value_get_double(&it, &out)); } if (cbor_value_is_float(&it)) { float f = 0.0f; @@ -90,9 +97,8 @@ inline CborError decode_cbor(CborValue& it, std::string& out) { CborError err = cbor_value_get_string_length(&it, &len); if (err) return err; out.resize(len); - err = cbor_value_copy_text_string(&it, out.empty() ? nullptr : &out[0], &len, nullptr); - if (err) return err; - return cbor_value_advance(&it); + return advance_if_ok( + it, cbor_value_copy_text_string(&it, out.empty() ? nullptr : &out[0], &len, nullptr)); } template @@ -113,6 +119,18 @@ inline CborError decode_cbor(CborValue& it, std::vector& out) { return cbor_value_leave_container(&it, &inner); } +// Counterpart to the byte-string encoder above: decode a CBOR byte string +// (major type 2) back into std::vector. +inline CborError decode_cbor(CborValue& it, std::vector& out) { + if (!cbor_value_is_byte_string(&it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_string_length(&it, &len); + if (err) return err; + out.resize(len); + return advance_if_ok( + it, cbor_value_copy_byte_string(&it, out.empty() ? nullptr : out.data(), &len, nullptr)); +} + template inline CborError decode_cbor(CborValue& it, std::optional& out) { if (cbor_value_is_null(&it)) { diff --git a/ffi/ffi_thread.nim b/ffi/ffi_thread.nim index ae5b6d8..a75cff9 100644 --- a/ffi/ffi_thread.nim +++ b/ffi/ffi_thread.nim @@ -128,7 +128,7 @@ proc ffiThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} = reapCompleted() - let gotSignal = await ctx.reqSignal.wait().withTimeout(100.milliseconds) + let gotSignal = await ctx.reqSignal.wait().withTimeout(chronos.milliseconds(100)) if not gotSignal: continue diff --git a/tests/e2e/cpp/test_timer_e2e.cpp b/tests/e2e/cpp/test_timer_e2e.cpp index 1f882a2..2f0dced 100644 --- a/tests/e2e/cpp/test_timer_e2e.cpp +++ b/tests/e2e/cpp/test_timer_e2e.cpp @@ -404,3 +404,33 @@ TEST(TimerE2E, RemoveEventListenerStopsDelivery) { EXPECT_EQ(keptHits.load(), 2); EXPECT_EQ(removedHits.load(), 1) << "removed listener fired after removeEventListener"; } + +// Cross-language byte-string contract: the generated C++ codec must round-trip +// a std::vector as a CBOR byte string (major type 2), byte-for-byte +// identical to what Nim's cbor_serialization emits for `seq[byte]`. The goldens +// below match tests/unit/test_wire_compat.nim and tests/unit/test_serial.nim. +TEST(WireCompat, SeqByteRidesAsByteString) { + const std::vector blob{1, 2, 3}; + auto enc = encodeCborFFI(blob); + ASSERT_FALSE(enc.isErr()) << enc.error(); + // 0x43 = byte string, length 3; then the raw bytes 01 02 03. + const std::vector golden{0x43, 0x01, 0x02, 0x03}; + EXPECT_EQ(enc.value(), golden); + + auto dec = decodeCborFFI>(enc.value()); + ASSERT_FALSE(dec.isErr()) << dec.error(); + EXPECT_EQ(dec.value(), blob); +} + +TEST(WireCompat, EmptySeqByteRidesAsEmptyByteString) { + const std::vector blob{}; + auto enc = encodeCborFFI(blob); + ASSERT_FALSE(enc.isErr()) << enc.error(); + // 0x40 = byte string, length 0. + const std::vector golden{0x40}; + EXPECT_EQ(enc.value(), golden); + + auto dec = decodeCborFFI>(enc.value()); + ASSERT_FALSE(dec.isErr()) << dec.error(); + EXPECT_TRUE(dec.value().empty()); +} diff --git a/tests/unit/test_wire_compat.nim b/tests/unit/test_wire_compat.nim index ddcccec..e6d2f54 100644 --- a/tests/unit/test_wire_compat.nim +++ b/tests/unit/test_wire_compat.nim @@ -33,6 +33,9 @@ type WireWithOption {.ffi.} = object type WireWithVector {.ffi.} = object items: seq[string] +type WireWithBytes {.ffi.} = object + blob: seq[byte] + proc toHex(bytes: openArray[byte]): string = var buf = "" for b in bytes: @@ -106,3 +109,28 @@ suite "wire format — seq[T]": # map(1), "items" -> array(3) of text strings "a", "bb", "ccc": # 83 (array 3) 61 61 ("a") 62 62 62 ("bb") 63 63 63 63 ("ccc") check toHex(bytes) == "a1656974656d7383616162626263636363" + +suite "wire format — seq[byte]": + ## `cbor_serialization` emits `seq[byte]` as a CBOR **byte string** (major + ## type 2), not an array (major type 4). The C++ codegen mirrors this with a + ## `std::vector` overload that uses `cbor_encode_byte_string`. + ## These goldens pin the cross-language contract. + + test "seq[byte] field rides as a CBOR byte string, not an array": + let v = WireWithBytes(blob: @[1'u8, 2'u8, 3'u8]) + let bytes = cborEncode(v) + # map(1): "blob" -> byte-string len 3 (0x43) 01 02 03 + check toHex(bytes) == "a164626c6f6243010203" + # The value is a byte string (0x40–0x5b), never an array (0x80–0x9b). + let valMajor = bytes[6] + check valMajor >= 0x40'u8 + check valMajor <= 0x5b'u8 + let back = cborDecode(bytes, WireWithBytes) + check back.isOk + check back.value.blob == v.blob + + test "empty seq[byte] field rides as byte-string(0)": + let v = WireWithBytes(blob: @[]) + let bytes = cborEncode(v) + # map(1): "blob" -> byte-string len 0 (0x40) + check toHex(bytes) == "a164626c6f6240"