//! quicprochat server plugin API — C-ABI vtable. //! //! # Overview //! //! Every plugin is a `cdylib` that exports one symbol: //! //! ```c //! extern "C" int32_t qpc_plugin_init(HookVTable *vtable); //! ``` //! //! The server passes a zeroed [`HookVTable`] to the init function. The plugin //! fills in whichever function pointers it cares about and returns `0` on //! success or a negative error code on failure. Unused slots remain null and //! the server treats them as no-ops. //! //! # Wire types //! //! All event structs are passed by const pointer across the FFI boundary. The //! server owns the memory; plugin code must not store these pointers beyond the //! duration of the callback. //! //! # Thread safety //! //! Hook callbacks are called from the Tokio worker thread that handles the RPC. //! Plugins must be `Send + Sync` in practice (the server wraps them in `Arc`). //! Global plugin state should be guarded with `Mutex` or `RwLock` if mutable. //! //! # Return values //! //! Hooks that can reject an operation return an `i32` result code. The server maps //! [`HOOK_CONTINUE`] to allow and any other value to reject, reading the reason //! string from [`HookVTable::error_message`]. #![no_std] /// Plugin init returned success. pub const PLUGIN_OK: i32 = 0; /// Hook should allow the operation to proceed. pub const HOOK_CONTINUE: i32 = 0; /// Hook wants to reject the operation. Fill [`HookVTable::error_message`] with /// a null-terminated reason before returning this. pub const HOOK_REJECT: i32 = 1; // ── Event structs (C-compatible) ───────────────────────────────────────────── /// Event data for message enqueue operations. /// /// Passed by pointer to [`HookVTable::on_message_enqueue`]. /// All pointer fields are valid for the duration of the callback only. #[repr(C)] pub struct CMessageEvent { /// Sender's Ed25519 identity key (32 bytes), or null if sealed sender. pub sender_identity: *const u8, /// Length of `sender_identity`; 0 when null. pub sender_identity_len: usize, /// Recipient's Ed25519 identity key (32 bytes). pub recipient_key: *const u8, pub recipient_key_len: usize, /// Channel ID (16 bytes). pub channel_id: *const u8, pub channel_id_len: usize, /// Length of the encrypted payload. pub payload_len: usize, /// Server-assigned sequence number. pub seq: u64, } /// Event data for authentication operations. #[repr(C)] pub struct CAuthEvent { /// Null-terminated username string. pub username: *const u8, pub username_len: usize, /// Non-zero on success. pub success: i32, /// Null-terminated failure reason (empty on success). pub failure_reason: *const u8, pub failure_reason_len: usize, } /// Event data for channel creation operations. #[repr(C)] pub struct CChannelEvent { pub channel_id: *const u8, pub channel_id_len: usize, pub initiator_key: *const u8, pub initiator_key_len: usize, pub peer_key: *const u8, pub peer_key_len: usize, /// Non-zero if this is a freshly created channel. pub was_new: i32, } /// Event data for message fetch operations. #[repr(C)] pub struct CFetchEvent { pub recipient_key: *const u8, pub recipient_key_len: usize, pub channel_id: *const u8, pub channel_id_len: usize, pub message_count: usize, } // ── HookVTable ──────────────────────────────────────────────────────────────── /// C-ABI function-pointer table filled by the plugin's `qpc_plugin_init` export. /// /// All fields default to null (no-op). The server only calls a slot when its /// pointer is non-null. The `user_data` field is passed as the first argument /// to every hook; use it to thread plugin-private state without global variables. #[repr(C)] pub struct HookVTable { /// Opaque pointer to plugin-private state. The server passes this as the /// first argument to every hook callback. May be null. pub user_data: *mut core::ffi::c_void, /// Called before a message is stored. Return [`HOOK_CONTINUE`] or /// [`HOOK_REJECT`]. On reject, populate `error_message`. pub on_message_enqueue: Option< unsafe extern "C" fn( user_data: *mut core::ffi::c_void, event: *const CMessageEvent, ) -> i32, >, /// Called after a batch of messages is enqueued (fire-and-forget, no return value). pub on_batch_enqueue: Option< unsafe extern "C" fn( user_data: *mut core::ffi::c_void, events: *const CMessageEvent, count: usize, ), >, /// Called after a login attempt (fire-and-forget). pub on_auth: Option< unsafe extern "C" fn( user_data: *mut core::ffi::c_void, event: *const CAuthEvent, ), >, /// Called after a channel is created or looked up (fire-and-forget). pub on_channel_created: Option< unsafe extern "C" fn( user_data: *mut core::ffi::c_void, event: *const CChannelEvent, ), >, /// Called after messages are fetched (fire-and-forget). pub on_fetch: Option< unsafe extern "C" fn( user_data: *mut core::ffi::c_void, event: *const CFetchEvent, ), >, /// Called after a user completes OPAQUE registration (fire-and-forget). pub on_user_registered: Option< unsafe extern "C" fn( user_data: *mut core::ffi::c_void, username: *const u8, username_len: usize, identity_key: *const u8, identity_key_len: usize, ), >, /// When a hook returns [`HOOK_REJECT`], the server calls this to retrieve /// the null-terminated rejection reason. The returned pointer must remain /// valid until the next call on the same `user_data`. May be null (server /// will use a generic message). pub error_message: Option< unsafe extern "C" fn(user_data: *mut core::ffi::c_void) -> *const u8, >, /// Called by the server when it is done with this plugin (shutdown). /// Release resources / join threads here. May be null. pub destroy: Option, /// Called when the server is shutting down, before connections are closed. /// Plugins can use this to flush buffers, close external connections, etc. /// May be null (server treats it as a no-op). pub on_shutdown: Option, } // SAFETY: `HookVTable` contains raw pointers (`user_data`, function pointers) // which are not inherently `Send`/`Sync`. These impls are sound because: // // 1. `user_data` is an opaque pointer managed entirely by the plugin. The plugin // contract (documented in the module-level doc comment) requires that plugins // use interior mutability (Mutex/RwLock) if `user_data` is mutated through // callbacks. The server wraps each loaded plugin in an `Arc` and // may invoke hooks from any Tokio worker thread. // // 2. All function pointers are `unsafe extern "C" fn` — they are plain addresses // with no captured state. The code they point to must be thread-safe per the // plugin contract. // // 3. The server guarantees that `destroy` is called exactly once during shutdown, // after which no further hook calls are made on the vtable. #[allow(unsafe_code)] unsafe impl Send for HookVTable {} #[allow(unsafe_code)] unsafe impl Sync for HookVTable {}