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/// Borrowed view; the host owns the underlying memory for the duration
234/// of the `init` call. The name carries the original filename (extension
235/// included) so the plugin can sniff the asset's format.
236#[repr(C)]
237#[derive(Debug)]
238pub struct FolditPluginAsset {
239    /// UTF-8 asset name (original filename); not null-terminated.
240    pub name_data: *const u8,
241    /// Byte length of `name_data`.
242    pub name_len: usize,
243    /// Pointer to the asset bytes.
244    pub data: *const u8,
245    /// Byte length of `data`.
246    pub data_len: usize,
247}
248
249/// Plugin opaque handle. The plugin allocates this in `create` and
250/// frees it in `destroy`. The host treats it as opaque and threads it
251/// through every other call.
252pub type FolditPluginHandle = *mut c_void;
253
254/// Function-pointer table exported by every native plugin dylib.
255///
256/// The plugin exports a single C symbol (`foldit_plugin_vtable`) that
257/// returns `*const FolditPluginVtable`. The host calls this once at
258/// load time, validates `abi_version`, and stores the pointer.
259#[repr(C)]
260pub struct FolditPluginVtable {
261    /// MUST equal [`FOLDIT_PLUGIN_ABI_VERSION`] - host rejects the
262    /// dylib otherwise.
263    pub abi_version: u32,
264    /// Padding to 8-align the following function pointers.
265    pub padding: u32,
266
267    // Lifecycle
268    /// Construct a plugin instance from a UTF-8 JSON-encoded config
269    /// dict. Returns null on failure.
270    pub create:
271        unsafe extern "C" fn(config_json: *const c_char, config_len: usize) -> FolditPluginHandle,
272
273    /// Free the plugin instance. Called once; safe to assume no
274    /// in-flight calls when invoked.
275    pub destroy: unsafe extern "C" fn(handle: FolditPluginHandle),
276
277    // Required protocol endpoints
278    /// Returns serialized `proto::PluginRegistration` (registration is
279    /// nested + only called once per session, so paying the proto cost
280    /// here is fine).
281    pub register: unsafe extern "C" fn(
282        handle: FolditPluginHandle,
283        out_buf: *mut FolditPluginBuffer,
284        out_err: *mut FolditPluginError,
285    ) -> FolditPluginStatus,
286
287    /// Open a session with the initial assembly bytes. Writes the
288    /// assigned session id to `*out_session` on success. `assets`
289    /// carries the puzzle assets (e.g. a density map, ligand params) as
290    /// borrowed name+bytes views valid only for this call. Also writes
291    /// assembly bytes of the assembly the plugin settled on after any
292    /// post-Init normalization (e.g. Rosetta builds a full-atom pose
293    /// from the input, which may add missing atoms, hydrogens, or
294    /// terminal O, changing the atom count) into `*out_initial_buf`.
295    /// Plugins with no normalization step write an empty buffer; host
296    /// then keeps its input assembly. Host owns the buffer afterward
297    /// (released via the same `free_buffer` path as `register`/`score`).
298    pub init: unsafe extern "C" fn(
299        handle: FolditPluginHandle,
300        assembly: *const u8,
301        assembly_len: usize,
302        assets: *const FolditPluginAsset,
303        assets_len: usize,
304        params: *const FolditPluginParamEntry,
305        params_len: usize,
306        out_session: *mut u64,
307        out_initial_buf: *mut FolditPluginBuffer,
308        out_err: *mut FolditPluginError,
309    ) -> FolditPluginStatus,
310
311    /// Push an Assembly update to a session. The payload is either a
312    /// full assembly snapshot (`payload_kind = Full`) or a delta edit
313    /// list (`payload_kind = Delta`). `from_gen` / `to_gen` are the
314    /// host's broadcast generation counters; a plugin whose local gen
315    /// doesn't match `from_gen` should arm a `STALE_GEN` error to
316    /// return on its next dispatch so the host re-syncs.
317    pub update_assembly: unsafe extern "C" fn(
318        handle: FolditPluginHandle,
319        session: u64,
320        payload_kind: FolditPluginAssemblyPayloadKind,
321        bytes: *const u8,
322        bytes_len: usize,
323        from_gen: u64,
324        to_gen: u64,
325        out_err: *mut FolditPluginError,
326    ) -> FolditPluginStatus,
327
328    /// Tear down a session and release its per-session state.
329    pub drop_session: unsafe extern "C" fn(
330        handle: FolditPluginHandle,
331        session: u64,
332        out_err: *mut FolditPluginError,
333    ) -> FolditPluginStatus,
334
335    // Optional protocol endpoints
336    /// Run a one-shot op. Writes resulting assembly bytes (typically
337    /// delta bytes) to `*out_assembly` on success.
338    pub invoke: unsafe extern "C" fn(
339        handle: FolditPluginHandle,
340        session: u64,
341        op_id: *const u8,
342        op_id_len: usize,
343        ctx: *const FolditPluginDispatchContext,
344        params: *const FolditPluginParamEntry,
345        params_len: usize,
346        out_assembly: *mut FolditPluginBuffer,
347        out_err: *mut FolditPluginError,
348    ) -> FolditPluginStatus,
349
350    /// Start a streaming op under the host-assigned `request_id`. The
351    /// plugin keys its stream state on that id; subsequent poll / update
352    /// / cancel calls thread the same id through.
353    pub start_stream: unsafe extern "C" fn(
354        handle: FolditPluginHandle,
355        session: u64,
356        op_id: *const u8,
357        op_id_len: usize,
358        ctx: *const FolditPluginDispatchContext,
359        params: *const FolditPluginParamEntry,
360        params_len: usize,
361        request_id: u64,
362        out_err: *mut FolditPluginError,
363    ) -> FolditPluginStatus,
364
365    /// Returns serialized `proto::PollStreamResponse`; the host
366    /// decodes the variant. Centralizing the variant set in proto
367    /// keeps the C ABI surface smaller; poll_stream is the only place
368    /// where the variant tagging matters.
369    pub poll_stream: unsafe extern "C" fn(
370        handle: FolditPluginHandle,
371        request_id: u64,
372        out_buf: *mut FolditPluginBuffer,
373        out_err: *mut FolditPluginError,
374    ) -> FolditPluginStatus,
375
376    /// Apply a live parameter update to an active stream. Plugins may
377    /// coalesce or defer updates until the next poll boundary.
378    pub update_stream: unsafe extern "C" fn(
379        handle: FolditPluginHandle,
380        request_id: u64,
381        params: *const FolditPluginParamEntry,
382        params_len: usize,
383        out_err: *mut FolditPluginError,
384    ) -> FolditPluginStatus,
385
386    /// Cancel an active stream. The plugin must release stream state
387    /// before the next poll returns `Final` or `Error`.
388    pub cancel_stream: unsafe extern "C" fn(
389        handle: FolditPluginHandle,
390        request_id: u64,
391        out_err: *mut FolditPluginError,
392    ) -> FolditPluginStatus,
393
394    /// Run a read-only query (no assembly mutation). When `assembly` is
395    /// non-null (`assembly_len > 0`) it names a specific composition to
396    /// read/score instead of the session: committed heads or a
397    /// checkpoint; null/0 operates on the session / its in-flight
398    /// snapshot. Result bytes are op-defined (e.g. a serialized
399    /// `proto::ScoreReport` for the `"score"` query); the caller parses
400    /// them against the query contract.
401    pub query: unsafe extern "C" fn(
402        handle: FolditPluginHandle,
403        session: u64,
404        query_id: *const u8,
405        query_id_len: usize,
406        ctx: *const FolditPluginDispatchContext,
407        params: *const FolditPluginParamEntry,
408        params_len: usize,
409        assembly: *const u8,
410        assembly_len: usize,
411        out_data: *mut FolditPluginBuffer,
412        out_err: *mut FolditPluginError,
413    ) -> FolditPluginStatus,
414
415    // Memory cleanup
416    /// Free a plugin-allocated buffer. No-op when `data` is null.
417    pub free_buffer: unsafe extern "C" fn(buf: *mut FolditPluginBuffer),
418    /// Free both inner buffers of a plugin-allocated error struct.
419    pub free_error: unsafe extern "C" fn(err: *mut FolditPluginError),
420}
421
422/// Symbol name the host looks up via `dlsym`. Returns
423/// `*const FolditPluginVtable`.
424pub const VTABLE_SYMBOL: &[u8] = b"foldit_plugin_vtable\0";
425
426/// Type signature of the `foldit_plugin_vtable` entry symbol.
427pub type FolditPluginVtableFn = unsafe extern "C" fn() -> *const FolditPluginVtable;