mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-04-02 01:23:11 +00:00
Implement a `client` crate that wraps the `libchat` context behind a simple `ChatClient<D>` API. The delivery strategy is pluggable via a `DeliveryService` trait, with two implementations provided: - `InProcessDelivery` — shared `MessageBus` for single-process tests - `CDelivery` — C function-pointer callback for the FFI layer Add a `client-ffi` crate that exposes the client as a C API via `safer-ffi`. A `generate-headers` binary produces the companion C header. Include two runnable examples: - `examples/in-process` — Alice/Bob exchange using in-process delivery - `examples/c-ffi` — same exchange written entirely in C; smoketested under valgrind (to catch memory leaks) in CI iterates: #71
203 lines
6.7 KiB
C
203 lines
6.7 KiB
C
/*
|
|
* c-client: Alice-Bob message exchange written entirely in C.
|
|
*
|
|
* Demonstrates that the client-ffi C API is straightforward to consume
|
|
* directly — no Rust glue required. Build with the provided Makefile.
|
|
*/
|
|
|
|
#include "client_ffi.h"
|
|
|
|
#include <assert.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
|
|
/* ------------------------------------------------------------------
|
|
* Convenience macros for building slice_ref_uint8_t values.
|
|
* SLICE(p, n) — arbitrary pointer + length.
|
|
* STR(s) — string literal (length computed at compile time).
|
|
* ------------------------------------------------------------------ */
|
|
|
|
#define SLICE(p, n) ((slice_ref_uint8_t){ .ptr = (const uint8_t *)(p), .len = (n) })
|
|
#define STR(s) SLICE(s, sizeof(s) - 1)
|
|
|
|
/* ------------------------------------------------------------------
|
|
* In-memory delivery bus (shared by all clients, like InProcessDelivery)
|
|
* ------------------------------------------------------------------ */
|
|
|
|
#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 Queue bus;
|
|
|
|
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: all clients share one bus.
|
|
* ------------------------------------------------------------------ */
|
|
|
|
static int32_t deliver_cb(
|
|
const uint8_t *addr_ptr, size_t addr_len,
|
|
const uint8_t *data_ptr, size_t data_len)
|
|
{
|
|
(void)addr_ptr; (void)addr_len;
|
|
queue_push(&bus, data_ptr, data_len);
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------
|
|
* Helper: pop one envelope from the bus and push it into receiver.
|
|
* Returns a heap-allocated result; caller frees with
|
|
* push_inbound_result_free().
|
|
* ------------------------------------------------------------------ */
|
|
|
|
static PushInboundResult_t *route(ClientHandle_t *receiver)
|
|
{
|
|
const uint8_t *data;
|
|
size_t len;
|
|
int ok = queue_pop(&bus, &data, &len);
|
|
assert(ok && "expected an envelope in the bus");
|
|
PushInboundResult_t *r = client_receive(receiver, SLICE(data, len));
|
|
assert(push_inbound_result_error_code(r) == 0 && "push_inbound failed");
|
|
return r;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------
|
|
* Main
|
|
* ------------------------------------------------------------------ */
|
|
|
|
int main(void)
|
|
{
|
|
queue_init(&bus);
|
|
|
|
/* Create clients — both share the same delivery bus */
|
|
ClientHandle_t *alice = client_create(STR("alice"), deliver_cb);
|
|
ClientHandle_t *bob = client_create(STR("bob"), deliver_cb);
|
|
|
|
assert(alice && "client_create returned NULL for alice");
|
|
assert(bob && "client_create returned NULL for bob");
|
|
|
|
/* Bob generates an intro bundle */
|
|
CreateIntroResult_t *bob_intro = client_create_intro_bundle(bob);
|
|
assert(create_intro_result_error_code(bob_intro) == 0);
|
|
slice_ref_uint8_t intro_bytes = create_intro_result_bytes(bob_intro);
|
|
|
|
/* Alice initiates a conversation with Bob */
|
|
CreateConvoResult_t *alice_convo = client_create_conversation(
|
|
alice, intro_bytes, STR("hello bob"));
|
|
assert(create_convo_result_error_code(alice_convo) == 0);
|
|
create_intro_result_free(bob_intro);
|
|
|
|
/* Route alice -> bob */
|
|
PushInboundResult_t *recv = route(bob);
|
|
|
|
assert(push_inbound_result_has_content(recv) && "expected content from alice");
|
|
assert(push_inbound_result_is_new_convo(recv) && "expected new-conversation flag");
|
|
|
|
slice_ref_uint8_t content = push_inbound_result_content(recv);
|
|
assert(content.len == 9);
|
|
assert(memcmp(content.ptr, "hello bob", 9) == 0);
|
|
printf("Bob received: \"%.*s\"\n", (int)content.len, content.ptr);
|
|
|
|
/* Copy Bob's convo_id before freeing recv */
|
|
slice_ref_uint8_t cid_ref = push_inbound_result_convo_id(recv);
|
|
uint8_t bob_cid[256];
|
|
size_t bob_cid_len = cid_ref.len;
|
|
if (bob_cid_len >= sizeof(bob_cid)) {
|
|
fprintf(stderr, "conversation id too long (%zu bytes)\n", bob_cid_len);
|
|
return 1;
|
|
}
|
|
memcpy(bob_cid, cid_ref.ptr, bob_cid_len);
|
|
push_inbound_result_free(recv);
|
|
|
|
/* Bob replies */
|
|
ErrorCode_t rc = client_send_message(
|
|
bob, SLICE(bob_cid, bob_cid_len), STR("hi alice"));
|
|
assert(rc == ERROR_CODE_NONE);
|
|
|
|
recv = route(alice);
|
|
assert(push_inbound_result_has_content(recv) && "expected content from bob");
|
|
assert(!push_inbound_result_is_new_convo(recv) && "unexpected new-convo flag");
|
|
content = push_inbound_result_content(recv);
|
|
assert(content.len == 8);
|
|
assert(memcmp(content.ptr, "hi alice", 8) == 0);
|
|
printf("Alice received: \"%.*s\"\n", (int)content.len, content.ptr);
|
|
push_inbound_result_free(recv);
|
|
|
|
/* Multiple back-and-forth rounds */
|
|
slice_ref_uint8_t alice_cid = create_convo_result_id(alice_convo);
|
|
for (int i = 0; i < 3; i++) {
|
|
char msg[32];
|
|
int mlen = snprintf(msg, sizeof(msg), "msg %d", i);
|
|
|
|
rc = client_send_message(alice, alice_cid, SLICE(msg, (size_t)mlen));
|
|
assert(rc == ERROR_CODE_NONE);
|
|
|
|
recv = route(bob);
|
|
assert(push_inbound_result_has_content(recv));
|
|
content = push_inbound_result_content(recv);
|
|
assert((int)content.len == mlen);
|
|
assert(memcmp(content.ptr, msg, (size_t)mlen) == 0);
|
|
push_inbound_result_free(recv);
|
|
|
|
char reply[32];
|
|
int rlen = snprintf(reply, sizeof(reply), "reply %d", i);
|
|
|
|
rc = client_send_message(
|
|
bob, SLICE(bob_cid, bob_cid_len), SLICE(reply, (size_t)rlen));
|
|
assert(rc == ERROR_CODE_NONE);
|
|
|
|
recv = route(alice);
|
|
assert(push_inbound_result_has_content(recv));
|
|
content = push_inbound_result_content(recv);
|
|
assert((int)content.len == rlen);
|
|
assert(memcmp(content.ptr, reply, (size_t)rlen) == 0);
|
|
push_inbound_result_free(recv);
|
|
}
|
|
|
|
/* Cleanup */
|
|
create_convo_result_free(alice_convo);
|
|
client_destroy(alice);
|
|
client_destroy(bob);
|
|
|
|
printf("Message exchange complete.\n");
|
|
return 0;
|
|
}
|