Skip to main content

foldit_plugin_sdk/
abi.rs

1//! C ABI for native plugins.
2//!
3//! Native plugins are shared libraries (`lib{id}.{dylib,so,dll}`) that
4//! export the symbol `foldit_plugin_vtable` returning a pointer to a
5//! [`FolditPluginVtable`]. The orchestrator (host) loads the dylib via
6//! `libloading`, reads the vtable, and dispatches into the plugin
7//! through the function pointers; no IPC, no proto serialization on
8//! hot paths.
9//!
10//! ## ABI version
11//!
12//! [`FolditPluginVtable::abi_version`] is checked by the host on load.
13//! Bump it whenever the vtable layout changes; old plugins fail to
14//! load with a clear error.
15//!
16//! ## Memory ownership
17//!
18//! - **Plugin to host buffers** (`FolditPluginBuffer` from `register`, `score`,
19//!   `invoke`, `query`, `poll_stream`): plugin allocates, host frees via
20//!   [`FolditPluginVtable::free_buffer`].
21//! - **Plugin to host errors** (`FolditPluginError` filled when a method returns
22//!   `FOLDIT_PLUGIN_ERR`): plugin allocates the inner `code` / `message`
23//!   buffers, host frees the whole struct via
24//!   [`FolditPluginVtable::free_error`].
25//! - **Host to plugin buffers** (assembly bytes, params bytes, session context):
26//!   plugin must NOT retain pointers past the call return. Copy if needed.
27//!
28//! ## Threading
29//!
30//! Calls into a single plugin instance are serialized by the host (the
31//! orchestrator owns each `Box<dyn Plugin>` exclusively). Plugin
32//! authors don't need to make their internals thread-safe across
33//! method calls, but each call may run on a different OS thread, so
34//! per-instance state must not assume a single thread.
35
36#![allow(non_camel_case_types)]
37
38use std::os::raw::{c_char, c_void};
39
40/// Current ABI version. Bump on any layout change.
41pub const FOLDIT_PLUGIN_ABI_VERSION: u32 = 7;
42
43/// Payload tag for [`FolditPluginVtable::update_assembly`].
44///
45/// `Full` carries fresh assembly bytes; discard prior state, decode and
46/// install. `Delta` carries delta bytes; decode via molex's
47/// `molex_delta_to_edits` and apply incrementally; preserves derived
48/// plugin state across mutations. Plugins that don't track incremental
49/// state may treat `Delta` the same as `Full` by reconstituting the
50/// assembly from the decoded edits.
51#[repr(u8)]
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum FolditPluginAssemblyPayloadKind {
54    /// Payload is a fresh assembly snapshot.
55    Full = 0,
56    /// Payload is a delta edit list.
57    Delta = 1,
58}
59
60/// Status code returned by every fallible vtable method.
61#[repr(u32)]
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum FolditPluginStatus {
64    /// Success. Out-parameters are valid.
65    Ok = 0,
66    /// Plugin returned an op-level error. `out_err` is populated; host
67    /// frees it via `free_error`.
68    Err = 1,
69    /// Plugin doesn't implement this method. Out-parameters are
70    /// untouched. (E.g. a plugin without streaming returns this from
71    /// `start_stream`.)
72    Unsupported = 2,
73}
74
75/// Plugin-allocated byte buffer. Host frees via
76/// [`FolditPluginVtable::free_buffer`].
77#[repr(C)]
78#[derive(Debug)]
79pub struct FolditPluginBuffer {
80    /// Pointer to the bytes. Null + `len = 0` for an empty buffer.
81    pub data: *mut u8,
82    /// Byte length of the valid region pointed to by `data`.
83    pub len: usize,
84    /// Capacity (typically `Vec` capacity); used by the plugin's
85    /// `free_buffer` to reconstruct the original allocation.
86    pub capacity: usize,
87}
88
89impl FolditPluginBuffer {
90    /// An empty buffer with no allocation. Safe to pass to
91    /// `free_buffer` (which should be a no-op on null `data`).
92    #[must_use]
93    pub const fn empty() -> Self {
94        Self {
95            data: std::ptr::null_mut(),
96            len: 0,
97            capacity: 0,
98        }
99    }
100}
101
102/// Plugin-allocated error payload. Host frees via
103/// [`FolditPluginVtable::free_error`].
104#[repr(C)]
105#[derive(Debug)]
106pub struct FolditPluginError {
107    /// UTF-8 machine-readable error code (e.g. `"INVALID_INPUT"`).
108    pub code: FolditPluginBuffer,
109    /// UTF-8 human-readable message.
110    pub message: FolditPluginBuffer,
111}
112
113impl FolditPluginError {
114    /// An empty error with null `code` and `message` buffers. Safe to
115    /// pass to `free_error`.
116    #[must_use]
117    pub const fn empty() -> Self {
118        Self {
119            code: FolditPluginBuffer::empty(),
120            message: FolditPluginBuffer::empty(),
121        }
122    }
123}
124
125/// Mirror of `proto::Vec3`.
126#[repr(C)]
127#[derive(Debug, Clone, Copy)]
128pub struct FolditPluginVec3 {
129    /// X component.
130    pub x: f32,
131    /// Y component.
132    pub y: f32,
133    /// Z component.
134    pub z: f32,
135}
136
137/// Mirror of `proto::ResidueRef`.
138#[repr(C)]
139#[derive(Debug, Clone, Copy)]
140pub struct FolditPluginResidueRef {
141    /// Entity the residue belongs to.
142    pub entity_id: u64,
143    /// 0-indexed residue within `entity_id`.
144    pub residue_index: u32,
145    /// Padding to align `entity_id` slots in arrays.
146    pub padding: u32,
147}
148
149/// Mirror of `proto::DispatchContext`. Borrowed view; host owns the
150/// `selection` array for the duration of the call.
151#[repr(C)]
152#[derive(Debug)]
153pub struct FolditPluginDispatchContext {
154    /// 0 = no focused entity, 1 = `focused_entity_id` is valid.
155    pub has_focused_entity: u8,
156    /// Padding so `focused_entity_id` is 8-aligned.
157    pub padding: [u8; 7],
158    /// Focused entity id when `has_focused_entity == 1`; otherwise
159    /// undefined.
160    pub focused_entity_id: u64,
161    /// Pointer to a host-owned array of `selection_len` residue refs.
162    /// May be null when `selection_len == 0`.
163    pub selection: *const FolditPluginResidueRef,
164    /// Number of entries in `selection`.
165    pub selection_len: usize,
166    /// Pointer to a host-owned array of `designable_len` residue refs: the
167    /// residues the plugin may redesign (the puzzle's design mask). May be
168    /// null when `designable_len == 0`.
169    pub designable: *const FolditPluginResidueRef,
170    /// Number of entries in `designable`.
171    pub designable_len: usize,
172}
173
174/// Tag for [`FolditPluginParamValue`]. Mirrors `proto::ParamType`.
175#[repr(u32)]
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum FolditPluginParamTag {
178    /// Default / unset; treated as "skip this param" by the plugin.
179    Unspecified = 0,
180    /// `int_value` is valid.
181    Int = 1,
182    /// `float_value` is valid.
183    Float = 2,
184    /// `bool_value` is valid.
185    Bool = 3,
186    /// UTF-8 string (also used for ENUM-typed params).
187    String = 4,
188    /// `vec3_value` is valid.
189    Vec3 = 5,
190}
191
192/// Mirror of `proto::ParamValue`. Tagged struct with all variant
193/// fields inlined (avoids `repr(C) union` portability concerns and
194/// keeps cbindgen happy).
195#[repr(C)]
196#[derive(Debug)]
197pub struct FolditPluginParamValue {
198    /// Discriminator selecting which payload field is valid.
199    pub tag: FolditPluginParamTag,
200    /// Padding for 8-alignment.
201    pub padding: u32,
202    /// Valid when `tag == Int`.
203    pub int_value: i32,
204    /// Valid when `tag == Float`.
205    pub float_value: f32,
206    /// Valid when `tag == Bool` (0 / 1).
207    pub bool_value: u8,
208    /// Padding to keep the following pointer 8-aligned.
209    pub padding2: [u8; 7],
210    /// UTF-8 string body when `tag == String`. Borrowed; not
211    /// null-terminated.
212    pub string_data: *const u8,
213    /// Byte length of `string_data`.
214    pub string_len: usize,
215    /// Valid when `tag == Vec3`.
216    pub vec3_value: FolditPluginVec3,
217}
218
219/// One entry in a parameter map. Borrowed view; the host owns the
220/// underlying memory for the duration of the call.
221#[repr(C)]
222#[derive(Debug)]
223pub struct FolditPluginParamEntry {
224    /// UTF-8 key; not null-terminated.
225    pub key_data: *const u8,
226    /// Byte length of `key_data`.
227    pub key_len: usize,
228    /// The parameter value.
229    pub value: FolditPluginParamValue,
230}
231
232/// One puzzle asset delivered at Init: a name plus its raw bytes.
233///
234/// Borrowed view; the host owns the underlying memory for the duration
235/// of the `init` call. The name carries the original filename (extension
236/// included) so the plugin can sniff the asset's format.
237#[repr(C)]
238#[derive(Debug)]
239pub struct FolditPluginAsset {
240    /// UTF-8 asset name (original filename); not null-terminated.
241    pub name_data: *const u8,
242    /// Byte length of `name_data`.
243    pub name_len: usize,
244    /// Pointer to the asset bytes.
245    pub data: *const u8,
246    /// Byte length of `data`.
247    pub data_len: usize,
248}
249
250/// Plugin opaque handle. The plugin allocates this in `create` and
251/// frees it in `destroy`. The host treats it as opaque and threads it
252/// through every other call.
253pub type FolditPluginHandle = *mut c_void;
254
255/// Function-pointer table exported by every native plugin dylib.
256///
257/// The plugin exports a single C symbol (`foldit_plugin_vtable`) that
258/// returns `*const FolditPluginVtable`. The host calls this once at
259/// load time, validates `abi_version`, and stores the pointer.
260#[repr(C)]
261pub struct FolditPluginVtable {
262    /// MUST equal [`FOLDIT_PLUGIN_ABI_VERSION`] - host rejects the
263    /// dylib otherwise.
264    pub abi_version: u32,
265    /// Padding to 8-align the following function pointers.
266    pub padding: u32,
267
268    // Lifecycle
269    /// Construct a plugin instance from a UTF-8 JSON-encoded config
270    /// dict. Returns null on failure.
271    pub create:
272        unsafe extern "C" fn(config_json: *const c_char, config_len: usize) -> FolditPluginHandle,
273
274    /// Free the plugin instance. Called once; safe to assume no
275    /// in-flight calls when invoked.
276    pub destroy: unsafe extern "C" fn(handle: FolditPluginHandle),
277
278    // Required protocol endpoints
279    /// Returns serialized `proto::PluginRegistration` (registration is
280    /// nested + only called once per session, so paying the proto cost
281    /// here is fine).
282    pub register: unsafe extern "C" fn(
283        handle: FolditPluginHandle,
284        out_buf: *mut FolditPluginBuffer,
285        out_err: *mut FolditPluginError,
286    ) -> FolditPluginStatus,
287
288    /// Open a session with the initial assembly bytes. Writes the
289    /// assigned session id to `*out_session` on success. `assets`
290    /// carries the puzzle assets (e.g. a density map, ligand params) as
291    /// borrowed name+bytes views valid only for this call. Also writes
292    /// assembly bytes of the assembly the plugin settled on after any
293    /// post-Init normalization (e.g. Rosetta builds a full-atom pose
294    /// from the input, which may add missing atoms, hydrogens, or
295    /// terminal O, changing the atom count) into `*out_initial_buf`.
296    /// Plugins with no normalization step write an empty buffer; host
297    /// then keeps its input assembly. Host owns the buffer afterward
298    /// (released via the same `free_buffer` path as `register`/`score`).
299    pub init: unsafe extern "C" fn(
300        handle: FolditPluginHandle,
301        assembly: *const u8,
302        assembly_len: usize,
303        assets: *const FolditPluginAsset,
304        assets_len: usize,
305        params: *const FolditPluginParamEntry,
306        params_len: usize,
307        out_session: *mut u64,
308        out_initial_buf: *mut FolditPluginBuffer,
309        out_err: *mut FolditPluginError,
310    ) -> FolditPluginStatus,
311
312    /// Push an Assembly update to a session. The payload is either a
313    /// full assembly snapshot (`payload_kind = Full`) or a delta edit
314    /// list (`payload_kind = Delta`). `from_gen` / `to_gen` are the
315    /// host's broadcast generation counters; a plugin whose local gen
316    /// doesn't match `from_gen` should arm a `STALE_GEN` error to
317    /// return on its next dispatch so the host re-syncs.
318    pub update_assembly: unsafe extern "C" fn(
319        handle: FolditPluginHandle,
320        session: u64,
321        payload_kind: FolditPluginAssemblyPayloadKind,
322        bytes: *const u8,
323        bytes_len: usize,
324        from_gen: u64,
325        to_gen: u64,
326        out_err: *mut FolditPluginError,
327    ) -> FolditPluginStatus,
328
329    /// Tear down a session and release its per-session state.
330    pub drop_session: unsafe extern "C" fn(
331        handle: FolditPluginHandle,
332        session: u64,
333        out_err: *mut FolditPluginError,
334    ) -> FolditPluginStatus,
335
336    // Optional protocol endpoints
337    /// Run a one-shot op. Writes resulting assembly bytes (typically
338    /// delta bytes) to `*out_assembly` on success.
339    pub invoke: unsafe extern "C" fn(
340        handle: FolditPluginHandle,
341        session: u64,
342        op_id: *const u8,
343        op_id_len: usize,
344        ctx: *const FolditPluginDispatchContext,
345        params: *const FolditPluginParamEntry,
346        params_len: usize,
347        out_assembly: *mut FolditPluginBuffer,
348        out_err: *mut FolditPluginError,
349    ) -> FolditPluginStatus,
350
351    /// Start a streaming op under the host-assigned `request_id`. The
352    /// plugin keys its stream state on that id; subsequent poll / update
353    /// / cancel calls thread the same id through.
354    pub start_stream: unsafe extern "C" fn(
355        handle: FolditPluginHandle,
356        session: u64,
357        op_id: *const u8,
358        op_id_len: usize,
359        ctx: *const FolditPluginDispatchContext,
360        params: *const FolditPluginParamEntry,
361        params_len: usize,
362        request_id: u64,
363        out_err: *mut FolditPluginError,
364    ) -> FolditPluginStatus,
365
366    /// Returns serialized `proto::PollStreamResponse`; the host
367    /// decodes the variant. Centralizing the variant set in proto
368    /// keeps the C ABI surface smaller; poll_stream is the only place
369    /// where the variant tagging matters.
370    pub poll_stream: unsafe extern "C" fn(
371        handle: FolditPluginHandle,
372        request_id: u64,
373        out_buf: *mut FolditPluginBuffer,
374        out_err: *mut FolditPluginError,
375    ) -> FolditPluginStatus,
376
377    /// Apply a live parameter update to an active stream. Plugins may
378    /// coalesce or defer updates until the next poll boundary.
379    pub update_stream: unsafe extern "C" fn(
380        handle: FolditPluginHandle,
381        request_id: u64,
382        params: *const FolditPluginParamEntry,
383        params_len: usize,
384        out_err: *mut FolditPluginError,
385    ) -> FolditPluginStatus,
386
387    /// Cancel an active stream. The plugin must release stream state
388    /// before the next poll returns `Final` or `Error`.
389    pub cancel_stream: unsafe extern "C" fn(
390        handle: FolditPluginHandle,
391        request_id: u64,
392        out_err: *mut FolditPluginError,
393    ) -> FolditPluginStatus,
394
395    /// Run a read-only query (no assembly mutation). When `assembly` is
396    /// non-null (`assembly_len > 0`) it names a specific composition to
397    /// read/score instead of the session: committed heads or a
398    /// checkpoint; null/0 operates on the session / its in-flight
399    /// snapshot. Result bytes are op-defined (e.g. a serialized
400    /// `proto::ScoreReport` for the `"score"` query); the caller parses
401    /// them against the query contract.
402    pub query: unsafe extern "C" fn(
403        handle: FolditPluginHandle,
404        session: u64,
405        query_id: *const u8,
406        query_id_len: usize,
407        ctx: *const FolditPluginDispatchContext,
408        params: *const FolditPluginParamEntry,
409        params_len: usize,
410        assembly: *const u8,
411        assembly_len: usize,
412        out_data: *mut FolditPluginBuffer,
413        out_err: *mut FolditPluginError,
414    ) -> FolditPluginStatus,
415
416    // Memory cleanup
417    /// Free a plugin-allocated buffer. No-op when `data` is null.
418    pub free_buffer: unsafe extern "C" fn(buf: *mut FolditPluginBuffer),
419    /// Free both inner buffers of a plugin-allocated error struct.
420    pub free_error: unsafe extern "C" fn(err: *mut FolditPluginError),
421}
422
423/// Symbol name the host looks up via `dlsym`. Returns
424/// `*const FolditPluginVtable`.
425pub const VTABLE_SYMBOL: &[u8] = b"foldit_plugin_vtable\0";
426
427/// Type signature of the `foldit_plugin_vtable` entry symbol.
428pub type FolditPluginVtableFn = unsafe extern "C" fn() -> *const FolditPluginVtable;