From 798fbf731aaa67f58b919100fdf58692a0efa602 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:28:02 -0800 Subject: [PATCH] Rust -> Nim ABI (#62) * Use correct build hook * force sret like return from rust code for nim compatibility * Fix target mismatch * Update usages --- .cargo/config.toml | 7 +++ conversations/src/api.rs | 53 +++++++++++++++-------- conversations/src/lib.rs | 21 +++++++-- nim-bindings/conversations_example.nimble | 9 +++- 4 files changed, 68 insertions(+), 22 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..23baf9f --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,7 @@ +[env] +# Pin the macOS deployment target so vendored C/C++ builds (SQLCipher, OpenSSL) +# compile with the same minimum version as the Nim linker expects. +# Without this, the host SDK version is used (currently 15.5), causing +# "was built for newer macOS version" linker warnings. +# This is caused by nimble and cargo defaulting to different targets. +MACOSX_DEPLOYMENT_TARGET = { value = "15.0", force = false } diff --git a/conversations/src/api.rs b/conversations/src/api.rs index 62d635c..31fb9d9 100644 --- a/conversations/src/api.rs +++ b/conversations/src/api.rs @@ -55,9 +55,12 @@ pub fn create_context(name: repr_c::String) -> repr_c::Box { } /// Returns the friendly name of the contexts installation. +/// +/// # ABI note +/// The result is written through `out` (Nim's calling convention for large struct returns). #[ffi_export] -pub fn installation_name(ctx: &ContextHandle) -> repr_c::String { - ctx.0.installation_name().to_string().into() +pub fn installation_name(ctx: &ContextHandle, out: &mut repr_c::String) { + *out = ctx.0.installation_name().to_string().into(); } /// Destroys a conversation store and frees its memory @@ -74,11 +77,13 @@ pub fn destroy_context(ctx: repr_c::Box) { /// Creates an intro bundle for sharing with other users /// /// # Returns -/// Returns the number of bytes written to bundle_out /// Check error_code field: 0 means success, negative values indicate errors (see ErrorCode). +/// +/// # ABI note +/// The result is written through `out` (Nim's calling convention for large struct returns). #[ffi_export] -pub fn create_intro_bundle(ctx: &mut ContextHandle) -> CreateIntroResult { - match ctx.0.create_intro_bundle() { +pub fn create_intro_bundle(ctx: &mut ContextHandle, out: &mut CreateIntroResult) { + *out = match ctx.0.create_intro_bundle() { Ok(v) => CreateIntroResult { error_code: ErrorCode::None as i32, intro_bytes: v.into(), @@ -87,7 +92,7 @@ pub fn create_intro_bundle(ctx: &mut ContextHandle) -> CreateIntroResult { error_code: ErrorCode::UnknownError as i32, intro_bytes: repr_c::Vec::EMPTY, }, - } + }; } /// Creates a new private conversation @@ -95,19 +100,24 @@ pub fn create_intro_bundle(ctx: &mut ContextHandle) -> CreateIntroResult { /// # Returns /// Returns a struct with payloads that must be sent, the conversation_id that was created. /// The NewConvoResult must be freed. +/// +/// # ABI note +/// The result is written through `out` (Nim's calling convention for large struct returns). #[ffi_export] pub fn create_new_private_convo( ctx: &mut ContextHandle, bundle: c_slice::Ref<'_, u8>, content: c_slice::Ref<'_, u8>, -) -> NewConvoResult { + out: &mut NewConvoResult, +) { // Convert input bundle to Introduction let Ok(intro) = Introduction::try_from(bundle.as_slice()) else { - return NewConvoResult { + *out = NewConvoResult { error_code: ErrorCode::BadIntro as i32, convo_id: "".into(), payloads: Vec::new().into(), }; + return; }; // Create conversation @@ -122,11 +132,11 @@ pub fn create_new_private_convo( }) .collect(); - NewConvoResult { + *out = NewConvoResult { error_code: 0, convo_id: convo_id.to_string().into(), payloads: ffi_payloads.into(), - } + }; } /// Sends content to an existing conversation @@ -134,19 +144,24 @@ pub fn create_new_private_convo( /// # 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). +/// +/// # ABI note +/// The result is written through `out` (Nim's calling convention for large struct returns). #[ffi_export] pub fn send_content( ctx: &mut ContextHandle, convo_id: repr_c::String, content: c_slice::Ref<'_, u8>, -) -> SendContentResult { + out: &mut SendContentResult, +) { let payloads = match ctx.0.send_content(&convo_id, &content) { Ok(p) => p, Err(_) => { - return SendContentResult { + *out = SendContentResult { error_code: ErrorCode::UnknownError as i32, payloads: safer_ffi::Vec::EMPTY, }; + return; } }; @@ -158,10 +173,10 @@ pub fn send_content( }) .collect(); - SendContentResult { + *out = SendContentResult { error_code: 0, payloads: ffi_payloads.into(), - } + }; } /// Handles an incoming payload @@ -170,15 +185,19 @@ pub fn send_content( /// Returns HandlePayloadResult /// This call does not always generate content. If data is zero bytes long then there /// is no data, and the converation_id should be ignored. +/// +/// # ABI note +/// The result is written through `out` (Nim's calling convention for large struct returns). #[ffi_export] pub fn handle_payload( ctx: &mut ContextHandle, payload: c_slice::Ref<'_, u8>, -) -> HandlePayloadResult { - match ctx.0.handle_payload(&payload) { + out: &mut HandlePayloadResult, +) { + *out = match ctx.0.handle_payload(&payload) { Ok(o) => o.into(), Err(e) => e.into(), - } + }; } // ------------------------------------------ diff --git a/conversations/src/lib.rs b/conversations/src/lib.rs index 810afb9..79d6a5a 100644 --- a/conversations/src/lib.rs +++ b/conversations/src/lib.rs @@ -25,7 +25,11 @@ mod tests { let mut raya = create_context("raya".into()); // Raya Creates Bundle and Sends to Saro - let intro_result = create_intro_bundle(&mut raya); + let mut intro_result = CreateIntroResult { + error_code: -99, + intro_bytes: safer_ffi::Vec::EMPTY, + }; + create_intro_bundle(&mut raya, &mut intro_result); assert!(is_ok(intro_result.error_code)); let raya_bundle = intro_result.intro_bytes.as_ref(); @@ -33,13 +37,24 @@ mod tests { // Saro creates a new conversation with Raya let content: &[u8] = "hello".as_bytes(); - let convo_result = create_new_private_convo(&mut saro, raya_bundle, content.into()); + let mut convo_result = NewConvoResult { + error_code: -99, + convo_id: "".into(), + payloads: safer_ffi::Vec::EMPTY, + }; + create_new_private_convo(&mut saro, raya_bundle, content.into(), &mut convo_result); assert!(is_ok(convo_result.error_code)); // Raya recieves initial message let payload = convo_result.payloads.first().unwrap(); - let handle_result = handle_payload(&mut raya, payload.data.as_ref()); + let mut handle_result: HandlePayloadResult = HandlePayloadResult { + error_code: -99, + convo_id: "".into(), + content: safer_ffi::Vec::EMPTY, + is_new_convo: false, + }; + handle_payload(&mut raya, payload.data.as_ref(), &mut handle_result); assert!(is_ok(handle_result.error_code)); // Check that the Content sent was the content received diff --git a/nim-bindings/conversations_example.nimble b/nim-bindings/conversations_example.nimble index dc48df7..575ed22 100644 --- a/nim-bindings/conversations_example.nimble +++ b/nim-bindings/conversations_example.nimble @@ -13,9 +13,14 @@ bin = @["libchat"] requires "nim >= 2.2.4" requires "results" -# Build Rust library before compiling Nim -before build: +proc buildRust() = exec "cargo build --release --manifest-path ../Cargo.toml" + +# Build Rust library before compiling Nim +before build: + buildRust() + task pingpong, "Run pingpong example": + buildRust() exec "nim c -r --path:src --passL:../target/release/liblibchat.a examples/pingpong.nim"