Skip to main content

void_sdk_rust/
lib.rs

1//! Void Rust SDK
2//! 
3//! This library provides helper macros and FFI boundaries to build stateful WebAssembly 
4//! plugins in Rust for the Void framework runtime.
5
6use std::collections::HashMap;
7use std::sync::Mutex;
8use std::sync::OnceLock;
9use std::os::raw::c_char;
10use std::ffi::{CStr, CString};
11use std::sync::atomic::{AtomicUsize, Ordering};
12
13pub use serde_json::Value;
14
15static ACTIVE_ALLOCS: AtomicUsize = AtomicUsize::new(0);
16static TOTAL_ALLOCATED_BYTES: AtomicUsize = AtomicUsize::new(0);
17
18/// Heap allocation statistics representing active FFI allocations inside the Rust SDK.
19#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
20pub struct MemoryStats {
21    pub allocated_objects: usize,
22    pub total_bytes_pinned: u64,
23}
24
25/// Retrieves active memory statistics for checking leaks and diagnostics.
26pub fn get_memory_stats() -> MemoryStats {
27    MemoryStats {
28        allocated_objects: ACTIVE_ALLOCS.load(Ordering::SeqCst),
29        total_bytes_pinned: TOTAL_ALLOCATED_BYTES.load(Ordering::SeqCst) as u64,
30    }
31}
32
33/// Manually allocates and pins raw memory layout on the heap.
34/// Note: You MUST free it using unpin_memory to prevent memory leaks.
35pub unsafe fn pin_memory(data: &[u8]) -> *mut u8 {
36    if data.is_empty() {
37        return std::ptr::null_mut();
38    }
39    let size = data.len();
40    let layout = std::alloc::Layout::from_size_align(size, 1).unwrap();
41    let ptr = std::alloc::alloc(layout);
42    if !ptr.is_null() {
43        std::ptr::copy_nonoverlapping(data.as_ptr(), ptr, size);
44        ACTIVE_ALLOCS.fetch_add(1, Ordering::SeqCst);
45        TOTAL_ALLOCATED_BYTES.fetch_add(size, Ordering::SeqCst);
46    }
47    ptr
48}
49
50/// Unpins and deallocates a manually pinned raw memory block.
51pub unsafe fn unpin_memory(ptr: *mut u8, size: usize) {
52    if !ptr.is_null() {
53        let layout = std::alloc::Layout::from_size_align(size, 1).unwrap();
54        std::alloc::dealloc(ptr, layout);
55        ACTIVE_ALLOCS.fetch_sub(1, Ordering::SeqCst);
56        TOTAL_ALLOCATED_BYTES.fetch_sub(size, Ordering::SeqCst);
57    }
58}
59
60/// Registry type mapping function name to a function pointer.
61/// Developers write handlers matching this signature.
62pub type NativeFn = fn(HashMap<String, Value>) -> Result<Value, String>;
63
64static REGISTRY: OnceLock<Mutex<HashMap<String, NativeFn>>> = OnceLock::new();
65
66fn get_registry() -> &'static Mutex<HashMap<String, NativeFn>> {
67    REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
68}
69
70/// Registers a function handler under a specific name inside the dynamic FFI registry.
71/// 
72/// # Example
73/// ```rust
74/// use std::collections::HashMap;
75/// use void_sdk_rust::{register, Value};
76/// 
77/// fn my_handler(args: HashMap<String, Value>) -> Result<Value, String> {
78///     Ok(Value::String("Hello!".to_string()))
79/// }
80/// 
81/// register("greet", my_handler);
82/// ```
83pub fn register(name: &str, func: NativeFn) {
84    if let Ok(mut reg) = get_registry().lock() {
85        reg.insert(name.to_string(), func);
86    }
87}
88
89/// Allocates memory block on the WebAssembly heap.
90/// This is called internally by the Javascript Host to pass inputs.
91#[no_mangle]
92pub unsafe extern "C" fn void_malloc(size: usize) -> *mut u8 {
93    let layout = std::alloc::Layout::from_size_align(size, 1).unwrap();
94    let ptr = std::alloc::alloc(layout);
95    if !ptr.is_null() {
96        ACTIVE_ALLOCS.fetch_add(1, Ordering::SeqCst);
97        TOTAL_ALLOCATED_BYTES.fetch_add(size, Ordering::SeqCst);
98    }
99    ptr
100}
101
102/// Frees allocated memory blocks on the WebAssembly heap.
103/// This is called internally by the Javascript Host to clean arguments.
104#[no_mangle]
105pub unsafe extern "C" fn void_free(ptr: *mut u8, size: usize) {
106    if !ptr.is_null() {
107        let layout = std::alloc::Layout::from_size_align(size, 1).unwrap();
108        std::alloc::dealloc(ptr, layout);
109        ACTIVE_ALLOCS.fetch_sub(1, Ordering::SeqCst);
110        TOTAL_ALLOCATED_BYTES.fetch_sub(size, Ordering::SeqCst);
111    }
112}
113
114extern "C" {
115    /// External setup entrypoint implemented in user plugins via the `void_plugin!` macro.
116    fn void_init();
117}
118
119static INIT: std::sync::Once = std::sync::Once::new();
120
121/// Verifies that user plugin registrations are fully initialized.
122pub fn ensure_registered() {
123    INIT.call_once(|| unsafe {
124        void_init();
125    });
126}
127
128/// Core FFI entrypoint invoked by the Host.
129/// Parses the JSON string input, executes the registered function, and returns the response pointer.
130#[no_mangle]
131pub extern "C" fn void_invoke(input: *const c_char) -> *mut c_char {
132    ensure_registered();
133
134    if input.is_null() {
135        return to_c_string_err("null input");
136    }
137
138    let c_str = unsafe { CStr::from_ptr(input) };
139    let str_slice = match c_str.to_str() {
140        Ok(s) => s,
141        Err(e) => return to_c_string_err(&format!("invalid utf-8 string: {}", e)),
142    };
143
144    let payload: serde_json::Value = match serde_json::from_str(str_slice) {
145        Ok(p) => p,
146        Err(e) => return to_c_string_err(&format!("invalid JSON payload: {}", e)),
147    };
148
149    let fn_name = match payload.get("fn").and_then(|v| v.as_str()) {
150        Some(name) => name,
151        None => return to_c_string_err("invalid or missing fn"),
152    };
153
154    // Reflect list of all registered functions
155    if fn_name == "__list_functions__" {
156        if let Ok(reg) = get_registry().lock() {
157            let keys: Vec<String> = reg.keys().cloned().collect();
158            return to_c_string_ok(&serde_json::Value::from(keys));
159        } else {
160            return to_c_string_err("failed to lock registry");
161        }
162    }
163
164    let data = payload.get("data")
165        .and_then(|v| v.as_object())
166        .map(|o| {
167            o.iter()
168                .map(|(k, v)| (k.clone(), v.clone()))
169                .collect::<HashMap<String, serde_json::Value>>()
170        })
171        .unwrap_or_default();
172
173    let reg = match get_registry().lock() {
174        Ok(r) => r,
175        Err(_) => return to_c_string_err("failed to lock registry"),
176    };
177
178    let func = match reg.get(fn_name) {
179        Some(f) => f,
180        None => return to_c_string_err(&format!("function '{}' not found", fn_name)),
181    };
182
183    match func(data) {
184        Ok(val) => to_c_string_ok(&val),
185        Err(e) => to_c_string_err(&e),
186    }
187}
188
189/// Frees returned output strings allocated on the heap.
190/// This is called by the Host loader once JSON responses are fully parsed.
191#[no_mangle]
192pub unsafe extern "C" fn void_free_string(ptr: *mut c_char) {
193    if !ptr.is_null() {
194        let c_str = CString::from_raw(ptr);
195        let size = c_str.as_bytes_with_nul().len();
196        ACTIVE_ALLOCS.fetch_sub(1, Ordering::SeqCst);
197        TOTAL_ALLOCATED_BYTES.fetch_sub(size, Ordering::SeqCst);
198    }
199}
200
201fn to_c_string_ok(val: &serde_json::Value) -> *mut c_char {
202    let response = serde_json::json!({
203        "ok": true,
204        "value": val
205    });
206    let s = serde_json::to_string(&response).unwrap();
207    let c_str = CString::new(s).unwrap();
208    let size = c_str.as_bytes_with_nul().len();
209    ACTIVE_ALLOCS.fetch_add(1, Ordering::SeqCst);
210    TOTAL_ALLOCATED_BYTES.fetch_add(size, Ordering::SeqCst);
211    c_str.into_raw()
212}
213
214fn to_c_string_err(err_msg: &str) -> *mut c_char {
215    let response = serde_json::json!({
216        "ok": false,
217        "error": err_msg
218    });
219    let s = serde_json::to_string(&response).unwrap();
220    let c_str = CString::new(s).unwrap();
221    let size = c_str.as_bytes_with_nul().len();
222    ACTIVE_ALLOCS.fetch_add(1, Ordering::SeqCst);
223    TOTAL_ALLOCATED_BYTES.fetch_add(size, Ordering::SeqCst);
224    c_str.into_raw()
225}
226
227/// Extracts a string parameter value from the arguments map.
228/// 
229/// Returns `Err` if the key is missing or not a String.
230pub fn get_string(m: &HashMap<String, Value>, key: &str) -> Result<String, String> {
231    match m.get(key) {
232        Some(Value::String(s)) => Ok(s.clone()),
233        Some(_) => Err(format!("invalid type for field '{}', expected String", key)),
234        None => Err(format!("missing required field '{}'", key)),
235    }
236}
237
238/// Extracts a boolean parameter value from the arguments map.
239/// 
240/// Returns default if the key is missing or not a Bool.
241pub fn get_bool(m: &HashMap<String, Value>, key: &str, def: bool) -> bool {
242    match m.get(key) {
243        Some(Value::Bool(b)) => *b,
244        _ => def,
245    }
246}
247
248/// Extracts a 64-bit integer parameter value from the arguments map.
249/// 
250/// Returns `Err` if the key is missing or not a Number.
251pub fn get_int(m: &HashMap<String, Value>, key: &str) -> Result<i64, String> {
252    match m.get(key) {
253        Some(Value::Number(n)) => {
254            if let Some(i) = n.as_i64() {
255                Ok(i)
256            } else if let Some(f) = n.as_f64() {
257                Ok(f as i64)
258            } else {
259                Err(format!("invalid number format for field '{}'", key))
260            }
261        }
262        Some(_) => Err(format!("invalid type for field '{}', expected Integer", key)),
263        None => Err(format!("missing required field '{}'", key)),
264    }
265}
266
267/// Extracts a 64-bit float parameter value from the arguments map.
268/// 
269/// Returns `Err` if the key is missing or not a Number.
270pub fn get_float(m: &HashMap<String, Value>, key: &str) -> Result<f64, String> {
271    match m.get(key) {
272        Some(Value::Number(n)) => {
273            if let Some(f) = n.as_f64() {
274                Ok(f)
275            } else {
276                Err(format!("invalid number format for field '{}'", key))
277            }
278        }
279        Some(_) => Err(format!("invalid type for field '{}', expected Float", key)),
280        None => Err(format!("missing required field '{}'", key)),
281    }
282}
283
284/// Extracts an Array parameter value from the arguments map.
285/// 
286/// Returns `Err` if the key is missing or not an Array.
287pub fn get_array(m: &HashMap<String, Value>, key: &str) -> Result<Vec<Value>, String> {
288    match m.get(key) {
289        Some(Value::Array(a)) => Ok(a.clone()),
290        Some(_) => Err(format!("invalid type for field '{}', expected Array", key)),
291        None => Err(format!("missing required field '{}'", key)),
292    }
293}
294
295/// Extracts an Object parameter value from the arguments map.
296/// 
297/// Returns `Err` if the key is missing or not an Object.
298pub fn get_object(m: &HashMap<String, Value>, key: &str) -> Result<serde_json::Map<String, Value>, String> {
299    match m.get(key) {
300        Some(Value::Object(o)) => Ok(o.clone()),
301        Some(_) => Err(format!("invalid type for field '{}', expected Object", key)),
302        None => Err(format!("missing required field '{}'", key)),
303    }
304}
305
306/// Extracts a raw JSON `Value` parameter value from the arguments map.
307/// 
308/// Returns `Err` if the key is missing.
309pub fn get_value(m: &HashMap<String, Value>, key: &str) -> Result<Value, String> {
310    match m.get(key) {
311        Some(v) => Ok(v.clone()),
312        None => Err(format!("missing required field '{}'", key)),
313    }
314}
315
316/// Verifies that a field inside the map is a JSON `Null` type.
317/// 
318/// Returns `Err` if the key is missing or not Null.
319pub fn get_null(m: &HashMap<String, Value>, key: &str) -> Result<(), String> {
320    match m.get(key) {
321        Some(Value::Null) => Ok(()),
322        Some(_) => Err(format!("invalid type for field '{}', expected Null", key)),
323        None => Err(format!("missing required field '{}'", key)),
324    }
325}
326
327/// Exports standard WebAssembly FFI functions (`void_invoke`, `void_free_string`, etc.)
328/// and invokes the user's plugin registration function on initialization.
329/// 
330/// # Example
331/// ```rust
332/// use void_sdk_rust::{register, void_plugin};
333/// 
334/// fn init_registrations() {
335///     register("greet", greet_func);
336/// }
337/// 
338/// void_plugin!(init_registrations);
339/// ```
340#[macro_export]
341macro_rules! void_plugin {
342    ($init_fn:ident) => {
343        #[no_mangle]
344        pub extern "C" fn void_init() {
345            $init_fn();
346        }
347    };
348}