Files
quicproquo/docs/src/getting-started/ffi.md
Christian Nennemann 2e081ead8e chore: rename quicproquo → quicprochat in docs, Docker, CI, and packaging
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
2026-03-21 19:14:06 +01:00

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

  1. QpqHandle is heap-allocated by qpc_connect and freed by qpc_disconnect. Do not use the handle after disconnecting.
  2. out_json from qpc_receive is heap-allocated. Free it with qpc_free_string.
  3. qpc_last_error returns a pointer owned by the handle. Do not free it; it is valid until the next FFI call on the same handle.
  4. 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.