orbis_plugin_api/
runtime.rs

1//! Runtime host functions available to plugins.
2//!
3//! This module provides the interface for WASM plugins to interact with the Orbis runtime.
4//! These functions are imported by plugins and implemented by the host runtime.
5//!
6//! # Memory Management
7//!
8//! Plugins must implement two functions for memory management:
9//! ```rust,no_run
10//! #[unsafe(no_mangle)]
11//! pub extern "C" fn allocate(size: i32) -> *mut u8 {
12//!     let mut buf = Vec::with_capacity(size as usize);
13//!     let ptr = buf.as_mut_ptr();
14//!     std::mem::forget(buf);
15//!     ptr
16//! }
17//!
18//! #[unsafe(no_mangle)]
19//! pub extern "C" fn deallocate(ptr: *mut u8, size: i32) {
20//!     unsafe {
21//!         let _ = Vec::from_raw_parts(ptr, 0, size as usize);
22//!     }
23//! }
24//! ```
25//!
26//! # Handler Functions
27//!
28//! Plugin handlers receive a pointer and length to JSON-serialized context data,
29//! and must return a pointer to JSON-serialized response data:
30//! ```rust,no_run
31//! #[unsafe(no_mangle)]
32//! pub extern "C" fn my_handler(context_ptr: i32, context_len: i32) -> i32 {
33//!     // Read context from memory
34//!     // Process request
35//!     // Return pointer to response (with length prefix)
36//!     0 // placeholder
37//! }
38//! ```
39//!
40//! Response format: [4 bytes length (u32 le)] [data bytes]
41
42use serde::{Deserialize, Serialize};
43use std::collections::HashMap;
44
45/// Context passed to plugin handlers.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct PluginContext {
48    /// HTTP method.
49    pub method: String,
50
51    /// Request path.
52    pub path: String,
53
54    /// Request headers.
55    pub headers: HashMap<String, String>,
56
57    /// Query parameters.
58    pub query: HashMap<String, String>,
59
60    /// Request body (as JSON).
61    #[serde(default)]
62    pub body: serde_json::Value,
63
64    /// Authenticated user ID.
65    #[serde(default)]
66    pub user_id: Option<String>,
67
68    /// Whether user is admin.
69    #[serde(default)]
70    pub is_admin: bool,
71}
72
73/// Log levels for plugin logging.
74#[repr(i32)]
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum LogLevel {
77    /// Error level (0).
78    Error = 0,
79    /// Warning level (1).
80    Warn = 1,
81    /// Info level (2).
82    Info = 2,
83    /// Debug level (3).
84    Debug = 3,
85    /// Trace level (4).
86    Trace = 4,
87}
88
89/// Host functions that plugins can import and call.
90///
91/// These functions are implemented by the Orbis runtime and available to all plugins.
92/// Plugins should declare these in an `unsafe extern "C"` block:
93///
94/// ```rust,no_run
95/// unsafe extern "C" {
96///     fn log(level: i32, ptr: i32, len: i32);
97///     fn state_get(key_ptr: i32, key_len: i32) -> i32;
98///     fn state_set(key_ptr: i32, key_len: i32, value_ptr: i32, value_len: i32) -> i32;
99///     fn state_remove(key_ptr: i32, key_len: i32) -> i32;
100/// }
101/// ```
102#[allow(dead_code)]
103pub struct HostFunctions;
104
105impl HostFunctions {
106    /// Log a message from the plugin.
107    ///
108    /// # Parameters
109    /// - `level`: Log level (0=ERROR, 1=WARN, 2=INFO, 3=DEBUG, 4=TRACE)
110    /// - `ptr`: Pointer to UTF-8 message bytes (as i32 in WASM)
111    /// - `len`: Length of message in bytes
112    ///
113    /// # Example
114    /// ```rust,no_run
115    /// unsafe extern "C" {
116    ///     fn log(level: i32, ptr: i32, len: i32);
117    /// }
118    ///
119    /// fn log_info(msg: &str) {
120    ///     unsafe {
121    ///         log(2, msg.as_ptr() as i32, msg.len() as i32);
122    ///     }
123    /// }
124    /// ```
125    pub const LOG: &'static str = "log";
126
127    /// Get a value from plugin state.
128    ///
129    /// # Parameters
130    /// - `key_ptr`: Pointer to UTF-8 key bytes (as i32 in WASM)
131    /// - `key_len`: Length of key in bytes
132    ///
133    /// # Returns
134    /// Pointer to JSON-serialized value (with 4-byte length prefix), or 0 if key not found.
135    ///
136    /// # Example
137    /// ```rust,no_run
138    /// unsafe extern "C" {
139    ///     fn state_get(key_ptr: i32, key_len: i32) -> i32;
140    /// }
141    ///
142    /// fn get_counter() -> Option<i64> {
143    ///     let key = "counter";
144    ///     let ptr = unsafe {
145    ///         state_get(key.as_ptr() as i32, key.len() as i32)
146    ///     };
147    ///     
148    ///     if ptr == 0 {
149    ///         return None;
150    ///     }
151    ///     
152    ///     // Read length and data, deserialize JSON
153    ///     // ...
154    ///     Some(0)
155    /// }
156    /// ```
157    pub const STATE_GET: &'static str = "state_get";
158
159    /// Set a value in plugin state.
160    ///
161    /// # Parameters
162    /// - `key_ptr`: Pointer to UTF-8 key bytes (as i32 in WASM)
163    /// - `key_len`: Length of key in bytes
164    /// - `value_ptr`: Pointer to JSON-serialized value bytes (as i32 in WASM)
165    /// - `value_len`: Length of value in bytes
166    ///
167    /// # Returns
168    /// 1 on success, 0 on failure.
169    ///
170    /// # Example
171    /// ```rust,no_run
172    /// unsafe extern "C" {
173    ///     fn state_set(key_ptr: i32, key_len: i32, value_ptr: i32, value_len: i32) -> i32;
174    /// }
175    ///
176    /// fn set_counter(value: i64) -> bool {
177    ///     let key = "counter";
178    ///     let value_json = serde_json::to_string(&value).unwrap();
179    ///     
180    ///     let result = unsafe {
181    ///         state_set(
182    ///             key.as_ptr() as i32,
183    ///             key.len() as i32,
184    ///             value_json.as_ptr() as i32,
185    ///             value_json.len() as i32,
186    ///         )
187    ///     };
188    ///     
189    ///     result == 1
190    /// }
191    /// ```
192    pub const STATE_SET: &'static str = "state_set";
193
194    /// Remove a value from plugin state.
195    ///
196    /// # Parameters
197    /// - `key_ptr`: Pointer to UTF-8 key bytes
198    /// - `key_len`: Length of key in bytes
199    ///
200    /// # Returns
201    /// 1 on success, 0 on failure.
202    ///
203    /// # Example
204    /// ```rust,no_run
205    /// unsafe extern "C" {
206    ///     fn state_remove(key_ptr: i32, key_len: i32) -> i32;
207    /// }
208    ///
209    /// fn clear_counter() -> bool {
210    ///     let key = "counter";
211    ///     let result = unsafe {
212    ///         state_remove(key.as_ptr() as i32, key.len() as i32)
213    ///     };
214    ///     
215    ///     result == 1
216    /// }
217    /// ```
218    pub const STATE_REMOVE: &'static str = "state_remove";
219}
220
221/// Helper utilities for plugin development.
222pub mod helpers {
223    use super::*;
224
225    /// Read bytes from a pointer.
226    ///
227    /// # Safety
228    /// The pointer must be valid and the length must be correct.
229    pub unsafe fn read_bytes(ptr: *const u8, len: usize) -> Vec<u8> {
230        let mut buffer = vec![0u8; len];
231        // SAFETY: Caller guarantees ptr is valid and len is correct
232        unsafe {
233            std::ptr::copy_nonoverlapping(ptr, buffer.as_mut_ptr(), len);
234        }
235        buffer
236    }
237
238    /// Read a length-prefixed value from a pointer.
239    ///
240    /// # Safety
241    /// The pointer must point to a valid length-prefixed value.
242    pub unsafe fn read_length_prefixed(ptr: *const u8) -> Vec<u8> {
243        if ptr.is_null() {
244            return Vec::new();
245        }
246
247        // SAFETY: Caller guarantees ptr points to valid length-prefixed data
248        unsafe {
249            let len = *(ptr as *const u32);
250            let data_ptr = ptr.add(4);
251            read_bytes(data_ptr, len as usize)
252        }
253    }
254
255    /// Write a length-prefixed value.
256    ///
257    /// Returns a pointer to the allocated memory (caller must deallocate).
258    ///
259    /// # Safety
260    /// The returned pointer must be deallocated by the plugin.
261    pub unsafe fn write_length_prefixed(data: &[u8], allocate_fn: extern "C" fn(i32) -> *mut u8) -> *mut u8 {
262        let len = data.len() as u32;
263        let total_size = 4 + data.len();
264        let ptr = allocate_fn(total_size as i32);
265
266        // SAFETY: allocate_fn returned a valid pointer with sufficient capacity
267        unsafe {
268            // Write length
269            *(ptr as *mut u32) = len;
270
271            // Write data
272            let data_ptr = ptr.add(4);
273            std::ptr::copy_nonoverlapping(data.as_ptr(), data_ptr, data.len());
274        }
275
276        ptr
277    }
278
279    /// Deserialize context from memory.
280    pub fn deserialize_context(ptr: *const u8, len: usize) -> Result<PluginContext, serde_json::Error> {
281        let bytes = unsafe { read_bytes(ptr, len) };
282        serde_json::from_slice(&bytes)
283    }
284
285    /// Serialize response to memory.
286    ///
287    /// # Safety
288    /// The allocate function must be valid.
289    pub unsafe fn serialize_response<T: Serialize>(
290        value: &T,
291        allocate_fn: extern "C" fn(i32) -> *mut u8,
292    ) -> Result<*mut u8, serde_json::Error> {
293        let json = serde_json::to_vec(value)?;
294        // SAFETY: Caller guarantees allocate_fn is valid
295        unsafe {
296            Ok(write_length_prefixed(&json, allocate_fn))
297        }
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn test_log_level_values() {
307        assert_eq!(LogLevel::Error as i32, 0);
308        assert_eq!(LogLevel::Warn as i32, 1);
309        assert_eq!(LogLevel::Info as i32, 2);
310        assert_eq!(LogLevel::Debug as i32, 3);
311        assert_eq!(LogLevel::Trace as i32, 4);
312    }
313
314    #[test]
315    fn test_plugin_context_serialization() {
316        let context = PluginContext {
317            method: "GET".to_string(),
318            path: "/test".to_string(),
319            headers: HashMap::new(),
320            query: HashMap::new(),
321            body: serde_json::json!({}),
322            user_id: Some("user123".to_string()),
323            is_admin: false,
324        };
325
326        let json = serde_json::to_string(&context).unwrap();
327        let deserialized: PluginContext = serde_json::from_str(&json).unwrap();
328
329        assert_eq!(context.method, deserialized.method);
330        assert_eq!(context.path, deserialized.path);
331        assert_eq!(context.user_id, deserialized.user_id);
332    }
333}