feat: typed CBOR requests for messaging and channel send

send and channel_send took a JSON string the Nim side parsed with
parseJson — JSON as the argument-passing mechanism, which defeats the
point of the CBOR ABI. They now take typed {.ffi.} request objects
(SendRequest, ChannelSendRequest) that ride the wire as CBOR, so the
arguments are structured on both sides with no JSON parsing.

payload is a base64 string rather than seq[byte]: nim-ffi's Rust codegen
maps seq[byte] to Vec<u8> without the serde_bytes annotation a CBOR byte
string needs, so native bytes do not round-trip (ciborium emits an array,
the Nim decoder wants a byte string). A base64 string value inside the
CBOR struct is the documented fallback until that codegen gap is fixed
upstream. create_node keeps its JSON string param — there the JSON is the
value, not an argument encoding.

Fields are unexported to keep the export marker out of the generated
bindings, and the reading proc lives in the same module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ivan FB 2026-07-16 14:48:52 +02:00
parent f2801b86a2
commit 0291318c95
No known key found for this signature in database
GPG Key ID: DF0C67A04C543270
5 changed files with 158 additions and 72 deletions

View File

@ -62,6 +62,15 @@ typedef struct {
NimFfiStr messageHash;
WakuMessagePayload wakuMessage;
} ReceivedMessagePayload;
typedef struct {
NimFfiStr contentTopic;
NimFfiStr payload;
bool ephemeral;
} SendRequest;
typedef struct {
NimFfiStr payload;
bool ephemeral;
} ChannelSendRequest;
typedef struct {
NimFfiStr configJson;
} LogosdeliveryCreateNodeCtorReq;
@ -78,7 +87,7 @@ typedef struct {
NimFfiStr contentTopicStr;
} LogosdeliveryUnsubscribeReq;
typedef struct {
NimFfiStr messageJson;
SendRequest req;
} LogosdeliverySendReq;
typedef struct {
char _nimffi_empty; /* C forbids empty structs */
@ -228,7 +237,7 @@ typedef struct {
} LogosdeliveryChannelCreateReq;
typedef struct {
NimFfiStr channelIdStr;
NimFfiStr messageJson;
ChannelSendRequest req;
} LogosdeliveryChannelSendReq;
typedef struct {
NimFfiStr channelIdStr;
@ -755,6 +764,88 @@ static inline void logosdelivery_free_ReceivedMessagePayload(ReceivedMessagePayl
nimffi_free_str(&v->messageHash);
logosdelivery_free_WakuMessagePayload(&v->wakuMessage);
}
static inline CborError logosdelivery_enc_SendRequest(
CborEncoder* e, const SendRequest* v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(e, &m, 3);
if (err) return err;
err = cbor_encode_text_stringz(&m, "contentTopic");
if (err) return err;
err = nimffi_enc_str(&m, &v->contentTopic);
if (err) return err;
err = cbor_encode_text_stringz(&m, "payload");
if (err) return err;
err = nimffi_enc_str(&m, &v->payload);
if (err) return err;
err = cbor_encode_text_stringz(&m, "ephemeral");
if (err) return err;
err = nimffi_enc_bool(&m, &v->ephemeral);
if (err) return err;
return cbor_encoder_close_container(e, &m);
}
static inline CborError logosdelivery_dec_SendRequest(
CborValue* it, SendRequest* out) {
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(it, "contentTopic", &field);
if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = nimffi_dec_str(&field, &out->contentTopic);
if (err) return err;
err = cbor_value_map_find_value(it, "payload", &field);
if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = nimffi_dec_str(&field, &out->payload);
if (err) return err;
err = cbor_value_map_find_value(it, "ephemeral", &field);
if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = nimffi_dec_bool(&field, &out->ephemeral);
if (err) return err;
return cbor_value_advance(it);
}
static inline void logosdelivery_free_SendRequest(SendRequest* v) {
if (!v) return;
nimffi_free_str(&v->contentTopic);
nimffi_free_str(&v->payload);
}
static inline CborError logosdelivery_enc_ChannelSendRequest(
CborEncoder* e, const ChannelSendRequest* v) {
CborEncoder m;
CborError err = cbor_encoder_create_map(e, &m, 2);
if (err) return err;
err = cbor_encode_text_stringz(&m, "payload");
if (err) return err;
err = nimffi_enc_str(&m, &v->payload);
if (err) return err;
err = cbor_encode_text_stringz(&m, "ephemeral");
if (err) return err;
err = nimffi_enc_bool(&m, &v->ephemeral);
if (err) return err;
return cbor_encoder_close_container(e, &m);
}
static inline CborError logosdelivery_dec_ChannelSendRequest(
CborValue* it, ChannelSendRequest* out) {
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(it, "payload", &field);
if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = nimffi_dec_str(&field, &out->payload);
if (err) return err;
err = cbor_value_map_find_value(it, "ephemeral", &field);
if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = nimffi_dec_bool(&field, &out->ephemeral);
if (err) return err;
return cbor_value_advance(it);
}
static inline void logosdelivery_free_ChannelSendRequest(ChannelSendRequest* v) {
if (!v) return;
nimffi_free_str(&v->payload);
}
static inline CborError logosdelivery_enc_LogosdeliveryCreateNodeCtorReq(
CborEncoder* e, const LogosdeliveryCreateNodeCtorReq* v) {
CborEncoder m;
@ -869,9 +960,9 @@ static inline CborError logosdelivery_enc_LogosdeliverySendReq(
CborEncoder m;
CborError err = cbor_encoder_create_map(e, &m, 1);
if (err) return err;
err = cbor_encode_text_stringz(&m, "messageJson");
err = cbor_encode_text_stringz(&m, "req");
if (err) return err;
err = nimffi_enc_str(&m, &v->messageJson);
err = logosdelivery_enc_SendRequest(&m, &v->req);
if (err) return err;
return cbor_encoder_close_container(e, &m);
}
@ -880,16 +971,16 @@ static inline CborError logosdelivery_dec_LogosdeliverySendReq(
if (!cbor_value_is_map(it)) return CborErrorImproperValue;
CborValue field;
CborError err;
err = cbor_value_map_find_value(it, "messageJson", &field);
err = cbor_value_map_find_value(it, "req", &field);
if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = nimffi_dec_str(&field, &out->messageJson);
err = logosdelivery_dec_SendRequest(&field, &out->req);
if (err) return err;
return cbor_value_advance(it);
}
static inline void logosdelivery_free_LogosdeliverySendReq(LogosdeliverySendReq* v) {
if (!v) return;
nimffi_free_str(&v->messageJson);
logosdelivery_free_SendRequest(&v->req);
}
static inline CborError logosdelivery_enc_LogosdeliveryGetAvailableNodeInfoIdsReq(
CborEncoder* e, const LogosdeliveryGetAvailableNodeInfoIdsReq* v) {
@ -2015,9 +2106,9 @@ static inline CborError logosdelivery_enc_LogosdeliveryChannelSendReq(
if (err) return err;
err = nimffi_enc_str(&m, &v->channelIdStr);
if (err) return err;
err = cbor_encode_text_stringz(&m, "messageJson");
err = cbor_encode_text_stringz(&m, "req");
if (err) return err;
err = nimffi_enc_str(&m, &v->messageJson);
err = logosdelivery_enc_ChannelSendRequest(&m, &v->req);
if (err) return err;
return cbor_encoder_close_container(e, &m);
}
@ -2031,17 +2122,17 @@ static inline CborError logosdelivery_dec_LogosdeliveryChannelSendReq(
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = nimffi_dec_str(&field, &out->channelIdStr);
if (err) return err;
err = cbor_value_map_find_value(it, "messageJson", &field);
err = cbor_value_map_find_value(it, "req", &field);
if (err) return err;
if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;
err = nimffi_dec_str(&field, &out->messageJson);
err = logosdelivery_dec_ChannelSendRequest(&field, &out->req);
if (err) return err;
return cbor_value_advance(it);
}
static inline void logosdelivery_free_LogosdeliveryChannelSendReq(LogosdeliveryChannelSendReq* v) {
if (!v) return;
nimffi_free_str(&v->channelIdStr);
nimffi_free_str(&v->messageJson);
logosdelivery_free_ChannelSendRequest(&v->req);
}
static inline CborError logosdelivery_enc_LogosdeliveryChannelCloseReq(
CborEncoder* e, const LogosdeliveryChannelCloseReq* v) {
@ -3003,10 +3094,10 @@ static void logosdelivery_send_reply_trampoline(int ret, const char* msg, size_t
nimffi_free_str(&out);
free(box);
}
static inline int logosdelivery_ctx_send(const LogosDeliveryCtx* ctx, NimFfiStr messageJson, LogosDeliverySendReplyFn on_reply, void* user_data) {
static inline int logosdelivery_ctx_send(const LogosDeliveryCtx* ctx, const SendRequest* req, LogosDeliverySendReplyFn on_reply, void* user_data) {
LogosdeliverySendReq ffi_req;
memset(&ffi_req, 0, sizeof(ffi_req));
ffi_req.messageJson = messageJson;
ffi_req.req = *req;
uint8_t* req_buf = NULL;
size_t req_len = 0;
char* err = NULL;
@ -5614,11 +5705,11 @@ static void logosdelivery_channel_send_reply_trampoline(int ret, const char* msg
nimffi_free_str(&out);
free(box);
}
static inline int logosdelivery_ctx_channel_send(const LogosDeliveryCtx* ctx, NimFfiStr channelIdStr, NimFfiStr messageJson, LogosDeliveryChannelSendReplyFn on_reply, void* user_data) {
static inline int logosdelivery_ctx_channel_send(const LogosDeliveryCtx* ctx, NimFfiStr channelIdStr, const ChannelSendRequest* req, LogosDeliveryChannelSendReplyFn on_reply, void* user_data) {
LogosdeliveryChannelSendReq ffi_req;
memset(&ffi_req, 0, sizeof(ffi_req));
ffi_req.channelIdStr = channelIdStr;
ffi_req.messageJson = messageJson;
ffi_req.req = *req;
uint8_t* req_buf = NULL;
size_t req_len = 0;
char* err = NULL;

View File

@ -1,4 +1,3 @@
import std/json
import chronos, results, ffi
import
logos_delivery/waku/common/base64,
@ -8,6 +7,12 @@ import
logos_delivery/api/types,
../declare_lib
# Typed CBOR request; `payload` is base64 for the same reason as SendRequest in
# messaging_api. Fields unexported to keep the export marker out of the bindings.
type ChannelSendRequest* {.ffi.} = object
payload: string
ephemeral: bool
proc installEncryption(mechanism: string): Result[void, string] =
## The Encrypt/Decrypt brokers dispatch to a single provider, so this installs
## it process-wide even though the mechanism is named per channel: channels
@ -43,28 +48,18 @@ proc logosdeliveryChannelCreate*(
return ok(string(id))
proc logosdeliveryChannelSend*(
lib: LogosDelivery, channelIdStr: string, messageJson: string
lib: LogosDelivery, channelIdStr: string, req: ChannelSendRequest
): Future[Result[string, string]] {.ffi.} =
## `messageJson` carries `{ "payload": <base64>, "ephemeral": <bool> }`.
requireChannels(lib, "ChannelSend"):
return err(errMsg)
var jsonNode: JsonNode
try:
jsonNode = parseJson(messageJson)
except Exception as e:
return err("Failed to parse channel message JSON: " & e.msg)
if not jsonNode.hasKey("payload"):
return err("Missing payload field")
let payload = base64.decode(Base64String(jsonNode["payload"].getStr())).valueOr:
let payload = base64.decode(Base64String(req.payload)).valueOr:
return err("invalid payload format: " & error)
let ephemeral = jsonNode.getOrDefault("ephemeral").getBool(false)
let requestId = (
await lib.reliableChannelManager.send(ChannelId(channelIdStr), payload, ephemeral)
await lib.reliableChannelManager.send(
ChannelId(channelIdStr), payload, req.ephemeral
)
).valueOr:
return err("ChannelSend failed: " & $error)

View File

@ -1,6 +1,4 @@
import std/[json]
import chronos, results, ffi
import stew/byteutils
import
logos_delivery/waku/common/base64,
logos_delivery/waku/waku,
@ -8,6 +6,17 @@ import
logos_delivery/api/types,
../declare_lib
# A typed CBOR request rather than a JSON blob the proc has to parse. Fields are
# unexported: genBindings copies field names verbatim, so an export marker would
# leak into the generated Rust as `pub payload*`; the proc reads them from this
# same module. `payload` is base64 because nim-ffi's Rust codegen emits seq[byte]
# as Vec<u8> without the serde_bytes annotation CBOR byte strings need, so native
# bytes do not round-trip yet -- a base64 string value does.
type SendRequest* {.ffi.} = object
contentTopic: string
payload: string
ephemeral: bool
proc logosdeliverySubscribe*(
lib: LogosDelivery, contentTopicStr: string
): Future[Result[string, string]] {.ffi.} =
@ -39,39 +48,18 @@ proc logosdeliveryUnsubscribe*(
return ok("")
proc logosdeliverySend*(
lib: LogosDelivery, messageJson: string
lib: LogosDelivery, req: SendRequest
): Future[Result[string, string]] {.ffi.} =
requireMessaging(lib, "Send"):
return err(errMsg)
## Parse the message JSON and send the message
var jsonNode: JsonNode
try:
jsonNode = parseJson(messageJson)
except Exception as e:
return err("Failed to parse message JSON: " & e.msg)
# Extract content topic
if not jsonNode.hasKey("contentTopic"):
return err("Missing contentTopic field")
# ContentTopic is just a string type alias
let contentTopic = ContentTopic(jsonNode["contentTopic"].getStr())
# Extract payload (expect base64 encoded string)
if not jsonNode.hasKey("payload"):
return err("Missing payload field")
let payloadStr = jsonNode["payload"].getStr()
let payload = base64.decode(Base64String(payloadStr)).valueOr:
let payload = base64.decode(Base64String(req.payload)).valueOr:
return err("invalid payload format: " & error)
# Extract ephemeral flag
let ephemeral = jsonNode.getOrDefault("ephemeral").getBool(false)
# Create message envelope
let envelope = MessageEnvelope.init(
contentTopic = contentTopic, payload = payload, ephemeral = ephemeral
contentTopic = ContentTopic(req.contentTopic),
payload = payload,
ephemeral = req.ephemeral,
)
# Send the message via the messaging layer's own API.

View File

@ -586,8 +586,8 @@ impl LogosDeliveryCtx {
decode_cbor::<String>(&raw_bytes)
}
pub fn send(&self, message_json: String) -> Result<String, String> {
let req = LogosdeliverySendReq { message_json };
pub fn send(&self, req: SendRequest) -> Result<String, String> {
let req = LogosdeliverySendReq { req };
let req_bytes = encode_cbor(&req)?;
let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe {
ffi::logosdelivery_send(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len())
@ -595,8 +595,8 @@ impl LogosDeliveryCtx {
decode_cbor::<String>(&raw_bytes)
}
pub async fn send_async(&self, message_json: String) -> Result<String, String> {
let req = LogosdeliverySendReq { message_json };
pub async fn send_async(&self, req: SendRequest) -> Result<String, String> {
let req = LogosdeliverySendReq { req };
let req_bytes = encode_cbor(&req)?;
let ptr = self.ptr as usize;
let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe {
@ -1384,8 +1384,8 @@ impl LogosDeliveryCtx {
decode_cbor::<String>(&raw_bytes)
}
pub fn channel_send(&self, channel_id_str: String, message_json: String) -> Result<String, String> {
let req = LogosdeliveryChannelSendReq { channel_id_str, message_json };
pub fn channel_send(&self, channel_id_str: String, req: ChannelSendRequest) -> Result<String, String> {
let req = LogosdeliveryChannelSendReq { channel_id_str, req };
let req_bytes = encode_cbor(&req)?;
let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe {
ffi::logosdelivery_channel_send(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len())
@ -1393,8 +1393,8 @@ impl LogosDeliveryCtx {
decode_cbor::<String>(&raw_bytes)
}
pub async fn channel_send_async(&self, channel_id_str: String, message_json: String) -> Result<String, String> {
let req = LogosdeliveryChannelSendReq { channel_id_str, message_json };
pub async fn channel_send_async(&self, channel_id_str: String, req: ChannelSendRequest) -> Result<String, String> {
let req = LogosdeliveryChannelSendReq { channel_id_str, req };
let req_bytes = encode_cbor(&req)?;
let ptr = self.ptr as usize;
let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe {

View File

@ -102,6 +102,20 @@ pub struct ReceivedMessagePayload {
pub waku_message: WakuMessagePayload,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendRequest {
#[serde(rename = "contentTopic")]
pub content_topic: String,
pub payload: String,
pub ephemeral: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelSendRequest {
pub payload: String,
pub ephemeral: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogosdeliveryCreateNodeCtorReq {
#[serde(rename = "configJson")]
@ -128,8 +142,7 @@ pub struct LogosdeliveryUnsubscribeReq {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogosdeliverySendReq {
#[serde(rename = "messageJson")]
pub message_json: String,
pub req: SendRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -375,8 +388,7 @@ pub struct LogosdeliveryChannelCreateReq {
pub struct LogosdeliveryChannelSendReq {
#[serde(rename = "channelIdStr")]
pub channel_id_str: String,
#[serde(rename = "messageJson")]
pub message_json: String,
pub req: ChannelSendRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]