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}