Rust -> Nim ABI (#62)

* Use correct build hook

* force sret like return from rust code for nim compatibility

* Fix target mismatch

* Update usages
This commit is contained in:
Jazz Turner-Baggs 2026-02-24 15:28:02 -08:00 committed by GitHub
parent 803a11ce27
commit 798fbf731a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 68 additions and 22 deletions

7
.cargo/config.toml Normal file
View File

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

View File

@ -55,9 +55,12 @@ pub fn create_context(name: repr_c::String) -> repr_c::Box<ContextHandle> {
} }
/// Returns the friendly name of the contexts installation. /// 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] #[ffi_export]
pub fn installation_name(ctx: &ContextHandle) -> repr_c::String { pub fn installation_name(ctx: &ContextHandle, out: &mut repr_c::String) {
ctx.0.installation_name().to_string().into() *out = ctx.0.installation_name().to_string().into();
} }
/// Destroys a conversation store and frees its memory /// Destroys a conversation store and frees its memory
@ -74,11 +77,13 @@ pub fn destroy_context(ctx: repr_c::Box<ContextHandle>) {
/// Creates an intro bundle for sharing with other users /// Creates an intro bundle for sharing with other users
/// ///
/// # Returns /// # Returns
/// Returns the number of bytes written to bundle_out
/// Check error_code field: 0 means success, negative values indicate errors (see ErrorCode). /// 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] #[ffi_export]
pub fn create_intro_bundle(ctx: &mut ContextHandle) -> CreateIntroResult { pub fn create_intro_bundle(ctx: &mut ContextHandle, out: &mut CreateIntroResult) {
match ctx.0.create_intro_bundle() { *out = match ctx.0.create_intro_bundle() {
Ok(v) => CreateIntroResult { Ok(v) => CreateIntroResult {
error_code: ErrorCode::None as i32, error_code: ErrorCode::None as i32,
intro_bytes: v.into(), intro_bytes: v.into(),
@ -87,7 +92,7 @@ pub fn create_intro_bundle(ctx: &mut ContextHandle) -> CreateIntroResult {
error_code: ErrorCode::UnknownError as i32, error_code: ErrorCode::UnknownError as i32,
intro_bytes: repr_c::Vec::EMPTY, intro_bytes: repr_c::Vec::EMPTY,
}, },
} };
} }
/// Creates a new private conversation /// Creates a new private conversation
@ -95,19 +100,24 @@ pub fn create_intro_bundle(ctx: &mut ContextHandle) -> CreateIntroResult {
/// # Returns /// # Returns
/// Returns a struct with payloads that must be sent, the conversation_id that was created. /// Returns a struct with payloads that must be sent, the conversation_id that was created.
/// The NewConvoResult must be freed. /// The NewConvoResult must be freed.
///
/// # ABI note
/// The result is written through `out` (Nim's calling convention for large struct returns).
#[ffi_export] #[ffi_export]
pub fn create_new_private_convo( pub fn create_new_private_convo(
ctx: &mut ContextHandle, ctx: &mut ContextHandle,
bundle: c_slice::Ref<'_, u8>, bundle: c_slice::Ref<'_, u8>,
content: c_slice::Ref<'_, u8>, content: c_slice::Ref<'_, u8>,
) -> NewConvoResult { out: &mut NewConvoResult,
) {
// Convert input bundle to Introduction // Convert input bundle to Introduction
let Ok(intro) = Introduction::try_from(bundle.as_slice()) else { let Ok(intro) = Introduction::try_from(bundle.as_slice()) else {
return NewConvoResult { *out = NewConvoResult {
error_code: ErrorCode::BadIntro as i32, error_code: ErrorCode::BadIntro as i32,
convo_id: "".into(), convo_id: "".into(),
payloads: Vec::new().into(), payloads: Vec::new().into(),
}; };
return;
}; };
// Create conversation // Create conversation
@ -122,11 +132,11 @@ pub fn create_new_private_convo(
}) })
.collect(); .collect();
NewConvoResult { *out = NewConvoResult {
error_code: 0, error_code: 0,
convo_id: convo_id.to_string().into(), convo_id: convo_id.to_string().into(),
payloads: ffi_payloads.into(), payloads: ffi_payloads.into(),
} };
} }
/// Sends content to an existing conversation /// Sends content to an existing conversation
@ -134,19 +144,24 @@ pub fn create_new_private_convo(
/// # Returns /// # Returns
/// Returns a PayloadResult with payloads that must be delivered to participants. /// Returns a PayloadResult with payloads that must be delivered to participants.
/// Check error_code field: 0 means success, negative values indicate errors (see ErrorCode). /// 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] #[ffi_export]
pub fn send_content( pub fn send_content(
ctx: &mut ContextHandle, ctx: &mut ContextHandle,
convo_id: repr_c::String, convo_id: repr_c::String,
content: c_slice::Ref<'_, u8>, content: c_slice::Ref<'_, u8>,
) -> SendContentResult { out: &mut SendContentResult,
) {
let payloads = match ctx.0.send_content(&convo_id, &content) { let payloads = match ctx.0.send_content(&convo_id, &content) {
Ok(p) => p, Ok(p) => p,
Err(_) => { Err(_) => {
return SendContentResult { *out = SendContentResult {
error_code: ErrorCode::UnknownError as i32, error_code: ErrorCode::UnknownError as i32,
payloads: safer_ffi::Vec::EMPTY, payloads: safer_ffi::Vec::EMPTY,
}; };
return;
} }
}; };
@ -158,10 +173,10 @@ pub fn send_content(
}) })
.collect(); .collect();
SendContentResult { *out = SendContentResult {
error_code: 0, error_code: 0,
payloads: ffi_payloads.into(), payloads: ffi_payloads.into(),
} };
} }
/// Handles an incoming payload /// Handles an incoming payload
@ -170,15 +185,19 @@ pub fn send_content(
/// Returns HandlePayloadResult /// Returns HandlePayloadResult
/// This call does not always generate content. If data is zero bytes long then there /// 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. /// 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] #[ffi_export]
pub fn handle_payload( pub fn handle_payload(
ctx: &mut ContextHandle, ctx: &mut ContextHandle,
payload: c_slice::Ref<'_, u8>, payload: c_slice::Ref<'_, u8>,
) -> HandlePayloadResult { out: &mut HandlePayloadResult,
match ctx.0.handle_payload(&payload) { ) {
*out = match ctx.0.handle_payload(&payload) {
Ok(o) => o.into(), Ok(o) => o.into(),
Err(e) => e.into(), Err(e) => e.into(),
} };
} }
// ------------------------------------------ // ------------------------------------------

View File

@ -25,7 +25,11 @@ mod tests {
let mut raya = create_context("raya".into()); let mut raya = create_context("raya".into());
// Raya Creates Bundle and Sends to Saro // 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)); assert!(is_ok(intro_result.error_code));
let raya_bundle = intro_result.intro_bytes.as_ref(); let raya_bundle = intro_result.intro_bytes.as_ref();
@ -33,13 +37,24 @@ mod tests {
// Saro creates a new conversation with Raya // Saro creates a new conversation with Raya
let content: &[u8] = "hello".as_bytes(); 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)); assert!(is_ok(convo_result.error_code));
// Raya recieves initial message // Raya recieves initial message
let payload = convo_result.payloads.first().unwrap(); 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)); assert!(is_ok(handle_result.error_code));
// Check that the Content sent was the content received // Check that the Content sent was the content received

View File

@ -13,9 +13,14 @@ bin = @["libchat"]
requires "nim >= 2.2.4" requires "nim >= 2.2.4"
requires "results" requires "results"
# Build Rust library before compiling Nim proc buildRust() =
before build:
exec "cargo build --release --manifest-path ../Cargo.toml" exec "cargo build --release --manifest-path ../Cargo.toml"
# Build Rust library before compiling Nim
before build:
buildRust()
task pingpong, "Run pingpong example": task pingpong, "Run pingpong example":
buildRust()
exec "nim c -r --path:src --passL:../target/release/liblibchat.a examples/pingpong.nim" exec "nim c -r --path:src --passL:../target/release/liblibchat.a examples/pingpong.nim"