# 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 ```bash # 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` ```c 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` ```c 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` ```c 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` ```c 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` ```c 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` ```c 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` ```c 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: ```c 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 ```c #include #include /* 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: ```bash 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`](https://github.com/nickvidal/quicprochat/tree/main/examples/python). ```bash # 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.