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;