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}