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
This commit is contained in:
Jazz Turner-Baggs 2026-01-29 01:50:41 +07:00 committed by GitHub
parent 1cb1ffc996
commit 1f0354f8e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 224 additions and 11 deletions

View File

@ -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<Payload> = 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
// ===============================================================================================================================

View File

@ -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<PayloadData> {
pub fn send_content(&mut self, convo_id: ConvoHandle, _content: &[u8]) -> Vec<PayloadData> {
// !TODO Replace Mock
vec![PayloadData {
delivery_address: _convo_id.into(),
delivery_address: format!("addr-for-{convo_id}"),
data: vec![40, 30, 20, 10],
}]
}

View File

@ -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);
}
}

View File

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

View File

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

View File

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