mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-30 05:09:32 +00:00
feat(ffi): scalar type mappings + seq[byte] byte-string codec (#99)
This commit is contained in:
parent
7362bfd212
commit
64a332ca8b
@ -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)) {
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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 (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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user