From 1f0354f8e2332f201173cda7afa6bf3cbc58cc57 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Thu, 29 Jan 2026 01:50:41 +0700 Subject: [PATCH] Add FFI functions for send_content and handle_payload (#29) * Add api calls for handle_payload and send_content * Add handle_payload and send_content to FFI --- conversations/src/api.rs | 65 ++++++++++++++++++++++++++- conversations/src/context.rs | 6 +-- conversations/src/lib.rs | 54 +++++++++++++++++++++-- nim-bindings/examples/pingpong.nim | 19 ++++++-- nim-bindings/src/bindings.nim | 20 +++++++++ nim-bindings/src/libchat.nim | 71 ++++++++++++++++++++++++++++++ 6 files changed, 224 insertions(+), 11 deletions(-) diff --git a/conversations/src/api.rs b/conversations/src/api.rs index 9fd7c09..2b127d4 100644 --- a/conversations/src/api.rs +++ b/conversations/src/api.rs @@ -12,7 +12,7 @@ pub enum ErrorCode { UnknownError = -6, } -use crate::context::{Context, Introduction}; +use crate::context::{Context, ConvoHandle, Introduction}; /// Opaque wrapper for Context #[derive_ReprC] @@ -101,6 +101,69 @@ pub fn create_new_private_convo( } } +/// Sends content to an existing conversation +/// +/// # Returns +/// Returns a PayloadResult with payloads that must be delivered to participants. +/// Check error_code field: 0 means success, negative values indicate errors (see ErrorCode). +#[ffi_export] +pub fn send_content( + ctx: &mut ContextHandle, + convo_handle: ConvoHandle, + content: c_slice::Ref<'_, u8>, +) -> PayloadResult { + let payloads = ctx.0.send_content(convo_handle, &content); + + let ffi_payloads: Vec = payloads + .into_iter() + .map(|p| Payload { + address: p.delivery_address.into(), + data: p.data.into(), + }) + .collect(); + + PayloadResult { + error_code: 0, + payloads: ffi_payloads.into(), + } +} + +/// Handles an incoming payload and writes content to caller-provided buffers +/// +/// # Returns +/// Returns the number of bytes written to data_out on success (>= 0). +/// Returns negative error code on failure (see ErrorCode). +/// conversation_id_out_len is set to the number of bytes written to conversation_id_out. +#[ffi_export] +pub fn handle_payload( + ctx: &mut ContextHandle, + payload: c_slice::Ref<'_, u8>, + mut conversation_id_out: c_slice::Mut<'_, u8>, + conversation_id_out_len: Out<'_, u32>, + mut content_out: c_slice::Mut<'_, u8>, +) -> i32 { + match ctx.0.handle_payload(&payload) { + Some(content) => { + let convo_id_bytes = content.conversation_id.as_bytes(); + + if conversation_id_out.len() < convo_id_bytes.len() { + return ErrorCode::BufferExceeded as i32; + } + + if content_out.len() < content.data.len() { + return ErrorCode::BufferExceeded as i32; + } + + conversation_id_out[..convo_id_bytes.len()].copy_from_slice(convo_id_bytes); + conversation_id_out_len.write(convo_id_bytes.len() as u32); + content_out[..content.data.len()].copy_from_slice(&content.data); + + content.data.len() as i32 + } + None => 0, + } +} + // ============================================================================ // safer_ffi implementation // =============================================================================================================================== diff --git a/conversations/src/context.rs b/conversations/src/context.rs index 75ced2f..b8e845e 100644 --- a/conversations/src/context.rs +++ b/conversations/src/context.rs @@ -14,7 +14,7 @@ pub use crate::inbox::Introduction; const INITIAL_CONVO_HANDLE: u32 = 0xF5000001; /// Used to identify a conversation on the othersize of the FFI. -type ConvoHandle = u32; +pub type ConvoHandle = u32; // This is the main entry point to the conversations api. // Ctx manages lifetimes of objects to process and generate payloads. @@ -63,10 +63,10 @@ impl Context { (convo_handle, payloads) } - pub fn send_content(&mut self, _convo_id: ConversationId, _content: &[u8]) -> Vec { + pub fn send_content(&mut self, convo_id: ConvoHandle, _content: &[u8]) -> Vec { // !TODO Replace Mock vec![PayloadData { - delivery_address: _convo_id.into(), + delivery_address: format!("addr-for-{convo_id}"), data: vec![40, 30, 20, 10], }] } diff --git a/conversations/src/lib.rs b/conversations/src/lib.rs index ed1a271..91aab32 100644 --- a/conversations/src/lib.rs +++ b/conversations/src/lib.rs @@ -30,18 +30,64 @@ mod tests { bundle.set_len(bundle_len as usize); } + assert!(bundle_len > 0, "bundle failed: {}", bundle_len); + let content = b"Hello"; + let result = create_new_private_convo(&mut ctx, bundle[..].into(), content[..].into()); + + assert!(result.error_code == 0, "Error: {}", result.error_code); + + destroy_context(ctx); + } + + #[test] + fn test_message_roundtrip() { + let mut saro = create_context(); + let mut raya = create_context(); + let mut raya_bundle = vec![0u8; 200]; + + let bundle_len = create_intro_bundle(&mut raya, (&mut raya_bundle[..]).into()); + unsafe { + raya_bundle.set_len(bundle_len as usize); + } + assert!(bundle_len > 0, "bundle failed: {}", bundle_len); let content = String::from_str("Hello").unwrap(); let result = create_new_private_convo( - &mut ctx, - bundle.as_slice().into(), + &mut saro, + raya_bundle.as_slice().into(), content.as_bytes().into(), ); assert!(result.error_code == 0, "Error: {}", result.error_code); - println!(" ID:{:?} Payloads:{:?}", result.convo_id, result.payloads); + // Handle payloads on raya's side + let mut conversation_id_out = vec![0u8; 256]; + let mut conversation_id_out_len: u32 = 0; + let mut content_out = vec![0u8; 256]; - destroy_context(ctx); + for p in result.payloads.iter() { + let bytes_written = handle_payload( + &mut raya, + p.data[..].into(), + (&mut conversation_id_out[..]).into(), + (&mut conversation_id_out_len).into(), + (&mut content_out[..]).into(), + ); + + unsafe { + content_out.set_len(bytes_written as usize); + } + + assert!( + bytes_written >= 0, + "handle_payload failed: {}", + bytes_written + ); + + //TODO: Verify output match + } + + destroy_context(saro); + destroy_context(raya); } } diff --git a/nim-bindings/examples/pingpong.nim b/nim-bindings/examples/pingpong.nim index 72b43ce..6ee3d43 100644 --- a/nim-bindings/examples/pingpong.nim +++ b/nim-bindings/examples/pingpong.nim @@ -1,3 +1,4 @@ +import options import results import ../src/libchat @@ -12,11 +13,23 @@ proc pingpong() = let intro = raya.createIntroductionBundle().expect("[Raya] Couldn't create intro bundle") echo "Raya's Intro Bundle: ",intro - var (convo_sr, payload) = saro.createNewPrivateConvo(intro,"Hey Raya").expect("[Saro] Couldn't create convo") + var (convo_sr, payloads) = saro.createNewPrivateConvo(intro, "Hey Raya").expect("[Saro] Couldn't create convo") echo "ConvoHandle:: ", convo_sr - echo "Payload:: ", payload - + echo "Payload:: ", payloads + ## Send Payloads to Raya + for p in payloads: + let res = raya.handlePayload(p.data) + if res.isOk: + let opt = res.get() + if opt.isSome: + let content_result = opt.get() + echo "RecvContent: ", content_result.conversationId, " ", content_result.data + else: + echo "Failed to handle payload: ", res.error + + echo "Done" when isMainModule: pingpong() + diff --git a/nim-bindings/src/bindings.nim b/nim-bindings/src/bindings.nim index 0b3fd26..3edf2d9 100644 --- a/nim-bindings/src/bindings.nim +++ b/nim-bindings/src/bindings.nim @@ -107,6 +107,26 @@ proc create_new_private_convo*( content: SliceUint8, ): NewConvoResult {.importc, dynlib: CONVERSATIONS_LIB.} +## Sends content to an existing conversation +## Returns: PayloadResult struct - check error_code field (0 = success, negative = error) +## The result must be freed with destroy_payload_result() +proc send_content*( + ctx: ContextHandle, + convo_handle: ConvoHandle, + content: SliceUint8, +): PayloadResult {.importc, dynlib: CONVERSATIONS_LIB.} + +## Handles an incoming payload and writes content to caller-provided buffers +## Returns: Number of bytes written to content_out on success (>= 0), negative error code on failure +## conversation_id_out_len is set to the number of bytes written to conversation_id_out +proc handle_payload*( + ctx: ContextHandle, + payload: SliceUint8, + conversation_id_out: SliceUint8, + conversation_id_out_len: ptr uint32, + content_out: SliceUint8, +): int32 {.importc, dynlib: CONVERSATIONS_LIB.} + ## Free the result from create_new_private_convo proc destroy_convo_result*(result: NewConvoResult) {.importc, dynlib: CONVERSATIONS_LIB.} diff --git a/nim-bindings/src/libchat.nim b/nim-bindings/src/libchat.nim index c0f3f6a..8203f46 100644 --- a/nim-bindings/src/libchat.nim +++ b/nim-bindings/src/libchat.nim @@ -1,3 +1,4 @@ +import std/options import results import bindings @@ -81,6 +82,76 @@ proc createNewPrivateConvo*(ctx: LibChat, bundle: string, content: string): Resu return ok((convoId, payloads)) +## Send content to an existing conversation +proc sendContent*(ctx: LibChat, convoHandle: ConvoHandle, content: string): Result[seq[PayloadResult], string] = + if ctx.handle == nil: + return err("Context handle is nil") + + if content.len == 0: + return err("content is zero length") + + let res = bindings.send_content( + ctx.handle, + convoHandle, + content.toSlice() + ) + + if res.error_code != 0: + result = err("Failed to send content: " & $res.error_code) + destroy_payload_result(res) + return + + # Convert payloads to Nim types + var payloads = newSeq[PayloadResult](res.payloads.len) + for i in 0 ..< res.payloads.len: + let p = res.payloads[int(i)] + payloads[int(i)] = PayloadResult( + address: $p.address, + data: p.data.toSeq() + ) + + destroy_payload_result(res) + return ok(payloads) + +type + ContentResult* = object + conversationId*: string + data*: seq[uint8] + +## Handle an incoming payload and decrypt content +proc handlePayload*(ctx: LibChat, payload: seq[byte]): Result[Option[ContentResult], string] = + if ctx.handle == nil: + return err("Context handle is nil") + + if payload.len == 0: + return err("payload is zero length") + + var conversationIdBuf = newSeq[byte](ctx.buffer_size) + var contentBuf = newSeq[byte](ctx.buffer_size) + var conversationIdLen: uint32 = 0 + + let bytesWritten = bindings.handle_payload( + ctx.handle, + payload.toSlice(), + conversationIdBuf.toSlice(), + addr conversationIdLen, + contentBuf.toSlice() + ) + + if bytesWritten < 0: + return err("Failed to handle payload: " & $bytesWritten) + + if bytesWritten == 0: + return ok(none(ContentResult)) + + conversationIdBuf.setLen(conversationIdLen) + contentBuf.setLen(bytesWritten) + + return ok(some(ContentResult( + conversationId: cast[string](conversationIdBuf), + data: contentBuf + ))) + proc `=destroy`(x: var LibChat) = # Automatically free handle when the destructor is called