Rename all project references from quicproquo/qpq to quicprochat/qpc across documentation, Docker configuration, CI workflows, packaging scripts, operational configs, and build tooling. - Docker: crate paths, binary names, user/group, data dirs, env vars - CI: workflow crate references, binary names, artifact names - Docs: all markdown files under docs/, SDK READMEs, book.toml - Packaging: OpenWrt Makefile, init script, UCI config (file renames) - Scripts: justfile, dev-shell, screenshot, cross-compile, ai_team - Operations: Prometheus config, alert rules, Grafana dashboard - Config: .env.example (QPQ_* → QPC_*), CODEOWNERS paths - Top-level: README, CONTRIBUTING, ROADMAP, CLAUDE.md
7.8 KiB
C FFI Bindings
The quicprochat-ffi crate provides a synchronous C API for the quicprochat
messaging client. It wraps the async quicprochat-client library behind an
opaque handle, so C, Python, Swift, or any language with C FFI support can
connect, authenticate, send messages, and receive messages.
Each QpqHandle owns a Tokio runtime internally; FFI functions use
runtime.block_on() to bridge synchronous C callers to the async Rust
internals.
Building
# Shared library (.so / .dylib / .dll) + static archive (.a)
cargo build --release -p quicprochat-ffi
Output:
| Platform | Shared library | Static library |
|---|---|---|
| Linux | target/release/libquicprochat_ffi.so |
target/release/libquicprochat_ffi.a |
| macOS | target/release/libquicprochat_ffi.dylib |
target/release/libquicprochat_ffi.a |
| Windows | target/release/quicprochat_ffi.dll |
target/release/quicprochat_ffi.lib |
Status Codes
| Code | Constant | Meaning |
|---|---|---|
| 0 | QPC_OK |
Success |
| 1 | QPC_ERROR |
Generic error (check qpc_last_error) |
| 2 | QPC_AUTH_FAILED |
OPAQUE authentication failed |
| 3 | QPC_TIMEOUT |
Receive timed out with no messages |
| 4 | QPC_NOT_CONNECTED |
Handle is null or not logged in |
C API Reference
All functions use the extern "C" calling convention. All string parameters
must be valid, non-null, null-terminated UTF-8. The opaque handle type is
QpqHandle *.
qpc_connect
QpqHandle *qpc_connect(
const char *server, /* "host:port", e.g. "127.0.0.1:7000" */
const char *ca_cert, /* path to CA certificate file (DER) */
const char *server_name /* TLS server name, e.g. "localhost" */
);
Creates a Tokio runtime, performs a health check against the server, and
returns a heap-allocated opaque handle. Returns NULL on failure (invalid
arguments, server unreachable, or runtime creation failed).
qpc_login
int32_t qpc_login(
QpqHandle *handle, /* handle from qpc_connect */
const char *username, /* OPAQUE username */
const char *password /* OPAQUE password */
);
Authenticates with the server using OPAQUE (password-authenticated key
exchange). On success the handle is marked as logged-in and subsequent
qpc_send/qpc_receive calls use the authenticated session.
Returns: QPC_OK on success, QPC_AUTH_FAILED on bad credentials,
QPC_NOT_CONNECTED if the handle is null, or QPC_ERROR on other failures.
qpc_send
int32_t qpc_send(
QpqHandle *handle, /* handle from qpc_connect */
const char *recipient, /* recipient username (null-terminated) */
const uint8_t *message, /* message bytes (UTF-8, not null-terminated) */
size_t message_len /* length of message in bytes */
);
Resolves the recipient by username, then sends an MLS-encrypted message
through the server. The message buffer must contain valid UTF-8 of at least
message_len bytes. The handle must be logged in.
Returns: QPC_OK on success, QPC_NOT_CONNECTED if not logged in, or
QPC_ERROR on failure (recipient not found, network error, etc.).
qpc_receive
int32_t qpc_receive(
QpqHandle *handle, /* handle from qpc_connect */
uint32_t timeout_ms, /* maximum wait time in milliseconds */
char **out_json /* output: heap-allocated JSON string */
);
Blocks up to timeout_ms milliseconds waiting for pending messages. On
success, *out_json points to a null-terminated JSON string containing an
array of decrypted message strings (e.g., ["hello","world"]). The caller
must free this string with qpc_free_string.
Returns: QPC_OK on success (even if the array is empty),
QPC_TIMEOUT if the wait expires with no messages, QPC_NOT_CONNECTED if
not logged in, or QPC_ERROR on failure.
qpc_disconnect
void qpc_disconnect(QpqHandle *handle);
Shuts down the Tokio runtime and frees the handle. After this call, the
handle must not be used again. Passing NULL is a safe no-op.
qpc_last_error
const char *qpc_last_error(const QpqHandle *handle);
Returns the last error message recorded on the handle, or NULL if no error
has occurred. The returned pointer is valid only until the next FFI call
on the same handle. Do not free this pointer -- it is owned by the handle.
qpc_free_string
void qpc_free_string(char *ptr);
Frees a string previously returned by qpc_receive via the out_json
output parameter. Passing NULL is a safe no-op. Do not use this to
free strings from qpc_last_error.
Memory Management Rules
QpqHandleis heap-allocated byqpc_connectand freed byqpc_disconnect. Do not use the handle after disconnecting.out_jsonfromqpc_receiveis heap-allocated. Free it withqpc_free_string.qpc_last_errorreturns a pointer owned by the handle. Do not free it; it is valid until the next FFI call on the same handle.- All
const char *input parameters are borrowed for the duration of the call and not stored beyond it.
Error Handling Pattern
Every function that returns int32_t uses the status codes above. The
recommended pattern is:
int rc = qpc_login(handle, "alice", "password123");
if (rc != QPC_OK) {
const char *err = qpc_last_error(handle);
fprintf(stderr, "login failed (code %d): %s\n", rc, err ? err : "unknown");
qpc_disconnect(handle);
return 1;
}
Example: C Usage
#include <stdio.h>
#include <string.h>
/* Link with: -lquicprochat_ffi -lpthread -ldl -lm */
typedef struct QpqHandle QpqHandle;
extern QpqHandle *qpc_connect(const char *, const char *, const char *);
extern int qpc_login(QpqHandle *, const char *, const char *);
extern int qpc_send(QpqHandle *, const char *, const unsigned char *, unsigned long);
extern int qpc_receive(QpqHandle *, unsigned int, char **);
extern void qpc_disconnect(QpqHandle *);
extern const char *qpc_last_error(const QpqHandle *);
extern void qpc_free_string(char *);
#define QPC_OK 0
int main(void) {
QpqHandle *h = qpc_connect("127.0.0.1:7000", "server-cert.der", "localhost");
if (!h) {
fprintf(stderr, "connection failed\n");
return 1;
}
if (qpc_login(h, "alice", "secret") != QPC_OK) {
fprintf(stderr, "login failed: %s\n", qpc_last_error(h));
qpc_disconnect(h);
return 1;
}
/* Send a message */
const char *msg = "hello from C";
if (qpc_send(h, "bob", (const unsigned char *)msg, strlen(msg)) != QPC_OK) {
fprintf(stderr, "send failed: %s\n", qpc_last_error(h));
}
/* Receive messages (5 second timeout) */
char *json = NULL;
int rc = qpc_receive(h, 5000, &json);
if (rc == QPC_OK && json) {
printf("received: %s\n", json);
qpc_free_string(json);
}
qpc_disconnect(h);
return 0;
}
Compile and link:
gcc -o qpc_demo qpc_demo.c -L target/release -lquicprochat_ffi -lpthread -ldl -lm
LD_LIBRARY_PATH=target/release ./qpc_demo
Python Bindings
A ready-made Python ctypes wrapper is provided in
examples/python/qpc_client.py.
# Build the FFI library first
cargo build --release -p quicprochat-ffi
# Run the Python client
python examples/python/qpc_client.py \
--server 127.0.0.1:7000 \
--ca-cert server-cert.der \
--username alice --password secret \
--receive --timeout 5000
Set QPC_FFI_LIB=/path/to/libquicprochat_ffi.so to override automatic
library discovery.