mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-03-28 07:03:43 +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"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "c-test"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"client-ffi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.54"
|
||||
@ -127,6 +135,14 @@ dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "client-ffi"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"client",
|
||||
"libchat",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
|
||||
@ -8,6 +8,8 @@ members = [
|
||||
"core/double-ratchets",
|
||||
"core/storage",
|
||||
"crates/client",
|
||||
"crates/client-ffi",
|
||||
"crates/c-test",
|
||||
]
|
||||
|
||||
[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)
|
||||
}
|
||||
|
||||
/// 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(
|
||||
&mut self,
|
||||
envelopes: Vec<AddressedEnvelope>,
|
||||
|
||||
@ -1,21 +1,41 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use libchat::AddressedEnvelope;
|
||||
|
||||
use crate::delivery::DeliveryService;
|
||||
|
||||
/// Mock delivery for TestPlatform. Envelopes are pushed to `inbox`; tests pop
|
||||
/// them manually and feed bytes into the peer's `receive()`.
|
||||
/// Companion handle tests retain to observe outbound envelopes without accessing
|
||||
/// 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)]
|
||||
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 {
|
||||
type Error = std::convert::Infallible;
|
||||
|
||||
fn deliver(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error> {
|
||||
self.inbox.push_back(envelope.data);
|
||||
self.outbox.lock().unwrap().push_back(envelope.data);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
mod delivery;
|
||||
|
||||
pub use delivery::MockDelivery;
|
||||
pub use delivery::{MockDelivery, MockOutbox};
|
||||
|
||||
use crate::{client::ChatClient, platform::Platform};
|
||||
|
||||
|
||||
@ -1,23 +1,25 @@
|
||||
use client::platforms::test::{MockDelivery, TestPlatform};
|
||||
use client::{ChatClient, ContentData, ConversationIdOwned, Platform, StorageConfig};
|
||||
use client::platforms::test::{MockDelivery, MockOutbox};
|
||||
use client::{ChatClient, ContentData, ConversationIdOwned, StorageConfig};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn new_client(name: &str) -> ChatClient<MockDelivery> {
|
||||
TestPlatform.into_client(name).unwrap()
|
||||
fn new_client(name: &str) -> (ChatClient<MockDelivery>, MockOutbox) {
|
||||
let outbox = MockOutbox::default();
|
||||
let delivery = MockDelivery::with_outbox(&outbox);
|
||||
(ChatClient::new(name, delivery), outbox)
|
||||
}
|
||||
|
||||
fn pop_and_receive(
|
||||
sender: &mut ChatClient<MockDelivery>,
|
||||
sender_outbox: &MockOutbox,
|
||||
receiver: &mut ChatClient<MockDelivery>,
|
||||
) -> 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")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alice_bob_message_exchange() {
|
||||
let mut alice = new_client("alice");
|
||||
let mut bob = new_client("bob");
|
||||
let (mut alice, alice_outbox) = new_client("alice");
|
||||
let (mut bob, bob_outbox) = new_client("bob");
|
||||
|
||||
// Exchange intro bundles out-of-band
|
||||
let bob_bundle = bob.create_intro_bundle().unwrap();
|
||||
@ -28,7 +30,7 @@ fn alice_bob_message_exchange() {
|
||||
.unwrap();
|
||||
|
||||
// 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!(content.is_new_convo);
|
||||
|
||||
@ -36,7 +38,7 @@ fn alice_bob_message_exchange() {
|
||||
|
||||
// Bob replies
|
||||
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!(!content.is_new_convo);
|
||||
|
||||
@ -44,12 +46,12 @@ fn alice_bob_message_exchange() {
|
||||
for i in 0u8..5 {
|
||||
let msg = format!("msg {i}");
|
||||
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());
|
||||
|
||||
let reply = format!("reply {i}");
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user