From ecd1be3b9e7b9032f6c42b62aaa6ebef5963aa9c Mon Sep 17 00:00:00 2001
From: pablo
Date: Fri, 27 Mar 2026 10:11:57 +0100
Subject: [PATCH 1/3] feat: prepare for logos module consumption
- add flake.nix for Nix builds (cdylib + header generation)
- switch crate-type from dylib to cdylib for C FFI
- replace placeholder delivery addresses with real derived addresses
- switch FFI inputs from owned to borrowed
- expose local_delivery_address() so caller knows where to subscribe
---
.gitignore | 4 +
core/conversations/Cargo.toml | 9 +-
core/conversations/src/api.rs | 18 +++-
.../src/bin/generate-libchat-headers.rs | 3 +
core/conversations/src/context.rs | 4 +
.../src/conversation/privatev1.rs | 13 ++-
core/conversations/src/inbox/handler.rs | 24 ++---
core/conversations/src/lib.rs | 11 ++-
flake.nix | 95 +++++++++++++++++++
9 files changed, 154 insertions(+), 27 deletions(-)
create mode 100644 core/conversations/src/bin/generate-libchat-headers.rs
create mode 100644 flake.nix
diff --git a/.gitignore b/.gitignore
index 1dc0b45..7e66fdf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,4 +30,8 @@ target
# Temporary data folder
tmp
+# Auto-generated C headers (regenerated by flake.nix build)
+libchat.h
+double_ratchet.h
+
.DS_Store
diff --git a/core/conversations/Cargo.toml b/core/conversations/Cargo.toml
index 4ea9408..7df95b2 100644
--- a/core/conversations/Cargo.toml
+++ b/core/conversations/Cargo.toml
@@ -4,7 +4,14 @@ version = "0.1.0"
edition = "2024"
[lib]
-crate-type = ["rlib","staticlib","dylib"]
+crate-type = ["rlib","staticlib","cdylib"]
+
+[features]
+headers = ["safer-ffi/headers"]
+
+[[bin]]
+name = "generate-libchat-headers"
+required-features = ["headers"]
[dependencies]
base64 = "0.22"
diff --git a/core/conversations/src/api.rs b/core/conversations/src/api.rs
index bd1e300..3041b1f 100644
--- a/core/conversations/src/api.rs
+++ b/core/conversations/src/api.rs
@@ -49,9 +49,9 @@ pub struct ContextHandle(pub(crate) Context);
/// # Returns
/// Opaque handle to the store. Must be freed with destroy_context()
#[ffi_export]
-pub fn create_context(name: repr_c::String) -> repr_c::Box {
- // Deference name to to `str` and then borrow to &str
- Box::new(ContextHandle(Context::new_with_name(&*name))).into()
+pub fn create_context(name: c_slice::Ref<'_, u8>) -> repr_c::Box {
+ let name_str = std::str::from_utf8(name.as_slice()).unwrap_or("default");
+ Box::new(ContextHandle(Context::new_with_name(name_str))).into()
}
/// Returns the friendly name of the contexts installation.
@@ -61,6 +61,13 @@ pub fn installation_name(ctx: &ContextHandle) -> repr_c::String {
ctx.0.installation_name().to_string().into()
}
+/// Returns the local delivery address (hex-encoded Blake2b-512 of the installation public key).
+/// This is the address other installations use to send messages to this context.
+#[ffi_export]
+pub fn local_delivery_address(ctx: &ContextHandle) -> repr_c::String {
+ ctx.0.local_delivery_address().to_string().into()
+}
+
/// Destroys a conversation store and frees its memory
///
/// # Safety
@@ -182,11 +189,12 @@ pub fn list_conversations(ctx: &mut ContextHandle) -> ListConvoResult {
#[ffi_export]
pub fn send_content(
ctx: &mut ContextHandle,
- convo_id: repr_c::String,
+ convo_id: c_slice::Ref<'_, u8>,
content: c_slice::Ref<'_, u8>,
out: &mut SendContentResult,
) {
- let payloads = match ctx.0.send_content(&convo_id, &content) {
+ let convo_id_str = std::str::from_utf8(convo_id.as_slice()).unwrap_or("");
+ let payloads = match ctx.0.send_content(convo_id_str, &content) {
Ok(p) => p,
Err(_) => {
*out = SendContentResult {
diff --git a/core/conversations/src/bin/generate-libchat-headers.rs b/core/conversations/src/bin/generate-libchat-headers.rs
new file mode 100644
index 0000000..ac480ce
--- /dev/null
+++ b/core/conversations/src/bin/generate-libchat-headers.rs
@@ -0,0 +1,3 @@
+fn main() -> std::io::Result<()> {
+ libchat::generate_headers()
+}
diff --git a/core/conversations/src/context.rs b/core/conversations/src/context.rs
index bec37a5..70a52cd 100644
--- a/core/conversations/src/context.rs
+++ b/core/conversations/src/context.rs
@@ -65,6 +65,10 @@ impl Context {
self._identity.get_name()
}
+ pub fn local_delivery_address(&self) -> &str {
+ self.inbox.id()
+ }
+
pub fn create_private_convo(
&mut self,
remote_bundle: &Introduction,
diff --git a/core/conversations/src/conversation/privatev1.rs b/core/conversations/src/conversation/privatev1.rs
index 0b8042e..0289be5 100644
--- a/core/conversations/src/conversation/privatev1.rs
+++ b/core/conversations/src/conversation/privatev1.rs
@@ -55,11 +55,12 @@ impl BaseConvoId {
pub struct PrivateV1Convo {
local_convo_id: String,
remote_convo_id: String,
+ remote_delivery_address: String,
dr_state: RatchetState,
}
impl PrivateV1Convo {
- pub fn new_initiator(seed_key: SymmetricKey32, remote: PublicKey) -> Self {
+ pub fn new_initiator(seed_key: SymmetricKey32, remote: PublicKey, remote_delivery_address: String) -> Self {
let base_convo_id = BaseConvoId::new(&seed_key);
let local_convo_id = base_convo_id.id_for_participant(Role::Initiator);
let remote_convo_id = base_convo_id.id_for_participant(Role::Responder);
@@ -73,11 +74,12 @@ impl PrivateV1Convo {
Self {
local_convo_id,
remote_convo_id,
+ remote_delivery_address,
dr_state,
}
}
- pub fn new_responder(seed_key: SymmetricKey32, dh_self: &PrivateKey) -> Self {
+ pub fn new_responder(seed_key: SymmetricKey32, dh_self: &PrivateKey, remote_delivery_address: String) -> Self {
let base_convo_id = BaseConvoId::new(&seed_key);
let local_convo_id = base_convo_id.id_for_participant(Role::Responder);
let remote_convo_id = base_convo_id.id_for_participant(Role::Initiator);
@@ -92,6 +94,7 @@ impl PrivateV1Convo {
Self {
local_convo_id,
remote_convo_id,
+ remote_delivery_address,
dr_state,
}
}
@@ -179,7 +182,7 @@ impl Convo for PrivateV1Convo {
let data = self.encrypt(frame);
Ok(vec![AddressedEncryptedPayload {
- delivery_address: "delivery_address".into(),
+ delivery_address: self.remote_delivery_address.clone(),
data,
}])
}
@@ -236,8 +239,8 @@ mod tests {
let seed_key_saro = SymmetricKey32::from(seed_key);
let seed_key_raya = SymmetricKey32::from(seed_key);
let send_content_bytes = vec![0, 2, 4, 6, 8];
- let mut sr_convo = PrivateV1Convo::new_initiator(seed_key_saro, pub_raya);
- let mut rs_convo = PrivateV1Convo::new_responder(seed_key_raya, &raya);
+ let mut sr_convo = PrivateV1Convo::new_initiator(seed_key_saro, pub_raya, "test_addr".into());
+ let mut rs_convo = PrivateV1Convo::new_responder(seed_key_raya, &raya, "test_addr".into());
let send_frame = PrivateV1Frame {
conversation_id: "_".into(),
diff --git a/core/conversations/src/inbox/handler.rs b/core/conversations/src/inbox/handler.rs
index 278ae16..6752b3a 100644
--- a/core/conversations/src/inbox/handler.rs
+++ b/core/conversations/src/inbox/handler.rs
@@ -16,12 +16,6 @@ use crate::inbox::handshake::InboxHandshake;
use crate::proto;
use crate::types::{AddressedEncryptedPayload, ContentData};
-/// Compute the deterministic Delivery_address for an installation
-fn delivery_address_for_installation(_: PublicKey) -> String {
- // TODO: Implement Delivery Address
- "delivery_address".into()
-}
-
pub struct Inbox {
ident: Rc,
local_convo_id: String,
@@ -78,13 +72,12 @@ impl Inbox {
let (seed_key, ephemeral_pub) =
InboxHandshake::perform_as_initiator(self.ident.secret(), &pkb, &mut rng);
- let mut convo = PrivateV1Convo::new_initiator(seed_key, *remote_bundle.ephemeral_key());
+ let remote_delivery_addr = Inbox::inbox_identifier_for_key(*remote_bundle.installation_key());
+ let mut convo = PrivateV1Convo::new_initiator(seed_key, *remote_bundle.ephemeral_key(), remote_delivery_addr.clone());
let mut payloads = convo.send_message(initial_message)?;
- // Wrap First payload in Invite
if let Some(first_message) = payloads.get_mut(0) {
- // Take the the value of .data - it's being replaced at the end of this block
let frame = Self::wrap_in_invite(std::mem::take(&mut first_message.data));
// TODO: Encrypt frame
@@ -102,10 +95,7 @@ impl Inbox {
payload: Bytes::from_owner(ciphertext),
};
- // Update the address field with the Inbox delivery_Address
- first_message.delivery_address =
- delivery_address_for_installation(*remote_bundle.installation_key());
- // Update the data field with new Payload
+ first_message.delivery_address = remote_delivery_addr;
first_message.data = proto::EncryptedPayload {
encryption: Some(proto::Encryption::InboxHandshake(handshake)),
};
@@ -128,12 +118,18 @@ impl Inbox {
let key_index = hex::encode(header.responder_ephemeral.as_ref());
let ephemeral_key = self.lookup_ephemeral_key(&key_index)?;
+ // Extract initiator's identity key for delivery address before header is consumed
+ let initiator_static_bytes: [u8; 32] = header.initiator_static.as_ref()
+ .try_into()
+ .map_err(|_| ChatError::BadBundleValue("wrong size - initiator static".into()))?;
+ let remote_delivery_addr = Inbox::inbox_identifier_for_key(PublicKey::from(initiator_static_bytes));
+
// Perform handshake and decrypt frame
let (seed_key, frame) = self.perform_handshake(ephemeral_key, header, handshake.payload)?;
match frame.frame_type.unwrap() {
proto::inbox_v1_frame::FrameType::InvitePrivateV1(_invite_private_v1) => {
- let mut convo = PrivateV1Convo::new_responder(seed_key, ephemeral_key);
+ let mut convo = PrivateV1Convo::new_responder(seed_key, ephemeral_key, remote_delivery_addr);
let Some(enc_payload) = _invite_private_v1.initial_message else {
return Err(ChatError::Protocol("missing initial encpayload".into()));
diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs
index de0c023..0cb6bf6 100644
--- a/core/conversations/src/lib.rs
+++ b/core/conversations/src/lib.rs
@@ -14,6 +14,13 @@ pub use api::*;
pub use context::{Context, Introduction};
pub use errors::ChatError;
+#[cfg(feature = "headers")]
+pub fn generate_headers() -> std::io::Result<()> {
+ safer_ffi::headers::builder()
+ .to_file("libchat.h")?
+ .generate()
+}
+
#[cfg(test)]
mod tests {
@@ -24,8 +31,8 @@ mod tests {
#[test]
fn test_message_roundtrip() {
- let mut saro = create_context("saro".into());
- let mut raya = create_context("raya".into());
+ let mut saro = create_context(b"saro".as_slice().into());
+ let mut raya = create_context(b"raya".as_slice().into());
// Raya Creates Bundle and Sends to Saro
let mut intro_result = CreateIntroResult {
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..7653b8c
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,95 @@
+{
+ description = "libchat - Logos Chat cryptographic library";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ rust-overlay = {
+ url = "github:oxalica/rust-overlay";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ };
+
+ outputs = { self, nixpkgs, rust-overlay }:
+ let
+ systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
+ forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f {
+ pkgs = import nixpkgs {
+ inherit system;
+ overlays = [ rust-overlay.overlays.default ];
+ };
+ });
+ in
+ {
+ packages = forAllSystems ({ pkgs }:
+ let
+ rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust_toolchain.toml;
+ rustPlatform = pkgs.makeRustPlatform {
+ cargo = rustToolchain;
+ rustc = rustToolchain;
+ };
+ in
+ {
+ default = rustPlatform.buildRustPackage {
+ pname = "libchat";
+ version = "0.1.0";
+ src = pkgs.lib.cleanSourceWith {
+ src = ./.;
+ filter = path: type:
+ let base = builtins.baseNameOf path;
+ in !(builtins.elem base [ "target" "nim-bindings" ".git" ".github" "tmp" ]);
+ };
+
+ cargoLock = {
+ lockFile = ./Cargo.lock;
+ outputHashes = {
+ "chat-proto-0.1.0" = "sha256-aCl80VOIkd/GK3gnmRuFoSAvPBfeE/FKCaNlLt5AbUU=";
+ };
+ };
+
+ # perl: required by openssl-sys (transitive dep)
+ nativeBuildInputs = [ pkgs.perl pkgs.pkg-config pkgs.cmake ];
+ doCheck = false; # tests require network access unavailable in nix sandbox
+
+ postBuild = ''
+ cargo run --release --bin generate-libchat-headers --features headers
+ '';
+
+ installPhase = ''
+ runHook preInstall
+ mkdir -p $out/lib $out/include
+
+ # Copy shared library
+ cp target/release/liblibchat.so $out/lib/ 2>/dev/null || true
+ cp target/release/liblibchat.dylib $out/lib/ 2>/dev/null || true
+ cp target/release/liblibchat.a $out/lib/ 2>/dev/null || true
+
+ # Copy generated header
+ cp libchat.h $out/include/
+
+ runHook postInstall
+ '';
+
+ meta = with pkgs.lib; {
+ description = "Logos Chat cryptographic library (C FFI)";
+ platforms = platforms.unix;
+ };
+ };
+ }
+ );
+
+ devShells = forAllSystems ({ pkgs }:
+ let
+ rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust_toolchain.toml;
+ in
+ {
+ default = pkgs.mkShell {
+ nativeBuildInputs = [
+ rustToolchain
+ pkgs.pkg-config
+ pkgs.cmake
+ ];
+ };
+ }
+ );
+ };
+}
From 14a403399edf821057b3e8cd3d975f581724c736 Mon Sep 17 00:00:00 2001
From: pablo
Date: Fri, 27 Mar 2026 10:43:29 +0100
Subject: [PATCH 2/3] pr feedback and test fix
---
core/conversations/src/api.rs | 19 +++++++++++++++----
.../src/conversation/privatev1.rs | 15 ++++++++++++---
core/conversations/src/inbox/handler.rs | 19 ++++++++++++++-----
flake.nix | 10 ++++++++--
nim-bindings/src/bindings.nim | 4 ++--
nim-bindings/tests/test_all_endpoints.nim | 12 ++++++------
6 files changed, 57 insertions(+), 22 deletions(-)
diff --git a/core/conversations/src/api.rs b/core/conversations/src/api.rs
index 3041b1f..a189627 100644
--- a/core/conversations/src/api.rs
+++ b/core/conversations/src/api.rs
@@ -47,11 +47,13 @@ pub struct ContextHandle(pub(crate) Context);
/// Creates a new libchat Ctx
///
/// # Returns
-/// Opaque handle to the store. Must be freed with destroy_context()
+/// Opaque handle to the store. Must be freed with destroy_context().
+/// Uses lossy UTF-8 conversion: invalid bytes are replaced with U+FFFD
+/// so the caller always gets a deterministic name reflecting their input.
#[ffi_export]
pub fn create_context(name: c_slice::Ref<'_, u8>) -> repr_c::Box {
- let name_str = std::str::from_utf8(name.as_slice()).unwrap_or("default");
- Box::new(ContextHandle(Context::new_with_name(name_str))).into()
+ let name_str = std::string::String::from_utf8_lossy(name.as_slice());
+ Box::new(ContextHandle(Context::new_with_name(&name_str))).into()
}
/// Returns the friendly name of the contexts installation.
@@ -193,7 +195,16 @@ pub fn send_content(
content: c_slice::Ref<'_, u8>,
out: &mut SendContentResult,
) {
- let convo_id_str = std::str::from_utf8(convo_id.as_slice()).unwrap_or("");
+ let convo_id_str = match std::str::from_utf8(convo_id.as_slice()) {
+ Ok(s) => s,
+ Err(_) => {
+ *out = SendContentResult {
+ error_code: ErrorCode::BadConvoId as i32,
+ payloads: safer_ffi::Vec::EMPTY,
+ };
+ return;
+ }
+ };
let payloads = match ctx.0.send_content(convo_id_str, &content) {
Ok(p) => p,
Err(_) => {
diff --git a/core/conversations/src/conversation/privatev1.rs b/core/conversations/src/conversation/privatev1.rs
index 0289be5..080f315 100644
--- a/core/conversations/src/conversation/privatev1.rs
+++ b/core/conversations/src/conversation/privatev1.rs
@@ -60,7 +60,11 @@ pub struct PrivateV1Convo {
}
impl PrivateV1Convo {
- pub fn new_initiator(seed_key: SymmetricKey32, remote: PublicKey, remote_delivery_address: String) -> Self {
+ pub fn new_initiator(
+ seed_key: SymmetricKey32,
+ remote: PublicKey,
+ remote_delivery_address: String,
+ ) -> Self {
let base_convo_id = BaseConvoId::new(&seed_key);
let local_convo_id = base_convo_id.id_for_participant(Role::Initiator);
let remote_convo_id = base_convo_id.id_for_participant(Role::Responder);
@@ -79,7 +83,11 @@ impl PrivateV1Convo {
}
}
- pub fn new_responder(seed_key: SymmetricKey32, dh_self: &PrivateKey, remote_delivery_address: String) -> Self {
+ pub fn new_responder(
+ seed_key: SymmetricKey32,
+ dh_self: &PrivateKey,
+ remote_delivery_address: String,
+ ) -> Self {
let base_convo_id = BaseConvoId::new(&seed_key);
let local_convo_id = base_convo_id.id_for_participant(Role::Responder);
let remote_convo_id = base_convo_id.id_for_participant(Role::Initiator);
@@ -239,7 +247,8 @@ mod tests {
let seed_key_saro = SymmetricKey32::from(seed_key);
let seed_key_raya = SymmetricKey32::from(seed_key);
let send_content_bytes = vec![0, 2, 4, 6, 8];
- let mut sr_convo = PrivateV1Convo::new_initiator(seed_key_saro, pub_raya, "test_addr".into());
+ let mut sr_convo =
+ PrivateV1Convo::new_initiator(seed_key_saro, pub_raya, "test_addr".into());
let mut rs_convo = PrivateV1Convo::new_responder(seed_key_raya, &raya, "test_addr".into());
let send_frame = PrivateV1Frame {
diff --git a/core/conversations/src/inbox/handler.rs b/core/conversations/src/inbox/handler.rs
index 6752b3a..6b1862e 100644
--- a/core/conversations/src/inbox/handler.rs
+++ b/core/conversations/src/inbox/handler.rs
@@ -72,8 +72,13 @@ impl Inbox {
let (seed_key, ephemeral_pub) =
InboxHandshake::perform_as_initiator(self.ident.secret(), &pkb, &mut rng);
- let remote_delivery_addr = Inbox::inbox_identifier_for_key(*remote_bundle.installation_key());
- let mut convo = PrivateV1Convo::new_initiator(seed_key, *remote_bundle.ephemeral_key(), remote_delivery_addr.clone());
+ let remote_delivery_addr =
+ Inbox::inbox_identifier_for_key(*remote_bundle.installation_key());
+ let mut convo = PrivateV1Convo::new_initiator(
+ seed_key,
+ *remote_bundle.ephemeral_key(),
+ remote_delivery_addr.clone(),
+ );
let mut payloads = convo.send_message(initial_message)?;
@@ -119,17 +124,21 @@ impl Inbox {
let ephemeral_key = self.lookup_ephemeral_key(&key_index)?;
// Extract initiator's identity key for delivery address before header is consumed
- let initiator_static_bytes: [u8; 32] = header.initiator_static.as_ref()
+ let initiator_static_bytes: [u8; 32] = header
+ .initiator_static
+ .as_ref()
.try_into()
.map_err(|_| ChatError::BadBundleValue("wrong size - initiator static".into()))?;
- let remote_delivery_addr = Inbox::inbox_identifier_for_key(PublicKey::from(initiator_static_bytes));
+ let remote_delivery_addr =
+ Inbox::inbox_identifier_for_key(PublicKey::from(initiator_static_bytes));
// Perform handshake and decrypt frame
let (seed_key, frame) = self.perform_handshake(ephemeral_key, header, handshake.payload)?;
match frame.frame_type.unwrap() {
proto::inbox_v1_frame::FrameType::InvitePrivateV1(_invite_private_v1) => {
- let mut convo = PrivateV1Convo::new_responder(seed_key, ephemeral_key, remote_delivery_addr);
+ let mut convo =
+ PrivateV1Convo::new_responder(seed_key, ephemeral_key, remote_delivery_addr);
let Some(enc_payload) = _invite_private_v1.initial_message else {
return Err(ChatError::Protocol("missing initial encpayload".into()));
diff --git a/flake.nix b/flake.nix
index 7653b8c..ea8d250 100644
--- a/flake.nix
+++ b/flake.nix
@@ -51,18 +51,24 @@
doCheck = false; # tests require network access unavailable in nix sandbox
postBuild = ''
- cargo run --release --bin generate-libchat-headers --features headers
+ cargo run --frozen --release --bin generate-libchat-headers --features headers
'';
installPhase = ''
runHook preInstall
mkdir -p $out/lib $out/include
- # Copy shared library
+ # Copy shared library (platform-dependent extension)
cp target/release/liblibchat.so $out/lib/ 2>/dev/null || true
cp target/release/liblibchat.dylib $out/lib/ 2>/dev/null || true
cp target/release/liblibchat.a $out/lib/ 2>/dev/null || true
+ # Fail if no library was produced
+ if [ -z "$(ls $out/lib/liblibchat.* 2>/dev/null)" ]; then
+ echo "ERROR: No library artifact found in target/release/"
+ exit 1
+ fi
+
# Copy generated header
cp libchat.h $out/include/
diff --git a/nim-bindings/src/bindings.nim b/nim-bindings/src/bindings.nim
index eb6053e..aa7310a 100644
--- a/nim-bindings/src/bindings.nim
+++ b/nim-bindings/src/bindings.nim
@@ -85,7 +85,7 @@ type
## Creates a new libchat Context
## Returns: Opaque handle to the context. Must be freed with destroy_context()
-proc create_context*(name: ReprCString): ContextHandle {.importc.}
+proc create_context*(name: SliceUint8): ContextHandle {.importc.}
## Returns the friendly name of the context's identity
## The result must be freed by the caller (repr_c::String ownership transfers)
@@ -129,7 +129,7 @@ proc list_conversations*(
## The result must be freed with destroy_send_content_result()
proc send_content*(
ctx: ContextHandle,
- convo_id: ReprCString,
+ convo_id: SliceUint8,
content: SliceUint8,
): SendContentResult {.importc.}
diff --git a/nim-bindings/tests/test_all_endpoints.nim b/nim-bindings/tests/test_all_endpoints.nim
index e9e3f54..bc12f95 100644
--- a/nim-bindings/tests/test_all_endpoints.nim
+++ b/nim-bindings/tests/test_all_endpoints.nim
@@ -72,7 +72,7 @@ proc testHelperProcs() =
proc testContextLifecycle() =
echo "\n--- testContextLifecycle ---"
- let ctx = create_context(toReprCString("lifecycle-test"))
+ let ctx = create_context(toSlice("lifecycle-test"))
check(ctx != nil, "create_context: returns non-nil handle")
let iname = installation_name(ctx)
@@ -94,10 +94,10 @@ proc testContextLifecycle() =
proc testFullConversationFlow() =
echo "\n--- testFullConversationFlow ---"
- let aliceCtx = create_context(toReprCString("alice"))
+ let aliceCtx = create_context(toSlice("alice"))
check(aliceCtx != nil, "Alice: create_context non-nil")
- let bobCtx = create_context(toReprCString("bob"))
+ let bobCtx = create_context(toSlice("bob"))
check(bobCtx != nil, "Bob: create_context non-nil")
# --- create_intro_bundle ---
@@ -177,7 +177,7 @@ proc testFullConversationFlow() =
# --- send_content ---
var sendRes = send_content(
aliceCtx,
- toReprCString(aliceConvoId),
+ toSlice(aliceConvoId),
toSlice("How are you, Bob?")
)
check(sendRes.error_code == ErrNone,
@@ -213,13 +213,13 @@ proc testFullConversationFlow() =
proc testErrorCases() =
echo "\n--- testErrorCases ---"
- let ctx = create_context(toReprCString("error-tester"))
+ let ctx = create_context(toSlice("error-tester"))
check(ctx != nil, "error-tester: create_context non-nil")
# send_content with a nonexistent convo_id must fail
var badSend = send_content(
ctx,
- toReprCString("00000000-0000-0000-0000-nonexistent"),
+ toSlice("00000000-0000-0000-0000-nonexistent"),
toSlice("payload")
)
check(badSend.error_code != ErrNone,
From cdd58ec7f4f01db8ac439f1c2ab2a3f186d14eae Mon Sep 17 00:00:00 2001
From: pablo
Date: Fri, 27 Mar 2026 10:51:45 +0100
Subject: [PATCH 3/3] fix: build
---
core/conversations/src/api.rs | 4 ++--
nim-bindings/src/libchat.nim | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/core/conversations/src/api.rs b/core/conversations/src/api.rs
index a189627..be0618a 100644
--- a/core/conversations/src/api.rs
+++ b/core/conversations/src/api.rs
@@ -52,8 +52,8 @@ pub struct ContextHandle(pub(crate) Context);
/// so the caller always gets a deterministic name reflecting their input.
#[ffi_export]
pub fn create_context(name: c_slice::Ref<'_, u8>) -> repr_c::Box {
- let name_str = std::string::String::from_utf8_lossy(name.as_slice());
- Box::new(ContextHandle(Context::new_with_name(&name_str))).into()
+ let name_str = std::string::String::from_utf8_lossy(name.as_slice()).into_owned();
+ Box::new(ContextHandle(Context::new_with_name(name_str))).into()
}
/// Returns the friendly name of the contexts installation.
diff --git a/nim-bindings/src/libchat.nim b/nim-bindings/src/libchat.nim
index 90960c1..7368c73 100644
--- a/nim-bindings/src/libchat.nim
+++ b/nim-bindings/src/libchat.nim
@@ -15,7 +15,7 @@ type
## Create a new conversations context
proc newConversationsContext*(name: string): LibChat =
- result.handle = create_context(name.toReprCString)
+ result.handle = create_context(name.toSlice)
result.buffer_size = 256
if result.handle.isNil:
raise newException(IOError, "Failed to create context")
@@ -95,7 +95,7 @@ proc sendContent*(ctx: LibChat, convoId: string, content: seq[byte]): Result[seq
if content.len == 0:
return err("content is zero length")
- let res = bindings.send_content(ctx.handle, convoId.toReprCString, content.toSlice())
+ let res = bindings.send_content(ctx.handle, convoId.toSlice, content.toSlice())
defer: destroy_send_content_result(res)
if res.error_code != 0: