feat: Sprint 3 — C FFI bindings, WASM compilation, Python example, SDK docs
- Create quicproquo-ffi crate with 7 extern "C" functions: connect, login, send, receive, disconnect, last_error, free_string (produces libquicproquo_ffi.so and .a) - Feature-gate quicproquo-core for WASM: identity, hybrid_kem, safety_numbers, sealed_sender, app_message, padding, transcript all compile to wasm32-unknown-unknown - Add Python ctypes example (examples/python/qpq_client.py) with QpqClient wrapper class and CLI - Add SDK documentation: FFI reference, WASM guide, qpq-gen generators - Update Dockerfile for quicproquo-ffi workspace member
This commit is contained in:
@@ -21,6 +21,9 @@
|
||||
- [Certificate Lifecycle and CA-Signed TLS](getting-started/certificate-lifecycle.md)
|
||||
- [Docker Deployment](getting-started/docker.md)
|
||||
- [Bot SDK](getting-started/bot-sdk.md)
|
||||
- [C FFI Bindings](getting-started/ffi.md)
|
||||
- [WASM Integration](getting-started/wasm.md)
|
||||
- [Code Generators (qpq-gen)](getting-started/generators.md)
|
||||
- [Demo Walkthrough: Alice and Bob](getting-started/demo-walkthrough.md)
|
||||
|
||||
---
|
||||
|
||||
243
docs/src/getting-started/ffi.md
Normal file
243
docs/src/getting-started/ffi.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# C FFI Bindings
|
||||
|
||||
The `quicproquo-ffi` crate provides a synchronous C API for the quicproquo
|
||||
messaging client. It wraps the async `quicproquo-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 quicproquo-ffi
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
| Platform | Shared library | Static library |
|
||||
|----------|---------------------------------------------|-----------------------------------------|
|
||||
| Linux | `target/release/libquicproquo_ffi.so` | `target/release/libquicproquo_ffi.a` |
|
||||
| macOS | `target/release/libquicproquo_ffi.dylib` | `target/release/libquicproquo_ffi.a` |
|
||||
| Windows | `target/release/quicproquo_ffi.dll` | `target/release/quicproquo_ffi.lib` |
|
||||
|
||||
## Status Codes
|
||||
|
||||
| Code | Constant | Meaning |
|
||||
|------|--------------------|------------------------------------------|
|
||||
| 0 | `QPQ_OK` | Success |
|
||||
| 1 | `QPQ_ERROR` | Generic error (check `qpq_last_error`) |
|
||||
| 2 | `QPQ_AUTH_FAILED` | OPAQUE authentication failed |
|
||||
| 3 | `QPQ_TIMEOUT` | Receive timed out with no messages |
|
||||
| 4 | `QPQ_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 *`.
|
||||
|
||||
### `qpq_connect`
|
||||
|
||||
```c
|
||||
QpqHandle *qpq_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).
|
||||
|
||||
### `qpq_login`
|
||||
|
||||
```c
|
||||
int32_t qpq_login(
|
||||
QpqHandle *handle, /* handle from qpq_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
|
||||
`qpq_send`/`qpq_receive` calls use the authenticated session.
|
||||
|
||||
**Returns:** `QPQ_OK` on success, `QPQ_AUTH_FAILED` on bad credentials,
|
||||
`QPQ_NOT_CONNECTED` if the handle is null, or `QPQ_ERROR` on other failures.
|
||||
|
||||
### `qpq_send`
|
||||
|
||||
```c
|
||||
int32_t qpq_send(
|
||||
QpqHandle *handle, /* handle from qpq_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:** `QPQ_OK` on success, `QPQ_NOT_CONNECTED` if not logged in, or
|
||||
`QPQ_ERROR` on failure (recipient not found, network error, etc.).
|
||||
|
||||
### `qpq_receive`
|
||||
|
||||
```c
|
||||
int32_t qpq_receive(
|
||||
QpqHandle *handle, /* handle from qpq_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 `qpq_free_string`.
|
||||
|
||||
**Returns:** `QPQ_OK` on success (even if the array is empty),
|
||||
`QPQ_TIMEOUT` if the wait expires with no messages, `QPQ_NOT_CONNECTED` if
|
||||
not logged in, or `QPQ_ERROR` on failure.
|
||||
|
||||
### `qpq_disconnect`
|
||||
|
||||
```c
|
||||
void qpq_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.
|
||||
|
||||
### `qpq_last_error`
|
||||
|
||||
```c
|
||||
const char *qpq_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.
|
||||
|
||||
### `qpq_free_string`
|
||||
|
||||
```c
|
||||
void qpq_free_string(char *ptr);
|
||||
```
|
||||
|
||||
Frees a string previously returned by `qpq_receive` via the `out_json`
|
||||
output parameter. Passing `NULL` is a safe no-op. Do **not** use this to
|
||||
free strings from `qpq_last_error`.
|
||||
|
||||
## Memory Management Rules
|
||||
|
||||
1. **`QpqHandle`** is heap-allocated by `qpq_connect` and freed by
|
||||
`qpq_disconnect`. Do not use the handle after disconnecting.
|
||||
2. **`out_json` from `qpq_receive`** is heap-allocated. Free it with
|
||||
`qpq_free_string`.
|
||||
3. **`qpq_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 = qpq_login(handle, "alice", "password123");
|
||||
if (rc != QPQ_OK) {
|
||||
const char *err = qpq_last_error(handle);
|
||||
fprintf(stderr, "login failed (code %d): %s\n", rc, err ? err : "unknown");
|
||||
qpq_disconnect(handle);
|
||||
return 1;
|
||||
}
|
||||
```
|
||||
|
||||
## Example: C Usage
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Link with: -lquicproquo_ffi -lpthread -ldl -lm */
|
||||
|
||||
typedef struct QpqHandle QpqHandle;
|
||||
|
||||
extern QpqHandle *qpq_connect(const char *, const char *, const char *);
|
||||
extern int qpq_login(QpqHandle *, const char *, const char *);
|
||||
extern int qpq_send(QpqHandle *, const char *, const unsigned char *, unsigned long);
|
||||
extern int qpq_receive(QpqHandle *, unsigned int, char **);
|
||||
extern void qpq_disconnect(QpqHandle *);
|
||||
extern const char *qpq_last_error(const QpqHandle *);
|
||||
extern void qpq_free_string(char *);
|
||||
|
||||
#define QPQ_OK 0
|
||||
|
||||
int main(void) {
|
||||
QpqHandle *h = qpq_connect("127.0.0.1:7000", "server-cert.der", "localhost");
|
||||
if (!h) {
|
||||
fprintf(stderr, "connection failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (qpq_login(h, "alice", "secret") != QPQ_OK) {
|
||||
fprintf(stderr, "login failed: %s\n", qpq_last_error(h));
|
||||
qpq_disconnect(h);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Send a message */
|
||||
const char *msg = "hello from C";
|
||||
if (qpq_send(h, "bob", (const unsigned char *)msg, strlen(msg)) != QPQ_OK) {
|
||||
fprintf(stderr, "send failed: %s\n", qpq_last_error(h));
|
||||
}
|
||||
|
||||
/* Receive messages (5 second timeout) */
|
||||
char *json = NULL;
|
||||
int rc = qpq_receive(h, 5000, &json);
|
||||
if (rc == QPQ_OK && json) {
|
||||
printf("received: %s\n", json);
|
||||
qpq_free_string(json);
|
||||
}
|
||||
|
||||
qpq_disconnect(h);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Compile and link:
|
||||
|
||||
```bash
|
||||
gcc -o qpq_demo qpq_demo.c -L target/release -lquicproquo_ffi -lpthread -ldl -lm
|
||||
LD_LIBRARY_PATH=target/release ./qpq_demo
|
||||
```
|
||||
|
||||
## Python Bindings
|
||||
|
||||
A ready-made Python `ctypes` wrapper is provided in
|
||||
[`examples/python/qpq_client.py`](https://github.com/nickvidal/quicproquo/tree/main/examples/python).
|
||||
|
||||
```bash
|
||||
# Build the FFI library first
|
||||
cargo build --release -p quicproquo-ffi
|
||||
|
||||
# Run the Python client
|
||||
python examples/python/qpq_client.py \
|
||||
--server 127.0.0.1:7000 \
|
||||
--ca-cert server-cert.der \
|
||||
--username alice --password secret \
|
||||
--receive --timeout 5000
|
||||
```
|
||||
|
||||
Set `QPQ_FFI_LIB=/path/to/libquicproquo_ffi.so` to override automatic
|
||||
library discovery.
|
||||
171
docs/src/getting-started/generators.md
Normal file
171
docs/src/getting-started/generators.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# Code Generators (qpq-gen)
|
||||
|
||||
The `qpq-gen` CLI tool scaffolds new plugins, bots, RPC methods, and hook
|
||||
events for the quicproquo ecosystem.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cargo install --path crates/quicproquo-gen
|
||||
```
|
||||
|
||||
Or run directly from the workspace:
|
||||
|
||||
```bash
|
||||
cargo run -p quicproquo-gen -- <subcommand>
|
||||
```
|
||||
|
||||
## Subcommands
|
||||
|
||||
### `qpq-gen plugin <name>` -- Server Plugin
|
||||
|
||||
Scaffolds a standalone Cargo project for a server plugin compiled as a shared
|
||||
library (`cdylib`). The generated plugin implements the `HookVTable` C ABI
|
||||
and is loaded by the server at startup via `--plugin-dir`.
|
||||
|
||||
```bash
|
||||
qpq-gen plugin rate-limiter
|
||||
qpq-gen plugin audit-log --output /tmp/plugins
|
||||
```
|
||||
|
||||
**Generated files:**
|
||||
|
||||
```
|
||||
rate_limiter/
|
||||
Cargo.toml # cdylib crate depending on quicproquo-plugin-api
|
||||
README.md # Build and install instructions
|
||||
src/lib.rs # Plugin skeleton with qpq_plugin_init entry point
|
||||
```
|
||||
|
||||
The template includes:
|
||||
- `qpq_plugin_init` -- called by the server on load; populates the `HookVTable`
|
||||
- `on_message_enqueue` -- sample hook that rejects payloads larger than 1 MB
|
||||
- `error_message` -- returns the rejection reason as a C string
|
||||
- `destroy` -- frees the plugin state
|
||||
|
||||
**What to customize:** Replace the `on_message_enqueue` logic with your own
|
||||
policy. Add more hooks by setting additional fields on the `HookVTable`
|
||||
(`on_auth`, `on_channel_created`, `on_fetch`, `on_user_registered`,
|
||||
`on_batch_enqueue`).
|
||||
|
||||
**Build and install:**
|
||||
|
||||
```bash
|
||||
cd rate_limiter
|
||||
cargo build --release
|
||||
cp target/release/librate_limiter.so /path/to/plugins/
|
||||
qpq-server --plugin-dir /path/to/plugins/
|
||||
```
|
||||
|
||||
### `qpq-gen bot <name>` -- Bot Project
|
||||
|
||||
Scaffolds a standalone bot project using the Bot SDK. The generated binary
|
||||
connects to a quicproquo server, authenticates via OPAQUE, and runs a
|
||||
message-handling loop.
|
||||
|
||||
```bash
|
||||
qpq-gen bot echo-bot
|
||||
qpq-gen bot moderation-bot --output /tmp/bots
|
||||
```
|
||||
|
||||
**Generated files:**
|
||||
|
||||
```
|
||||
moderation_bot/
|
||||
Cargo.toml # Binary crate depending on quicproquo-bot + tokio
|
||||
README.md # Quick-start and command reference
|
||||
src/main.rs # Bot skeleton with handle_message dispatcher
|
||||
```
|
||||
|
||||
The template ships with four built-in commands as examples:
|
||||
|
||||
| Command | Description |
|
||||
|-----------------|---------------------------|
|
||||
| `!help` | List available commands |
|
||||
| `!echo <text>` | Echo back the text |
|
||||
| `!whoami` | Show the sender's username|
|
||||
| `!ping` | Respond with "pong!" |
|
||||
|
||||
**Configuration** is read from environment variables:
|
||||
|
||||
| Variable | Default |
|
||||
|-------------------|----------------------|
|
||||
| `QPQ_SERVER` | `127.0.0.1:7000` |
|
||||
| `QPQ_USERNAME` | `<bot-name>` |
|
||||
| `QPQ_PASSWORD` | `changeme` |
|
||||
| `QPQ_CA_CERT` | `server-cert.der` |
|
||||
| `QPQ_STATE_PATH` | `<bot-name>-state.bin` |
|
||||
|
||||
**What to customize:** Edit the `handle_message` function in `src/main.rs`
|
||||
to add your own command handlers. Return `Some(response)` to reply, or
|
||||
`None` to stay silent.
|
||||
|
||||
**Run:**
|
||||
|
||||
```bash
|
||||
cd moderation_bot
|
||||
QPQ_SERVER=127.0.0.1:7000 \
|
||||
QPQ_USERNAME=moderation_bot \
|
||||
QPQ_PASSWORD=changeme \
|
||||
QPQ_CA_CERT=path/to/server-cert.der \
|
||||
cargo run
|
||||
```
|
||||
|
||||
### `qpq-gen rpc <Name>` -- RPC Method Guide
|
||||
|
||||
Prints a step-by-step guide for adding a new Cap'n Proto RPC method to the
|
||||
server. This generator does not create files; it outputs instructions and
|
||||
code snippets to copy into the appropriate locations.
|
||||
|
||||
```bash
|
||||
qpq-gen rpc listChannels
|
||||
```
|
||||
|
||||
The `Name` argument should be in camelCase (e.g., `listChannels`). The
|
||||
generator derives the `snake_case` form automatically for file and function
|
||||
names.
|
||||
|
||||
**Steps covered:**
|
||||
|
||||
1. **Schema** -- Add the method to the `interface NodeService` block in
|
||||
`schemas/node.capnp`, then rebuild with `cargo build -p quicproquo-proto`
|
||||
2. **Handler module** -- Create
|
||||
`crates/quicproquo-server/src/node_service/<name>.rs` with the handler
|
||||
implementation (template code is printed)
|
||||
3. **Registration** -- Wire the handler into `node_service/mod.rs`
|
||||
4. **Storage** (if needed) -- Add a method to the `Store` trait and implement
|
||||
it in `sql_store.rs` and `storage.rs`
|
||||
5. **Hook** (optional) -- Run `qpq-gen hook <name>` to let plugins observe
|
||||
the new RPC
|
||||
6. **Verify** -- `cargo build -p quicproquo-server && cargo test -p quicproquo-server`
|
||||
|
||||
### `qpq-gen hook <name>` -- Hook Event Guide
|
||||
|
||||
Prints a step-by-step guide for adding a new server hook event that plugins
|
||||
can observe. Like `rpc`, this generator outputs instructions rather than
|
||||
creating files.
|
||||
|
||||
```bash
|
||||
qpq-gen hook message_deleted
|
||||
```
|
||||
|
||||
The `name` argument should be in `snake_case` (e.g., `message_deleted`). The
|
||||
generator derives the `PascalCase` form for struct names.
|
||||
|
||||
**Steps covered:**
|
||||
|
||||
1. **Event struct** -- Define `MessageDeletedEvent` in
|
||||
`crates/quicproquo-server/src/hooks.rs`
|
||||
2. **Trait method** -- Add `on_message_deleted` to the `ServerHooks` trait
|
||||
with a default no-op implementation
|
||||
3. **Tracing** -- Implement the hook in `TracingHooks` with a `tracing::info!`
|
||||
call
|
||||
4. **Plugin API** -- Add a C-compatible `CMessageDeletedEvent` struct and an
|
||||
`on_message_deleted` field to `HookVTable` in
|
||||
`crates/quicproquo-plugin-api/src/lib.rs`
|
||||
5. **Plugin dispatch** -- Wire the conversion and dispatch in
|
||||
`plugin_loader.rs`
|
||||
6. **Call site** -- Fire the hook from the relevant RPC handler in
|
||||
`node_service/`
|
||||
7. **Verify** -- Build and test `quicproquo-plugin-api` and
|
||||
`quicproquo-server`
|
||||
70
docs/src/getting-started/wasm.md
Normal file
70
docs/src/getting-started/wasm.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# WASM Integration
|
||||
|
||||
The `quicproquo-core` crate supports compilation to `wasm32-unknown-unknown`
|
||||
when the `native` feature is disabled. This exposes the pure-crypto subset of
|
||||
the library for use in browsers or other WASM runtimes.
|
||||
|
||||
## Building for WASM
|
||||
|
||||
```bash
|
||||
rustup target add wasm32-unknown-unknown
|
||||
|
||||
cargo build -p quicproquo-core \
|
||||
--target wasm32-unknown-unknown \
|
||||
--no-default-features
|
||||
```
|
||||
|
||||
The `--no-default-features` flag disables the `native` feature, which gates
|
||||
MLS (openmls), OPAQUE, Cap'n Proto, and tokio -- all of which have dependencies
|
||||
that do not compile to WASM.
|
||||
|
||||
## What is available in WASM mode
|
||||
|
||||
The following modules compile to WASM and are fully functional:
|
||||
|
||||
| Module | Description |
|
||||
|-------------------|----------------------------------------------------|
|
||||
| `identity` | Ed25519 identity keypair (generate, sign, verify) |
|
||||
| `hybrid_kem` | X25519 + ML-KEM-768 hybrid key encapsulation |
|
||||
| `safety_numbers` | Signal-style safety number computation |
|
||||
| `sealed_sender` | Sender identity + Ed25519 signature envelope |
|
||||
| `app_message` | Rich application message serialisation/parsing |
|
||||
| `padding` | Message padding to hide plaintext lengths |
|
||||
| `transcript` | Encrypted tamper-evident message transcript |
|
||||
| `error` | `CoreError` type |
|
||||
|
||||
## What is NOT available in WASM mode
|
||||
|
||||
The following require the `native` feature and will not compile to WASM:
|
||||
|
||||
- `group` -- MLS group state machine (openmls)
|
||||
- `keypackage` -- MLS KeyPackage generation
|
||||
- `hybrid_crypto` -- hybrid HPKE provider for OpenMLS
|
||||
- `keystore` -- OpenMLS key store with disk persistence
|
||||
- `opaque_auth` -- OPAQUE cipher suite configuration
|
||||
|
||||
Networking (`quicproquo-client`, `quicproquo-server`) is not available in WASM.
|
||||
|
||||
## Random number generation
|
||||
|
||||
On `wasm32`, the `getrandom` crate is configured with the `js` feature to
|
||||
use the browser's `crypto.getRandomValues()` API. This is set automatically
|
||||
in `quicproquo-core/Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
```
|
||||
|
||||
This means the WASM build works in browser environments out of the box.
|
||||
For non-browser WASM runtimes (WASI, etc.), you may need to adjust the
|
||||
`getrandom` backend.
|
||||
|
||||
## Future plans
|
||||
|
||||
- **wasm-bindgen JS bindings**: Wrap the WASM-compatible modules with
|
||||
`#[wasm_bindgen]` annotations to provide a native JavaScript/TypeScript API.
|
||||
This would allow web frontends to perform client-side encryption without a
|
||||
server round-trip.
|
||||
- **wasm-pack integration**: Publish the WASM module as an npm package for
|
||||
easy consumption in web projects.
|
||||
Reference in New Issue
Block a user