feat(ffi): scalar type mappings + seq[byte] byte-string codec (#99)

This commit is contained in:
Gabriel Cruz 2026-06-25 17:16:21 -03:00 committed by GitHub
parent 7362bfd212
commit 64a332ca8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 150 additions and 19 deletions

View File

@ -129,6 +129,14 @@ inline CborError encode_cbor(CborEncoder& e, const std::vector<T>& 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<T>
// template in overload resolution, so std::vector<std::uint8_t> fields use it
// automatically.
inline CborError encode_cbor(CborEncoder& e, const std::vector<std::uint8_t>& v) {
return cbor_encode_byte_string(&e, v.data(), v.size());
}
template<typename T>
inline CborError encode_cbor(CborEncoder& e, const std::optional<T>& v) {
if (!v) return cbor_encode_null(&e);
@ -206,6 +214,20 @@ inline CborError decode_cbor(CborValue& it, std::vector<T>& 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<std::uint8_t>.
inline CborError decode_cbor(CborValue& it, std::vector<std::uint8_t>& 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<typename T>
inline CborError decode_cbor(CborValue& it, std::optional<T>& out) {
if (cbor_value_is_null(&it)) {

View File

@ -130,6 +130,14 @@ inline CborError encode_cbor(CborEncoder& e, const std::vector<T>& 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<T>
// template in overload resolution, so std::vector<std::uint8_t> fields use it
// automatically.
inline CborError encode_cbor(CborEncoder& e, const std::vector<std::uint8_t>& v) {
return cbor_encode_byte_string(&e, v.data(), v.size());
}
template<typename T>
inline CborError encode_cbor(CborEncoder& e, const std::optional<T>& v) {
if (!v) return cbor_encode_null(&e);
@ -207,6 +215,20 @@ inline CborError decode_cbor(CborValue& it, std::vector<T>& 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<std::uint8_t>.
inline CborError decode_cbor(CborValue& it, std::vector<std::uint8_t>& 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<typename T>
inline CborError decode_cbor(CborValue& it, std::optional<T>& out) {
if (cbor_value_is_null(&it)) {

View File

@ -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"

View File

@ -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

View File

@ -36,6 +36,14 @@ inline CborError encode_cbor(CborEncoder& e, const std::vector<T>& 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<T>
// template in overload resolution, so std::vector<std::uint8_t> fields use it
// automatically.
inline CborError encode_cbor(CborEncoder& e, const std::vector<std::uint8_t>& v) {
return cbor_encode_byte_string(&e, v.data(), v.size());
}
template<typename T>
inline CborError encode_cbor(CborEncoder& e, const std::optional<T>& v) {
if (!v) return cbor_encode_null(&e);
@ -44,17 +52,20 @@ inline CborError encode_cbor(CborEncoder& e, const std::optional<T>& 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<typename T>
@ -113,6 +119,18 @@ inline CborError decode_cbor(CborValue& it, std::vector<T>& 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<std::uint8_t>.
inline CborError decode_cbor(CborValue& it, std::vector<std::uint8_t>& 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<typename T>
inline CborError decode_cbor(CborValue& it, std::optional<T>& out) {
if (cbor_value_is_null(&it)) {

View File

@ -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

View File

@ -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<std::uint8_t> 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<std::uint8_t> 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<std::uint8_t> golden{0x43, 0x01, 0x02, 0x03};
EXPECT_EQ(enc.value(), golden);
auto dec = decodeCborFFI<std::vector<std::uint8_t>>(enc.value());
ASSERT_FALSE(dec.isErr()) << dec.error();
EXPECT_EQ(dec.value(), blob);
}
TEST(WireCompat, EmptySeqByteRidesAsEmptyByteString) {
const std::vector<std::uint8_t> blob{};
auto enc = encodeCborFFI(blob);
ASSERT_FALSE(enc.isErr()) << enc.error();
// 0x40 = byte string, length 0.
const std::vector<std::uint8_t> golden{0x40};
EXPECT_EQ(enc.value(), golden);
auto dec = decodeCborFFI<std::vector<std::uint8_t>>(enc.value());
ASSERT_FALSE(dec.isErr()) << dec.error();
EXPECT_TRUE(dec.value().empty());
}

View File

@ -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<std::uint8_t>` 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 (0x400x5b), never an array (0x800x9b).
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"