Skip to main content

palladium_plugin_api/
lib.rs

1//! C ABI type definitions for the Palladium plugin system.
2//!
3//! This crate is intentionally dependency-free. All types are `#[repr(C)]`
4//! and safe to expose across shared library (`cdylib`) and WASM boundaries.
5//!
6//! Plugin authors implement the required C exports; the engine host reads
7//! plugin metadata and calls actor lifecycle functions through these types.
8//!
9//! # Required exports (every plugin shared library)
10//!
11//! ```c
12//! PdPluginInfo* pd_plugin_init(void);
13//! void          pd_plugin_shutdown(void);
14//!
15//! void*   pd_actor_create (const char* type_name, uint32_t type_name_len,
16//!                          const uint8_t* config,  uint32_t config_len);
17//! void    pd_actor_destroy(void* actor);
18//! int32_t pd_actor_on_start  (void* actor, PdActorContext* ctx);
19//! int32_t pd_actor_on_message(void* actor, PdActorContext* ctx,
20//!                             const uint8_t* envelope_bytes,
21//!                             const uint8_t* payload, uint32_t payload_len);
22//! void    pd_actor_on_stop   (void* actor, PdActorContext* ctx, int32_t reason);
23//! ```
24
25#![forbid(unsafe_code)]
26
27use core::ffi::c_void;
28
29// ── ABI Version ───────────────────────────────────────────────────────────────
30
31/// ABI version that every plugin and the engine must agree on at load time.
32///
33/// Increment this on any breaking change to the structs or function signatures
34/// in this crate. The engine rejects plugins whose [`PdPluginInfo::abi_version`]
35/// does not match.
36pub const PD_ABI_VERSION: u32 = 1;
37
38// ── Error Codes ───────────────────────────────────────────────────────────────
39
40/// Return codes for plugin actor lifecycle functions.
41///
42/// Plugin-side functions return `PdError::Ok` (0) on success.  Any non-zero
43/// value is treated as an error and mapped to the corresponding
44/// `palladium_actor::ActorError` variant by the engine.
45#[repr(i32)]
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum PdError {
48    /// Operation succeeded.
49    Ok = 0,
50    /// Actor failed to initialize (`on_start` returned an error).
51    Init = -1,
52    /// Message handler returned an error (`on_message` failed).
53    Handler = -2,
54    /// Requested actor type or resource was not found.
55    NotFound = -3,
56}
57
58impl PdError {
59    /// Convert an `i32` return code to a `PdError`.
60    ///
61    /// Returns `None` for unknown negative codes (treated as `Handler`-severity
62    /// by the engine).
63    pub fn from_i32(v: i32) -> Option<Self> {
64        match v {
65            0 => Some(Self::Ok),
66            -1 => Some(Self::Init),
67            -2 => Some(Self::Handler),
68            -3 => Some(Self::NotFound),
69            _ => None,
70        }
71    }
72
73    /// Returns `true` if the code represents success.
74    pub fn is_ok(self) -> bool {
75        self == Self::Ok
76    }
77}
78
79// ── Envelope ──────────────────────────────────────────────────────────────────
80
81/// Fixed-size C-compatible message header passed to plugin lifecycle hooks.
82///
83/// The layout is identical to `palladium_actor::Envelope` (80 bytes, `repr(C)`),
84/// so the engine can reinterpret Envelope bytes directly without copying.
85///
86/// All multi-byte fields are **little-endian**.
87///
88/// Field offsets:
89/// ```text
90/// offset  0 – 15 : message_id    (u128)
91/// offset 16 – 31 : source        (u128, actor AddrHash)
92/// offset 32 – 47 : destination   (u128, actor AddrHash)
93/// offset 48 – 55 : type_tag      (u64,  message type discriminant)
94/// offset 56 – 59 : payload_len   (u32)
95/// offset 60 – 63 : flags         (u32,  see FLAG_* constants)
96/// offset 64 – 71 : correlation_id(u64)
97/// offset 72 – 79 : (implicit repr(C) padding — write as zeros)
98/// ```
99///
100/// Total size: 80 bytes (verified by [`ENVELOPE_SIZE`]).
101#[repr(C)]
102#[derive(Debug, Clone, Copy)]
103pub struct PdEnvelope {
104    /// Unique message identifier (engine-assigned).
105    pub message_id: u128,
106    /// Sender's `AddrHash` (path + generation, packed into a u128).
107    pub source: u128,
108    /// Recipient's `AddrHash`.
109    pub destination: u128,
110    /// Message type discriminant (FNV-1a of the fully-qualified type name).
111    pub type_tag: u64,
112    /// Byte length of the accompanying payload buffer.
113    pub payload_len: u32,
114    /// Bitfield flags — see `FLAG_RESPONSE` and `FLAG_PRIORITY_MASK`.
115    pub flags: u32,
116    /// Response correlation: matches the `message_id` of the request.
117    pub correlation_id: u64,
118    // 8 bytes of implicit repr(C) trailing padding follow here.
119}
120
121/// Expected byte size of [`PdEnvelope`] (matches `palladium_actor::Envelope::SIZE`).
122pub const ENVELOPE_SIZE: usize = 80;
123
124/// Flag bit: this message is a response to a previous request.
125pub const FLAG_RESPONSE: u32 = 1 << 0;
126/// Mask for the 2-bit priority field (bits 1–2); higher value = higher priority.
127pub const FLAG_PRIORITY_MASK: u32 = 0b11 << 1;
128
129// ── Actor Type Info ───────────────────────────────────────────────────────────
130
131/// Metadata for a single actor type exported by a plugin.
132///
133/// All string pointers are **not** null-terminated; use the accompanying
134/// `_len` field for the byte count.
135///
136/// `config_schema` is optional: set to a null pointer and `config_schema_len`
137/// to 0 if the actor type accepts no configuration.
138///
139/// The pointed-to data must remain valid for the lifetime of the plugin.
140#[repr(C)]
141pub struct PdActorTypeInfo {
142    /// UTF-8 actor type name (e.g. `b"echo"`) — not null-terminated.
143    pub type_name: *const u8,
144    /// Byte length of `type_name`.
145    pub type_name_len: u32,
146    /// Optional UTF-8 JSON Schema for actor configuration, or null.
147    pub config_schema: *const u8,
148    /// Byte length of `config_schema`, or 0 if null.
149    pub config_schema_len: u32,
150}
151
152// ── Plugin Info ───────────────────────────────────────────────────────────────
153
154/// Top-level plugin metadata returned by the `pd_plugin_init` export.
155///
156/// The engine reads this struct immediately after calling `pd_plugin_init`.
157/// All pointed-to data must remain valid until `pd_plugin_shutdown` returns.
158#[repr(C)]
159pub struct PdPluginInfo {
160    /// UTF-8 plugin name — not null-terminated.
161    pub name: *const u8,
162    /// Byte length of `name`.
163    pub name_len: u32,
164    /// Semantic version — major component.
165    pub version_major: u32,
166    /// Semantic version — minor component.
167    pub version_minor: u32,
168    /// Semantic version — patch component.
169    pub version_patch: u32,
170    /// Must equal [`PD_ABI_VERSION`]; the engine rejects mismatches with
171    /// `PluginError::AbiMismatch`.
172    pub abi_version: u32,
173    /// Number of elements in the `actor_types` array.
174    pub actor_type_count: u32,
175    /// Pointer to the first element of an array of [`PdActorTypeInfo`].
176    pub actor_types: *const PdActorTypeInfo,
177}
178
179// ── Host Vtable ───────────────────────────────────────────────────────────────
180
181/// Engine capabilities made available to plugin actors during lifecycle calls.
182///
183/// The engine allocates and fills this struct before invoking any actor
184/// lifecycle function.  Plugin code accesses it via [`PdActorContext::vtable`].
185/// All function pointers are guaranteed non-null during lifecycle calls.
186#[repr(C)]
187pub struct PdHostVtable {
188    /// Send a message to another actor.
189    ///
190    /// `envelope_bytes` must point to exactly [`ENVELOPE_SIZE`] (80) bytes
191    /// in [`PdEnvelope`] layout. `payload` points to `payload_len` bytes.
192    ///
193    /// Returns 0 on success; a negative `PdError` code on failure.
194    pub pd_send: Option<
195        unsafe extern "C" fn(
196            ctx: *mut PdActorContext,
197            envelope_bytes: *const u8,
198            payload: *const u8,
199            payload_len: u32,
200        ) -> i32,
201    >,
202
203    /// Return the current engine time in microseconds since an arbitrary epoch.
204    ///
205    /// In production, this is wall-clock time. In simulation (`pd-test`),
206    /// this returns the deterministic virtual clock value.
207    pub pd_now_micros: Option<unsafe extern "C" fn(ctx: *mut PdActorContext) -> u64>,
208
209    /// Emit a structured log message.
210    ///
211    /// `level`: 0 = error, 1 = warn, 2 = info, 3 = debug, 4 = trace.
212    /// `msg` points to `msg_len` UTF-8 bytes — not null-terminated.
213    pub pd_log: Option<
214        unsafe extern "C" fn(ctx: *mut PdActorContext, level: i32, msg: *const u8, msg_len: u32),
215    >,
216}
217
218// ── Actor Context ─────────────────────────────────────────────────────────────
219
220/// Per-call context passed to every plugin actor lifecycle hook.
221///
222/// Plugin code receives a `*mut PdActorContext` in each lifecycle function.
223/// Use the [`vtable`](Self::vtable) to send messages, query time, or log.
224///
225/// The `engine_cookie` field is an opaque handle owned by the engine;
226/// plugins must pass it unchanged to vtable functions and must never free it.
227#[repr(C)]
228pub struct PdActorContext {
229    /// Engine capability table. Always non-null during lifecycle calls.
230    pub vtable: *const PdHostVtable,
231    /// Opaque per-actor engine handle. Treat as an opaque cookie.
232    pub engine_cookie: *mut c_void,
233}
234
235// ── Tests ─────────────────────────────────────────────────────────────────────
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use core::mem;
241
242    #[test]
243    fn pd_envelope_size_matches_engine_envelope() {
244        // Must equal palladium_actor::Envelope::SIZE = 80.
245        // u128 fields (align 16) cause repr(C) to pad 72 bytes of fields → 80.
246        assert_eq!(mem::size_of::<PdEnvelope>(), ENVELOPE_SIZE);
247    }
248
249    #[test]
250    fn pd_envelope_alignment_is_16() {
251        // Driven by the u128 fields; must agree with palladium_actor::Envelope.
252        assert_eq!(mem::align_of::<PdEnvelope>(), 16);
253    }
254
255    #[test]
256    fn pd_error_discriminants() {
257        assert_eq!(PdError::Ok as i32, 0);
258        assert_eq!(PdError::Init as i32, -1);
259        assert_eq!(PdError::Handler as i32, -2);
260        assert_eq!(PdError::NotFound as i32, -3);
261    }
262
263    #[test]
264    fn pd_error_from_i32_roundtrip() {
265        assert_eq!(PdError::from_i32(0), Some(PdError::Ok));
266        assert_eq!(PdError::from_i32(-1), Some(PdError::Init));
267        assert_eq!(PdError::from_i32(-2), Some(PdError::Handler));
268        assert_eq!(PdError::from_i32(-3), Some(PdError::NotFound));
269        assert_eq!(PdError::from_i32(-99), None);
270    }
271
272    #[test]
273    fn pd_error_is_ok() {
274        assert!(PdError::Ok.is_ok());
275        assert!(!PdError::Init.is_ok());
276        assert!(!PdError::Handler.is_ok());
277    }
278
279    #[test]
280    fn pd_actor_context_size() {
281        // Two pointers: vtable + engine_cookie.
282        assert_eq!(
283            mem::size_of::<PdActorContext>(),
284            2 * mem::size_of::<usize>()
285        );
286    }
287
288    #[test]
289    fn pd_abi_version_is_nonzero() {
290        let version = std::hint::black_box(PD_ABI_VERSION);
291        assert!(version > 0);
292    }
293
294    #[test]
295    fn flag_constants_do_not_overlap() {
296        assert_eq!(FLAG_RESPONSE & FLAG_PRIORITY_MASK, 0);
297    }
298}