mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-03-28 15:13:07 +00:00
wip
This commit is contained in:
parent
5189bcf872
commit
504b743929
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -60,6 +60,14 @@ version = "1.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "c-test"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"client-ffi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.54"
|
version = "1.2.54"
|
||||||
@ -127,6 +135,14 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "client-ffi"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"client",
|
||||||
|
"libchat",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
|
|||||||
@ -8,6 +8,8 @@ members = [
|
|||||||
"core/double-ratchets",
|
"core/double-ratchets",
|
||||||
"core/storage",
|
"core/storage",
|
||||||
"crates/client",
|
"crates/client",
|
||||||
|
"crates/client-ffi",
|
||||||
|
"crates/c-test",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|||||||
13
crates/c-test/Cargo.toml
Normal file
13
crates/c-test/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "c-test"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
client-ffi = { path = "../client-ffi" }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cc = "1"
|
||||||
17
crates/c-test/build.rs
Normal file
17
crates/c-test/build.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
fn main() {
|
||||||
|
let manifest_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let header_dir = manifest_dir.parent().unwrap().join("client-ffi");
|
||||||
|
|
||||||
|
// alice_bob_test.c contains both the delivery implementation (queue +
|
||||||
|
// callback) and the test logic. It has no direct references to Rust
|
||||||
|
// symbols; all protocol calls go through the ClientOps vtable supplied
|
||||||
|
// by the Rust harness.
|
||||||
|
cc::Build::new()
|
||||||
|
.file("c/alice_bob_test.c")
|
||||||
|
.include(&header_dir)
|
||||||
|
.compile("c_alice_bob");
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-changed=c/alice_bob_test.c");
|
||||||
|
println!("cargo:rerun-if-changed=c/delivery.c");
|
||||||
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
|
}
|
||||||
206
crates/c-test/c/alice_bob_test.c
Normal file
206
crates/c-test/c/alice_bob_test.c
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
* c-test platform: Alice-Bob message exchange written entirely in C.
|
||||||
|
*
|
||||||
|
* Delivery (queue + callback) is implemented here in C.
|
||||||
|
* Protocol operations are called through a ClientOps vtable supplied by
|
||||||
|
* the Rust test harness. This keeps the C archive free of direct references
|
||||||
|
* to Rust symbols, avoiding linker-ordering issues.
|
||||||
|
* Persistence is in-memory ephemeral (same as the Rust TestPlatform).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "client_ffi.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* In-memory delivery queue
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/* Protocol messages (intro bundles, ratchet envelopes) are well under 2 KB;
|
||||||
|
* using modest constants keeps both queues inside a few hundred KB of stack. */
|
||||||
|
#define MAX_ENVELOPES 32
|
||||||
|
#define MAX_ENVELOPE_SZ 2048
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t data[MAX_ENVELOPE_SZ];
|
||||||
|
size_t len;
|
||||||
|
} Envelope;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Envelope items[MAX_ENVELOPES];
|
||||||
|
int head;
|
||||||
|
int tail;
|
||||||
|
int count;
|
||||||
|
} Queue;
|
||||||
|
|
||||||
|
static void queue_init(Queue *q)
|
||||||
|
{
|
||||||
|
memset(q, 0, sizeof(*q));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void queue_push(Queue *q, const uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
assert(q->count < MAX_ENVELOPES && "delivery queue overflow");
|
||||||
|
assert(len <= MAX_ENVELOPE_SZ && "envelope too large");
|
||||||
|
memcpy(q->items[q->tail].data, data, len);
|
||||||
|
q->items[q->tail].len = len;
|
||||||
|
q->tail = (q->tail + 1) % MAX_ENVELOPES;
|
||||||
|
q->count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int queue_pop(Queue *q, const uint8_t **data_out, size_t *len_out)
|
||||||
|
{
|
||||||
|
if (q->count == 0) return 0;
|
||||||
|
*data_out = q->items[q->head].data;
|
||||||
|
*len_out = q->items[q->head].len;
|
||||||
|
q->head = (q->head + 1) % MAX_ENVELOPES;
|
||||||
|
q->count--;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Delivery callback: userdata is the sending client's Queue*.
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
static int32_t deliver_cb(
|
||||||
|
const uint8_t *addr_ptr, size_t addr_len,
|
||||||
|
const uint8_t *data_ptr, size_t data_len,
|
||||||
|
void *userdata)
|
||||||
|
{
|
||||||
|
(void)addr_ptr; (void)addr_len; /* routing is done manually below */
|
||||||
|
queue_push((Queue *)userdata, data_ptr, data_len);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Helper: pop one envelope from `outbox` and push it into `receiver`.
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
static void route(
|
||||||
|
const ClientOps *ops,
|
||||||
|
Queue *outbox,
|
||||||
|
ClientHandle_t *receiver,
|
||||||
|
PushInboundResult_t *out)
|
||||||
|
{
|
||||||
|
const uint8_t *data;
|
||||||
|
size_t len;
|
||||||
|
int ok = queue_pop(outbox, &data, &len);
|
||||||
|
assert(ok && "expected an envelope in the outbox");
|
||||||
|
ops->push_inbound(receiver, data, len, out);
|
||||||
|
assert(out->error_code == CLIENT_FFI_OK && "push_inbound failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Entry point called from the Rust test harness.
|
||||||
|
* Returns 0 on success, non-zero on failure.
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
int run_alice_bob_test(const ClientOps *ops)
|
||||||
|
{
|
||||||
|
Queue alice_outbox, bob_outbox;
|
||||||
|
queue_init(&alice_outbox);
|
||||||
|
queue_init(&bob_outbox);
|
||||||
|
|
||||||
|
/* ---- Create clients ------------------------------------------------- */
|
||||||
|
/* Each client's delivery callback pushes to its own outbox. */
|
||||||
|
ClientHandle_t *alice = ops->create(
|
||||||
|
(const uint8_t *)"alice", 5, deliver_cb, &alice_outbox);
|
||||||
|
ClientHandle_t *bob = ops->create(
|
||||||
|
(const uint8_t *)"bob", 3, deliver_cb, &bob_outbox);
|
||||||
|
|
||||||
|
assert(alice && "create returned NULL for alice");
|
||||||
|
assert(bob && "create returned NULL for bob");
|
||||||
|
|
||||||
|
/* ---- Bob generates an intro bundle ---------------------------------- */
|
||||||
|
CreateIntroResult_t bob_intro;
|
||||||
|
memset(&bob_intro, 0, sizeof(bob_intro));
|
||||||
|
ops->create_intro_bundle(bob, &bob_intro);
|
||||||
|
assert(bob_intro.error_code == CLIENT_FFI_OK);
|
||||||
|
|
||||||
|
/* ---- Alice initiates a conversation with Bob ------------------------ */
|
||||||
|
CreateConvoResult_t alice_convo;
|
||||||
|
memset(&alice_convo, 0, sizeof(alice_convo));
|
||||||
|
ops->create_conversation(
|
||||||
|
alice,
|
||||||
|
bob_intro.intro_bytes_ptr, bob_intro.intro_bytes_len,
|
||||||
|
(const uint8_t *)"hello bob", 9,
|
||||||
|
&alice_convo);
|
||||||
|
assert(alice_convo.error_code == CLIENT_FFI_OK);
|
||||||
|
ops->destroy_intro_result(&bob_intro);
|
||||||
|
|
||||||
|
/* ---- Route alice -> bob --------------------------------------------- */
|
||||||
|
PushInboundResult_t recv;
|
||||||
|
memset(&recv, 0, sizeof(recv));
|
||||||
|
route(ops, &alice_outbox, bob, &recv);
|
||||||
|
|
||||||
|
assert(recv.has_content && "expected content from alice");
|
||||||
|
assert(recv.is_new_convo && "expected new-conversation flag");
|
||||||
|
assert(recv.content_len == 9);
|
||||||
|
assert(memcmp(recv.content_ptr, "hello bob", 9) == 0);
|
||||||
|
|
||||||
|
/* Save Bob's convo_id for replies */
|
||||||
|
uint8_t bob_cid[256];
|
||||||
|
size_t bob_cid_len = recv.convo_id_len;
|
||||||
|
assert(bob_cid_len < sizeof(bob_cid));
|
||||||
|
memcpy(bob_cid, recv.convo_id_ptr, bob_cid_len);
|
||||||
|
ops->destroy_inbound_result(&recv);
|
||||||
|
|
||||||
|
/* ---- Bob replies ---------------------------------------------------- */
|
||||||
|
int32_t rc = ops->send_message(
|
||||||
|
bob, bob_cid, bob_cid_len,
|
||||||
|
(const uint8_t *)"hi alice", 8);
|
||||||
|
assert(rc == CLIENT_FFI_OK);
|
||||||
|
|
||||||
|
/* Route bob -> alice */
|
||||||
|
memset(&recv, 0, sizeof(recv));
|
||||||
|
route(ops, &bob_outbox, alice, &recv);
|
||||||
|
assert(recv.has_content && "expected content from bob");
|
||||||
|
assert(!recv.is_new_convo && "unexpected new-convo flag");
|
||||||
|
assert(recv.content_len == 8);
|
||||||
|
assert(memcmp(recv.content_ptr, "hi alice", 8) == 0);
|
||||||
|
ops->destroy_inbound_result(&recv);
|
||||||
|
|
||||||
|
/* ---- Multiple back-and-forth rounds --------------------------------- */
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
char msg[32];
|
||||||
|
int mlen = snprintf(msg, sizeof(msg), "msg %d", i);
|
||||||
|
|
||||||
|
rc = ops->send_message(
|
||||||
|
alice,
|
||||||
|
alice_convo.convo_id_ptr, alice_convo.convo_id_len,
|
||||||
|
(const uint8_t *)msg, (size_t)mlen);
|
||||||
|
assert(rc == CLIENT_FFI_OK);
|
||||||
|
|
||||||
|
memset(&recv, 0, sizeof(recv));
|
||||||
|
route(ops, &alice_outbox, bob, &recv);
|
||||||
|
assert(recv.has_content);
|
||||||
|
assert((int)recv.content_len == mlen);
|
||||||
|
assert(memcmp(recv.content_ptr, msg, (size_t)mlen) == 0);
|
||||||
|
|
||||||
|
char reply[32];
|
||||||
|
int rlen = snprintf(reply, sizeof(reply), "reply %d", i);
|
||||||
|
|
||||||
|
rc = ops->send_message(
|
||||||
|
bob, bob_cid, bob_cid_len,
|
||||||
|
(const uint8_t *)reply, (size_t)rlen);
|
||||||
|
assert(rc == CLIENT_FFI_OK);
|
||||||
|
ops->destroy_inbound_result(&recv);
|
||||||
|
|
||||||
|
memset(&recv, 0, sizeof(recv));
|
||||||
|
route(ops, &bob_outbox, alice, &recv);
|
||||||
|
assert(recv.has_content);
|
||||||
|
assert((int)recv.content_len == rlen);
|
||||||
|
assert(memcmp(recv.content_ptr, reply, (size_t)rlen) == 0);
|
||||||
|
ops->destroy_inbound_result(&recv);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Cleanup -------------------------------------------------------- */
|
||||||
|
ops->destroy_convo_result(&alice_convo);
|
||||||
|
ops->destroy(alice);
|
||||||
|
ops->destroy(bob);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
88
crates/c-test/c/delivery.c
Normal file
88
crates/c-test/c/delivery.c
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* C delivery implementation for the c-test platform.
|
||||||
|
*
|
||||||
|
* Provides a simple fixed-size queue and a delivery callback. The queue acts
|
||||||
|
* as the outbox for a client: every outbound envelope is pushed here; the test
|
||||||
|
* (orchestrated from Rust) pops envelopes and routes them to the receiving
|
||||||
|
* client via client_push_inbound.
|
||||||
|
*
|
||||||
|
* This file has NO dependencies on the client-ffi or libchat Rust crates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Queue implementation
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
#define MAX_ENVELOPES 64
|
||||||
|
#define MAX_ENVELOPE_SZ 32768
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t data[MAX_ENVELOPE_SZ];
|
||||||
|
size_t len;
|
||||||
|
} Envelope;
|
||||||
|
|
||||||
|
typedef struct Queue {
|
||||||
|
Envelope items[MAX_ENVELOPES];
|
||||||
|
int head;
|
||||||
|
int tail;
|
||||||
|
int count;
|
||||||
|
} Queue;
|
||||||
|
|
||||||
|
Queue *c_queue_new(void)
|
||||||
|
{
|
||||||
|
Queue *q = (Queue *)calloc(1, sizeof(Queue));
|
||||||
|
assert(q && "out of memory");
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
void c_queue_free(Queue *q)
|
||||||
|
{
|
||||||
|
free(q);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pop the oldest envelope from the queue.
|
||||||
|
*
|
||||||
|
* @param q Queue to pop from.
|
||||||
|
* @param data_out Set to a pointer into the queue's internal buffer.
|
||||||
|
* Valid only until the next push or free.
|
||||||
|
* @param len_out Set to the envelope length in bytes.
|
||||||
|
* @return 1 if an envelope was available, 0 if the queue was empty.
|
||||||
|
*/
|
||||||
|
int c_queue_pop(Queue *q, const uint8_t **data_out, size_t *len_out)
|
||||||
|
{
|
||||||
|
if (q->count == 0) return 0;
|
||||||
|
*data_out = q->items[q->head].data;
|
||||||
|
*len_out = q->items[q->head].len;
|
||||||
|
q->head = (q->head + 1) % MAX_ENVELOPES;
|
||||||
|
q->count--;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Delivery callback
|
||||||
|
*
|
||||||
|
* `userdata` must be a Queue* for the sending client's outbox.
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
int32_t c_deliver_cb(
|
||||||
|
const uint8_t *addr_ptr, size_t addr_len,
|
||||||
|
const uint8_t *data_ptr, size_t data_len,
|
||||||
|
void *userdata)
|
||||||
|
{
|
||||||
|
(void)addr_ptr; (void)addr_len; /* routing is done externally */
|
||||||
|
Queue *q = (Queue *)userdata;
|
||||||
|
assert(q->count < MAX_ENVELOPES && "delivery queue overflow");
|
||||||
|
assert(data_len <= MAX_ENVELOPE_SZ && "envelope too large for queue");
|
||||||
|
memcpy(q->items[q->tail].data, data_ptr, data_len);
|
||||||
|
q->items[q->tail].len = data_len;
|
||||||
|
q->tail = (q->tail + 1) % MAX_ENVELOPES;
|
||||||
|
q->count++;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
86
crates/c-test/src/lib.rs
Normal file
86
crates/c-test/src/lib.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/// c-test platform — delivery implemented in C, protocol via client-ffi.
|
||||||
|
///
|
||||||
|
/// The C test code (`c/alice_bob_test.c`) receives a `ClientOps` vtable
|
||||||
|
/// populated here with all the client-ffi function pointers. This keeps
|
||||||
|
/// the C archive free of direct Rust-symbol references, which avoids the
|
||||||
|
/// linker-ordering problem (C archives are searched before Rust rlibs).
|
||||||
|
/// The Rust side explicitly names every function in the vtable, so Rust's
|
||||||
|
/// DCE cannot eliminate them from the binary.
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::ffi::c_void;
|
||||||
|
|
||||||
|
use client_ffi::{
|
||||||
|
ClientHandle, CreateConvoResult, CreateIntroResult, DeliverFn, PushInboundResult,
|
||||||
|
client_create, client_create_conversation, client_create_intro_bundle, client_destroy,
|
||||||
|
client_push_inbound, client_send_message, destroy_create_convo_result,
|
||||||
|
destroy_create_intro_result, destroy_push_inbound_result,
|
||||||
|
};
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// ClientOps vtable — must match the C definition in client_ffi.h exactly.
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct ClientOps {
|
||||||
|
create: unsafe extern "C" fn(
|
||||||
|
*const u8, usize,
|
||||||
|
Option<DeliverFn>,
|
||||||
|
*mut c_void,
|
||||||
|
) -> *mut ClientHandle,
|
||||||
|
destroy: unsafe extern "C" fn(*mut ClientHandle),
|
||||||
|
create_intro_bundle:
|
||||||
|
unsafe extern "C" fn(*mut ClientHandle, *mut CreateIntroResult),
|
||||||
|
destroy_intro_result: unsafe extern "C" fn(*mut CreateIntroResult),
|
||||||
|
create_conversation: unsafe extern "C" fn(
|
||||||
|
*mut ClientHandle,
|
||||||
|
*const u8, usize,
|
||||||
|
*const u8, usize,
|
||||||
|
*mut CreateConvoResult,
|
||||||
|
),
|
||||||
|
destroy_convo_result: unsafe extern "C" fn(*mut CreateConvoResult),
|
||||||
|
send_message: unsafe extern "C" fn(
|
||||||
|
*mut ClientHandle,
|
||||||
|
*const u8, usize,
|
||||||
|
*const u8, usize,
|
||||||
|
) -> i32,
|
||||||
|
push_inbound: unsafe extern "C" fn(
|
||||||
|
*mut ClientHandle,
|
||||||
|
*const u8, usize,
|
||||||
|
*mut PushInboundResult,
|
||||||
|
),
|
||||||
|
destroy_inbound_result: unsafe extern "C" fn(*mut PushInboundResult),
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn run_alice_bob_test(ops: *const ClientOps) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Test
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// End-to-end Alice-Bob exchange.
|
||||||
|
///
|
||||||
|
/// Delivery is fully in C (`c/alice_bob_test.c`): queue management,
|
||||||
|
/// callback, and test orchestration. The vtable below pins every
|
||||||
|
/// client-ffi symbol into the binary so neither the Rust compiler
|
||||||
|
/// nor the linker can drop them.
|
||||||
|
#[test]
|
||||||
|
fn alice_bob_c_delivery() {
|
||||||
|
let ops = ClientOps {
|
||||||
|
create: client_create,
|
||||||
|
destroy: client_destroy,
|
||||||
|
create_intro_bundle: client_create_intro_bundle,
|
||||||
|
destroy_intro_result: destroy_create_intro_result,
|
||||||
|
create_conversation: client_create_conversation,
|
||||||
|
destroy_convo_result: destroy_create_convo_result,
|
||||||
|
send_message: client_send_message,
|
||||||
|
push_inbound: client_push_inbound,
|
||||||
|
destroy_inbound_result: destroy_push_inbound_result,
|
||||||
|
};
|
||||||
|
let result = unsafe { run_alice_bob_test(&ops) };
|
||||||
|
assert_eq!(result, 0, "C alice-bob test returned non-zero");
|
||||||
|
}
|
||||||
|
}
|
||||||
11
crates/client-ffi/Cargo.toml
Normal file
11
crates/client-ffi/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "client-ffi"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["staticlib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
client = { path = "../client" }
|
||||||
|
libchat = { workspace = true }
|
||||||
207
crates/client-ffi/client_ffi.h
Normal file
207
crates/client-ffi/client_ffi.h
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
#ifndef CLIENT_FFI_H
|
||||||
|
#define CLIENT_FFI_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Opaque handle returned by client_create.
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
typedef struct ClientHandle ClientHandle_t;
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* C delivery callback.
|
||||||
|
*
|
||||||
|
* Called for each outbound envelope. `addr_ptr/addr_len` is the
|
||||||
|
* delivery address (UTF-8, not NUL-terminated). `data_ptr/data_len`
|
||||||
|
* is the encrypted payload. `userdata` is the pointer supplied to
|
||||||
|
* client_create. Return 0 on success, negative on error.
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
typedef int32_t (*DeliverFn_t)(
|
||||||
|
const uint8_t *addr_ptr, size_t addr_len,
|
||||||
|
const uint8_t *data_ptr, size_t data_len,
|
||||||
|
void *userdata);
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Error codes (match crates/client-ffi/src/api.rs ErrorCode)
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
#define CLIENT_FFI_OK 0
|
||||||
|
#define CLIENT_FFI_ERR_BAD_UTF8 (-1)
|
||||||
|
#define CLIENT_FFI_ERR_BAD_INTRO (-2)
|
||||||
|
#define CLIENT_FFI_ERR_DELIVERY (-3)
|
||||||
|
#define CLIENT_FFI_ERR_UNKNOWN (-4)
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Result structs — all heap-allocated fields are owned by the caller
|
||||||
|
* and must be freed via the matching destroy_* function.
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int32_t error_code;
|
||||||
|
uint8_t *intro_bytes_ptr;
|
||||||
|
size_t intro_bytes_len;
|
||||||
|
} CreateIntroResult_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int32_t error_code;
|
||||||
|
/* UTF-8, NOT NUL-terminated */
|
||||||
|
uint8_t *convo_id_ptr;
|
||||||
|
size_t convo_id_len;
|
||||||
|
} CreateConvoResult_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int32_t error_code;
|
||||||
|
bool has_content;
|
||||||
|
bool is_new_convo;
|
||||||
|
/* UTF-8, NOT NUL-terminated. NULL when has_content is false. */
|
||||||
|
uint8_t *convo_id_ptr;
|
||||||
|
size_t convo_id_len;
|
||||||
|
/* NULL when has_content is false. */
|
||||||
|
uint8_t *content_ptr;
|
||||||
|
size_t content_len;
|
||||||
|
} PushInboundResult_t;
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Lifecycle
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an ephemeral in-memory client.
|
||||||
|
*
|
||||||
|
* @param name_ptr UTF-8 installation name (need not be NUL-terminated).
|
||||||
|
* @param name_len Byte length of name_ptr.
|
||||||
|
* @param callback Non-null delivery callback.
|
||||||
|
* @param userdata Opaque pointer forwarded to each callback invocation.
|
||||||
|
* @return Opaque handle, or NULL on error. Free with client_destroy.
|
||||||
|
*/
|
||||||
|
ClientHandle_t *client_create(
|
||||||
|
const uint8_t *name_ptr, size_t name_len,
|
||||||
|
DeliverFn_t callback,
|
||||||
|
void *userdata);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy a client handle. Must not be used after this call.
|
||||||
|
*/
|
||||||
|
void client_destroy(ClientHandle_t *handle);
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Identity
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a pointer to the installation name bytes (UTF-8, not NUL-terminated).
|
||||||
|
* The pointer is valid until the next mutating call or client_destroy.
|
||||||
|
* The caller must NOT free it.
|
||||||
|
*
|
||||||
|
* @param out_len Receives the byte length of the returned string.
|
||||||
|
*/
|
||||||
|
const uint8_t *client_installation_name(const ClientHandle_t *handle, size_t *out_len);
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Intro bundle
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a serialised introduction bundle for out-of-band sharing.
|
||||||
|
* Always call destroy_create_intro_result when done.
|
||||||
|
*/
|
||||||
|
void client_create_intro_bundle(ClientHandle_t *handle, CreateIntroResult_t *out);
|
||||||
|
|
||||||
|
/** Free the result from client_create_intro_bundle. */
|
||||||
|
void destroy_create_intro_result(CreateIntroResult_t *result);
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Create conversation
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an intro bundle and initiate a private conversation.
|
||||||
|
* Outbound envelopes are dispatched through the delivery callback.
|
||||||
|
* Always call destroy_create_convo_result when done.
|
||||||
|
*/
|
||||||
|
void client_create_conversation(
|
||||||
|
ClientHandle_t *handle,
|
||||||
|
const uint8_t *bundle_ptr, size_t bundle_len,
|
||||||
|
const uint8_t *content_ptr, size_t content_len,
|
||||||
|
CreateConvoResult_t *out);
|
||||||
|
|
||||||
|
/** Free the result from client_create_conversation. */
|
||||||
|
void destroy_create_convo_result(CreateConvoResult_t *result);
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Send message
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt content and dispatch outbound envelopes.
|
||||||
|
*
|
||||||
|
* @param convo_id_ptr UTF-8 conversation ID (not NUL-terminated).
|
||||||
|
* @return CLIENT_FFI_OK on success, error code otherwise.
|
||||||
|
*/
|
||||||
|
int32_t client_send_message(
|
||||||
|
ClientHandle_t *handle,
|
||||||
|
const uint8_t *convo_id_ptr, size_t convo_id_len,
|
||||||
|
const uint8_t *content_ptr, size_t content_len);
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* Push inbound
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt an inbound payload. has_content is false for protocol frames.
|
||||||
|
* Always call destroy_push_inbound_result when error_code == CLIENT_FFI_OK.
|
||||||
|
*/
|
||||||
|
void client_push_inbound(
|
||||||
|
ClientHandle_t *handle,
|
||||||
|
const uint8_t *payload_ptr, size_t payload_len,
|
||||||
|
PushInboundResult_t *out);
|
||||||
|
|
||||||
|
/** Free the result from client_push_inbound. */
|
||||||
|
void destroy_push_inbound_result(PushInboundResult_t *result);
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* ClientOps — vtable used by c-test so C can call the Rust client-ffi
|
||||||
|
* functions indirectly via function pointers supplied by the Rust harness.
|
||||||
|
*
|
||||||
|
* This avoids the linker-ordering problem that arises when C archives
|
||||||
|
* are searched before Rust rlibs: the C code has no direct undefined
|
||||||
|
* references to Rust symbols; it only uses the pointers it receives.
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
ClientHandle_t *(*create)(
|
||||||
|
const uint8_t *name_ptr, size_t name_len,
|
||||||
|
DeliverFn_t callback, void *userdata);
|
||||||
|
void (*destroy)(ClientHandle_t *handle);
|
||||||
|
void (*create_intro_bundle)(ClientHandle_t *handle, CreateIntroResult_t *out);
|
||||||
|
void (*destroy_intro_result)(CreateIntroResult_t *result);
|
||||||
|
void (*create_conversation)(
|
||||||
|
ClientHandle_t *handle,
|
||||||
|
const uint8_t *bundle_ptr, size_t bundle_len,
|
||||||
|
const uint8_t *content_ptr, size_t content_len,
|
||||||
|
CreateConvoResult_t *out);
|
||||||
|
void (*destroy_convo_result)(CreateConvoResult_t *result);
|
||||||
|
int32_t (*send_message)(
|
||||||
|
ClientHandle_t *handle,
|
||||||
|
const uint8_t *convo_id_ptr, size_t convo_id_len,
|
||||||
|
const uint8_t *content_ptr, size_t content_len);
|
||||||
|
void (*push_inbound)(
|
||||||
|
ClientHandle_t *handle,
|
||||||
|
const uint8_t *payload_ptr, size_t payload_len,
|
||||||
|
PushInboundResult_t *out);
|
||||||
|
void (*destroy_inbound_result)(PushInboundResult_t *result);
|
||||||
|
} ClientOps;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* CLIENT_FFI_H */
|
||||||
399
crates/client-ffi/src/api.rs
Normal file
399
crates/client-ffi/src/api.rs
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use client::{ChatClient, ClientError};
|
||||||
|
|
||||||
|
use crate::delivery::{CDelivery, DeliverFn};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Opaque client handle
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub struct ClientHandle(pub(crate) ChatClient<CDelivery>);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Memory helpers: leak a Vec<u8> / String as raw pointer + length, and the
|
||||||
|
// corresponding safe reconstruction for frees. Box<[u8]> is used so that
|
||||||
|
// capacity == length, which simplifies round-tripping via from_raw.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn leak_bytes(v: Vec<u8>) -> (*mut u8, usize) {
|
||||||
|
let mut b = v.into_boxed_slice();
|
||||||
|
let len = b.len();
|
||||||
|
let ptr = b.as_mut_ptr();
|
||||||
|
std::mem::forget(b);
|
||||||
|
(ptr, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// `ptr` must have been returned by `leak_bytes` or `leak_string` with the same `len`.
|
||||||
|
unsafe fn free_bytes(ptr: *mut u8, len: usize) {
|
||||||
|
if !ptr.is_null() && len > 0 {
|
||||||
|
unsafe {
|
||||||
|
drop(Box::from_raw(std::slice::from_raw_parts_mut(ptr, len)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leak_string(s: String) -> (*mut u8, usize) {
|
||||||
|
leak_bytes(s.into_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Error codes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum ErrorCode {
|
||||||
|
None = 0,
|
||||||
|
BadUtf8 = -1,
|
||||||
|
BadIntro = -2,
|
||||||
|
DeliveryFail = -3,
|
||||||
|
UnknownError = -4,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Result structs
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Output of client_create_intro_bundle.
|
||||||
|
/// Free with destroy_create_intro_result.
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct CreateIntroResult {
|
||||||
|
pub error_code: i32,
|
||||||
|
/// Heap-allocated; free via destroy_create_intro_result.
|
||||||
|
pub intro_bytes_ptr: *mut u8,
|
||||||
|
pub intro_bytes_len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Output of client_create_conversation.
|
||||||
|
/// Free with destroy_create_convo_result.
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct CreateConvoResult {
|
||||||
|
pub error_code: i32,
|
||||||
|
/// UTF-8, not NUL-terminated. Free via destroy_create_convo_result.
|
||||||
|
pub convo_id_ptr: *mut u8,
|
||||||
|
pub convo_id_len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Output of client_push_inbound.
|
||||||
|
/// Free with destroy_push_inbound_result when error_code == 0.
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct PushInboundResult {
|
||||||
|
pub error_code: i32,
|
||||||
|
/// False when the payload was a protocol frame with no user content.
|
||||||
|
pub has_content: bool,
|
||||||
|
pub is_new_convo: bool,
|
||||||
|
/// UTF-8, not NUL-terminated. Null when has_content is false.
|
||||||
|
pub convo_id_ptr: *mut u8,
|
||||||
|
pub convo_id_len: usize,
|
||||||
|
/// Null when has_content is false.
|
||||||
|
pub content_ptr: *mut u8,
|
||||||
|
pub content_len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Lifecycle
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Create an ephemeral in-memory client. Identity is lost on drop.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// - `name_ptr` must point to `name_len` valid UTF-8 bytes.
|
||||||
|
/// - `callback` must be non-null.
|
||||||
|
/// - `userdata` is threaded to every callback invocation; caller ensures its validity.
|
||||||
|
/// - The returned pointer must be freed with `client_destroy`.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn client_create(
|
||||||
|
name_ptr: *const u8,
|
||||||
|
name_len: usize,
|
||||||
|
callback: Option<DeliverFn>,
|
||||||
|
userdata: *mut core::ffi::c_void,
|
||||||
|
) -> *mut ClientHandle {
|
||||||
|
let name = unsafe {
|
||||||
|
match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return std::ptr::null_mut(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Some(callback) = callback else { return std::ptr::null_mut() };
|
||||||
|
let delivery = CDelivery { callback, userdata };
|
||||||
|
Box::into_raw(Box::new(ClientHandle(ChatClient::new(name, delivery))))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Free a client handle returned by `client_create`.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// - `handle` must be a valid non-null pointer from `client_create`.
|
||||||
|
/// - `handle` must not be used after this call.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn client_destroy(handle: *mut ClientHandle) {
|
||||||
|
if !handle.is_null() {
|
||||||
|
unsafe { drop(Box::from_raw(handle)) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Identity
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Return the installation name as a pointer to UTF-8 bytes (not NUL-terminated).
|
||||||
|
/// The `*out_len` is set to the length. The pointer is valid until the handle is
|
||||||
|
/// destroyed; the caller must NOT free it.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// - `handle` must be a valid non-null pointer from `client_create`.
|
||||||
|
/// - `out_len` must be a valid non-null pointer.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn client_installation_name(
|
||||||
|
handle: *const ClientHandle,
|
||||||
|
out_len: *mut usize,
|
||||||
|
) -> *const u8 {
|
||||||
|
if handle.is_null() || out_len.is_null() {
|
||||||
|
return std::ptr::null();
|
||||||
|
}
|
||||||
|
let name = unsafe { (*handle).0.installation_name() };
|
||||||
|
unsafe { *out_len = name.len() };
|
||||||
|
name.as_ptr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Intro bundle
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Produce a serialised introduction bundle for out-of-band sharing.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// - `handle` and `out` must be valid non-null pointers.
|
||||||
|
/// - Call `destroy_create_intro_result` when done.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn client_create_intro_bundle(
|
||||||
|
handle: *mut ClientHandle,
|
||||||
|
out: *mut CreateIntroResult,
|
||||||
|
) {
|
||||||
|
if handle.is_null() || out.is_null() {
|
||||||
|
if !out.is_null() {
|
||||||
|
unsafe {
|
||||||
|
*out = CreateIntroResult {
|
||||||
|
error_code: ErrorCode::UnknownError as i32,
|
||||||
|
intro_bytes_ptr: std::ptr::null_mut(),
|
||||||
|
intro_bytes_len: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
*out = match (*handle).0.create_intro_bundle() {
|
||||||
|
Ok(bytes) => {
|
||||||
|
let (ptr, len) = leak_bytes(bytes);
|
||||||
|
CreateIntroResult { error_code: ErrorCode::None as i32, intro_bytes_ptr: ptr, intro_bytes_len: len }
|
||||||
|
}
|
||||||
|
Err(_) => CreateIntroResult {
|
||||||
|
error_code: ErrorCode::UnknownError as i32,
|
||||||
|
intro_bytes_ptr: std::ptr::null_mut(),
|
||||||
|
intro_bytes_len: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Free the result from `client_create_intro_bundle`.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `result` must point to a `CreateIntroResult` previously populated by
|
||||||
|
/// `client_create_intro_bundle`.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn destroy_create_intro_result(result: *mut CreateIntroResult) {
|
||||||
|
if result.is_null() { return; }
|
||||||
|
unsafe {
|
||||||
|
let r = &mut *result;
|
||||||
|
free_bytes(r.intro_bytes_ptr, r.intro_bytes_len);
|
||||||
|
r.intro_bytes_ptr = std::ptr::null_mut();
|
||||||
|
r.intro_bytes_len = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Create conversation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Parse an intro bundle and initiate a private conversation.
|
||||||
|
/// Outbound envelopes are dispatched through the delivery callback.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// - All pointer parameters must be valid for the stated lengths.
|
||||||
|
/// - Call `destroy_create_convo_result` when done.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn client_create_conversation(
|
||||||
|
handle: *mut ClientHandle,
|
||||||
|
bundle_ptr: *const u8,
|
||||||
|
bundle_len: usize,
|
||||||
|
content_ptr: *const u8,
|
||||||
|
content_len: usize,
|
||||||
|
out: *mut CreateConvoResult,
|
||||||
|
) {
|
||||||
|
if handle.is_null() || out.is_null() {
|
||||||
|
if !out.is_null() {
|
||||||
|
unsafe {
|
||||||
|
*out = CreateConvoResult {
|
||||||
|
error_code: ErrorCode::UnknownError as i32,
|
||||||
|
convo_id_ptr: std::ptr::null_mut(),
|
||||||
|
convo_id_len: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
let bundle = std::slice::from_raw_parts(bundle_ptr, bundle_len);
|
||||||
|
let content = std::slice::from_raw_parts(content_ptr, content_len);
|
||||||
|
*out = match (*handle).0.create_conversation(bundle, content) {
|
||||||
|
Ok(convo_id) => {
|
||||||
|
let (ptr, len) = leak_string(convo_id.to_string());
|
||||||
|
CreateConvoResult { error_code: ErrorCode::None as i32, convo_id_ptr: ptr, convo_id_len: len }
|
||||||
|
}
|
||||||
|
Err(ClientError::Chat(_)) => CreateConvoResult {
|
||||||
|
error_code: ErrorCode::BadIntro as i32,
|
||||||
|
convo_id_ptr: std::ptr::null_mut(),
|
||||||
|
convo_id_len: 0,
|
||||||
|
},
|
||||||
|
Err(ClientError::Delivery(_)) => CreateConvoResult {
|
||||||
|
error_code: ErrorCode::DeliveryFail as i32,
|
||||||
|
convo_id_ptr: std::ptr::null_mut(),
|
||||||
|
convo_id_len: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Free the result from `client_create_conversation`.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `result` must point to a `CreateConvoResult` previously populated by
|
||||||
|
/// `client_create_conversation`.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn destroy_create_convo_result(result: *mut CreateConvoResult) {
|
||||||
|
if result.is_null() { return; }
|
||||||
|
unsafe {
|
||||||
|
let r = &mut *result;
|
||||||
|
free_bytes(r.convo_id_ptr, r.convo_id_len);
|
||||||
|
r.convo_id_ptr = std::ptr::null_mut();
|
||||||
|
r.convo_id_len = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Send message
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Encrypt `content` and dispatch outbound envelopes. Returns an `ErrorCode`.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// - All pointer parameters must be valid for the stated lengths.
|
||||||
|
/// - `convo_id` bytes must be valid UTF-8.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn client_send_message(
|
||||||
|
handle: *mut ClientHandle,
|
||||||
|
convo_id_ptr: *const u8,
|
||||||
|
convo_id_len: usize,
|
||||||
|
content_ptr: *const u8,
|
||||||
|
content_len: usize,
|
||||||
|
) -> i32 {
|
||||||
|
if handle.is_null() { return ErrorCode::UnknownError as i32; }
|
||||||
|
unsafe {
|
||||||
|
let id_str = match std::str::from_utf8(std::slice::from_raw_parts(convo_id_ptr, convo_id_len)) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return ErrorCode::BadUtf8 as i32,
|
||||||
|
};
|
||||||
|
let convo_id: client::ConversationIdOwned = Arc::from(id_str);
|
||||||
|
let content = std::slice::from_raw_parts(content_ptr, content_len);
|
||||||
|
match (*handle).0.send_message(&convo_id, content) {
|
||||||
|
Ok(()) => ErrorCode::None as i32,
|
||||||
|
Err(ClientError::Delivery(_)) => ErrorCode::DeliveryFail as i32,
|
||||||
|
Err(_) => ErrorCode::UnknownError as i32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Push inbound
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Decrypt an inbound payload. `has_content` is false for protocol frames.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// - All pointer parameters must be valid for the stated lengths.
|
||||||
|
/// - Call `destroy_push_inbound_result` when error_code == 0.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn client_push_inbound(
|
||||||
|
handle: *mut ClientHandle,
|
||||||
|
payload_ptr: *const u8,
|
||||||
|
payload_len: usize,
|
||||||
|
out: *mut PushInboundResult,
|
||||||
|
) {
|
||||||
|
if handle.is_null() || out.is_null() {
|
||||||
|
if !out.is_null() {
|
||||||
|
unsafe { *out = push_inbound_error(ErrorCode::UnknownError as i32) };
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
let payload = std::slice::from_raw_parts(payload_ptr, payload_len);
|
||||||
|
*out = match (*handle).0.receive(payload) {
|
||||||
|
Ok(Some(cd)) => {
|
||||||
|
let (cid_ptr, cid_len) = leak_string(cd.conversation_id);
|
||||||
|
let (con_ptr, con_len) = leak_bytes(cd.data);
|
||||||
|
PushInboundResult {
|
||||||
|
error_code: ErrorCode::None as i32,
|
||||||
|
has_content: true,
|
||||||
|
is_new_convo: cd.is_new_convo,
|
||||||
|
convo_id_ptr: cid_ptr,
|
||||||
|
convo_id_len: cid_len,
|
||||||
|
content_ptr: con_ptr,
|
||||||
|
content_len: con_len,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => PushInboundResult {
|
||||||
|
error_code: ErrorCode::None as i32,
|
||||||
|
has_content: false,
|
||||||
|
is_new_convo: false,
|
||||||
|
convo_id_ptr: std::ptr::null_mut(),
|
||||||
|
convo_id_len: 0,
|
||||||
|
content_ptr: std::ptr::null_mut(),
|
||||||
|
content_len: 0,
|
||||||
|
},
|
||||||
|
Err(_) => push_inbound_error(ErrorCode::UnknownError as i32),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_inbound_error(error_code: i32) -> PushInboundResult {
|
||||||
|
PushInboundResult {
|
||||||
|
error_code,
|
||||||
|
has_content: false,
|
||||||
|
is_new_convo: false,
|
||||||
|
convo_id_ptr: std::ptr::null_mut(),
|
||||||
|
convo_id_len: 0,
|
||||||
|
content_ptr: std::ptr::null_mut(),
|
||||||
|
content_len: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Free the result from `client_push_inbound`.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `result` must point to a `PushInboundResult` previously populated by
|
||||||
|
/// `client_push_inbound`.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn destroy_push_inbound_result(result: *mut PushInboundResult) {
|
||||||
|
if result.is_null() { return; }
|
||||||
|
unsafe {
|
||||||
|
let r = &mut *result;
|
||||||
|
free_bytes(r.convo_id_ptr, r.convo_id_len);
|
||||||
|
free_bytes(r.content_ptr, r.content_len);
|
||||||
|
r.convo_id_ptr = std::ptr::null_mut();
|
||||||
|
r.convo_id_len = 0;
|
||||||
|
r.content_ptr = std::ptr::null_mut();
|
||||||
|
r.content_len = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
crates/client-ffi/src/delivery.rs
Normal file
40
crates/client-ffi/src/delivery.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
use libchat::AddressedEnvelope;
|
||||||
|
|
||||||
|
use client::DeliveryService;
|
||||||
|
|
||||||
|
/// C callback invoked for each outbound envelope. Return 0 on success, negative on error.
|
||||||
|
/// `userdata` is an opaque pointer threaded through without interpretation.
|
||||||
|
pub type DeliverFn = unsafe extern "C" fn(
|
||||||
|
addr_ptr: *const u8,
|
||||||
|
addr_len: usize,
|
||||||
|
data_ptr: *const u8,
|
||||||
|
data_len: usize,
|
||||||
|
userdata: *mut core::ffi::c_void,
|
||||||
|
) -> i32;
|
||||||
|
|
||||||
|
pub struct CDelivery {
|
||||||
|
pub callback: DeliverFn,
|
||||||
|
pub userdata: *mut core::ffi::c_void,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: The C caller is responsible for ensuring userdata outlives the client
|
||||||
|
// and that any concurrent access is externally synchronized.
|
||||||
|
unsafe impl Send for CDelivery {}
|
||||||
|
|
||||||
|
impl DeliveryService for CDelivery {
|
||||||
|
type Error = i32;
|
||||||
|
|
||||||
|
fn deliver(&mut self, envelope: AddressedEnvelope) -> Result<(), i32> {
|
||||||
|
let addr = envelope.delivery_address.as_bytes();
|
||||||
|
let rc = unsafe {
|
||||||
|
(self.callback)(
|
||||||
|
addr.as_ptr(),
|
||||||
|
addr.len(),
|
||||||
|
envelope.data.as_ptr(),
|
||||||
|
envelope.data.len(),
|
||||||
|
self.userdata,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if rc == 0 { Ok(()) } else { Err(rc) }
|
||||||
|
}
|
||||||
|
}
|
||||||
5
crates/client-ffi/src/lib.rs
Normal file
5
crates/client-ffi/src/lib.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
mod api;
|
||||||
|
mod delivery;
|
||||||
|
|
||||||
|
pub use api::*;
|
||||||
|
pub use delivery::*;
|
||||||
@ -78,11 +78,6 @@ impl<D: DeliveryService> ChatClient<D> {
|
|||||||
self.ctx.handle_payload(payload).map_err(Into::into)
|
self.ctx.handle_payload(payload).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Access the delivery service (e.g. to pop inbound envelopes in tests).
|
|
||||||
pub fn delivery_mut(&mut self) -> &mut D {
|
|
||||||
&mut self.delivery
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dispatch_all(
|
fn dispatch_all(
|
||||||
&mut self,
|
&mut self,
|
||||||
envelopes: Vec<AddressedEnvelope>,
|
envelopes: Vec<AddressedEnvelope>,
|
||||||
|
|||||||
@ -1,21 +1,41 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use libchat::AddressedEnvelope;
|
use libchat::AddressedEnvelope;
|
||||||
|
|
||||||
use crate::delivery::DeliveryService;
|
use crate::delivery::DeliveryService;
|
||||||
|
|
||||||
/// Mock delivery for TestPlatform. Envelopes are pushed to `inbox`; tests pop
|
/// Companion handle tests retain to observe outbound envelopes without accessing
|
||||||
/// them manually and feed bytes into the peer's `receive()`.
|
/// the client internals.
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct MockOutbox(pub Arc<Mutex<VecDeque<Vec<u8>>>>);
|
||||||
|
|
||||||
|
impl MockOutbox {
|
||||||
|
pub fn pop_front(&self) -> Option<Vec<u8>> {
|
||||||
|
self.0.lock().unwrap().pop_front()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mock delivery for TestPlatform. Envelopes are pushed to the shared outbox;
|
||||||
|
/// tests pop them via a `MockOutbox` handle and feed bytes into the peer's
|
||||||
|
/// `receive()`.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct MockDelivery {
|
pub struct MockDelivery {
|
||||||
pub inbox: VecDeque<Vec<u8>>,
|
outbox: Arc<Mutex<VecDeque<Vec<u8>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockDelivery {
|
||||||
|
/// Create a delivery wired to an existing outbox handle.
|
||||||
|
pub fn with_outbox(outbox: &MockOutbox) -> Self {
|
||||||
|
Self { outbox: Arc::clone(&outbox.0) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeliveryService for MockDelivery {
|
impl DeliveryService for MockDelivery {
|
||||||
type Error = std::convert::Infallible;
|
type Error = std::convert::Infallible;
|
||||||
|
|
||||||
fn deliver(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error> {
|
fn deliver(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error> {
|
||||||
self.inbox.push_back(envelope.data);
|
self.outbox.lock().unwrap().push_back(envelope.data);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
mod delivery;
|
mod delivery;
|
||||||
|
|
||||||
pub use delivery::MockDelivery;
|
pub use delivery::{MockDelivery, MockOutbox};
|
||||||
|
|
||||||
use crate::{client::ChatClient, platform::Platform};
|
use crate::{client::ChatClient, platform::Platform};
|
||||||
|
|
||||||
|
|||||||
@ -1,23 +1,25 @@
|
|||||||
use client::platforms::test::{MockDelivery, TestPlatform};
|
use client::platforms::test::{MockDelivery, MockOutbox};
|
||||||
use client::{ChatClient, ContentData, ConversationIdOwned, Platform, StorageConfig};
|
use client::{ChatClient, ContentData, ConversationIdOwned, StorageConfig};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn new_client(name: &str) -> ChatClient<MockDelivery> {
|
fn new_client(name: &str) -> (ChatClient<MockDelivery>, MockOutbox) {
|
||||||
TestPlatform.into_client(name).unwrap()
|
let outbox = MockOutbox::default();
|
||||||
|
let delivery = MockDelivery::with_outbox(&outbox);
|
||||||
|
(ChatClient::new(name, delivery), outbox)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pop_and_receive(
|
fn pop_and_receive(
|
||||||
sender: &mut ChatClient<MockDelivery>,
|
sender_outbox: &MockOutbox,
|
||||||
receiver: &mut ChatClient<MockDelivery>,
|
receiver: &mut ChatClient<MockDelivery>,
|
||||||
) -> Option<ContentData> {
|
) -> Option<ContentData> {
|
||||||
let raw = sender.delivery_mut().inbox.pop_front().expect("expected envelope");
|
let raw = sender_outbox.pop_front().expect("expected envelope");
|
||||||
receiver.receive(&raw).expect("receive failed")
|
receiver.receive(&raw).expect("receive failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn alice_bob_message_exchange() {
|
fn alice_bob_message_exchange() {
|
||||||
let mut alice = new_client("alice");
|
let (mut alice, alice_outbox) = new_client("alice");
|
||||||
let mut bob = new_client("bob");
|
let (mut bob, bob_outbox) = new_client("bob");
|
||||||
|
|
||||||
// Exchange intro bundles out-of-band
|
// Exchange intro bundles out-of-band
|
||||||
let bob_bundle = bob.create_intro_bundle().unwrap();
|
let bob_bundle = bob.create_intro_bundle().unwrap();
|
||||||
@ -28,7 +30,7 @@ fn alice_bob_message_exchange() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Bob receives Alice's initial message
|
// Bob receives Alice's initial message
|
||||||
let content = pop_and_receive(&mut alice, &mut bob).expect("expected content");
|
let content = pop_and_receive(&alice_outbox, &mut bob).expect("expected content");
|
||||||
assert_eq!(content.data, b"hello bob");
|
assert_eq!(content.data, b"hello bob");
|
||||||
assert!(content.is_new_convo);
|
assert!(content.is_new_convo);
|
||||||
|
|
||||||
@ -36,7 +38,7 @@ fn alice_bob_message_exchange() {
|
|||||||
|
|
||||||
// Bob replies
|
// Bob replies
|
||||||
bob.send_message(&bob_convo_id, b"hi alice").unwrap();
|
bob.send_message(&bob_convo_id, b"hi alice").unwrap();
|
||||||
let content = pop_and_receive(&mut bob, &mut alice).expect("expected content");
|
let content = pop_and_receive(&bob_outbox, &mut alice).expect("expected content");
|
||||||
assert_eq!(content.data, b"hi alice");
|
assert_eq!(content.data, b"hi alice");
|
||||||
assert!(!content.is_new_convo);
|
assert!(!content.is_new_convo);
|
||||||
|
|
||||||
@ -44,12 +46,12 @@ fn alice_bob_message_exchange() {
|
|||||||
for i in 0u8..5 {
|
for i in 0u8..5 {
|
||||||
let msg = format!("msg {i}");
|
let msg = format!("msg {i}");
|
||||||
alice.send_message(&alice_convo_id, msg.as_bytes()).unwrap();
|
alice.send_message(&alice_convo_id, msg.as_bytes()).unwrap();
|
||||||
let content = pop_and_receive(&mut alice, &mut bob).expect("expected content");
|
let content = pop_and_receive(&alice_outbox, &mut bob).expect("expected content");
|
||||||
assert_eq!(content.data, msg.as_bytes());
|
assert_eq!(content.data, msg.as_bytes());
|
||||||
|
|
||||||
let reply = format!("reply {i}");
|
let reply = format!("reply {i}");
|
||||||
bob.send_message(&bob_convo_id, reply.as_bytes()).unwrap();
|
bob.send_message(&bob_convo_id, reply.as_bytes()).unwrap();
|
||||||
let content = pop_and_receive(&mut bob, &mut alice).expect("expected content");
|
let content = pop_and_receive(&bob_outbox, &mut alice).expect("expected content");
|
||||||
assert_eq!(content.data, reply.as_bytes());
|
assert_eq!(content.data, reply.as_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user