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};
11
12pub use serde_json::Value;
13
14/// Registry type mapping function name to a function pointer.
15/// Developers write handlers matching this signature.
16pub type NativeFn = fn(HashMap<String, Value>) -> Result<Value, String>;
17
18static REGISTRY: OnceLock<Mutex<HashMap<String, NativeFn>>> = OnceLock::new();
19
20fn get_registry() -> &'static Mutex<HashMap<String, NativeFn>> {
21    REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
22}
23
24/// Registers a function handler under a specific name inside the dynamic FFI registry.
25/// 
26/// # Example
27/// ```rust
28/// use std::collections::HashMap;
29/// use void_sdk_rust::{register, Value};
30/// 
31/// fn my_handler(args: HashMap<String, Value>) -> Result<Value, String> {
32///     Ok(Value::String("Hello!".to_string()))
33/// }
34/// 
35/// register("greet", my_handler);
36/// ```
37pub fn register(name: &str, func: NativeFn) {
38    if let Ok(mut reg) = get_registry().lock() {
39        reg.insert(name.to_string(), func);
40    }
41}
42
43/// Allocates memory block on the WebAssembly heap.
44/// This is called internally by the Javascript Host to pass inputs.
45#[no_mangle]
46pub unsafe extern "C" fn void_malloc(size: usize) -> *mut u8 {
47    let layout = std::alloc::Layout::from_size_align(size, 1).unwrap();
48    std::alloc::alloc(layout)
49}
50
51/// Frees allocated memory blocks on the WebAssembly heap.
52/// This is called internally by the Javascript Host to clean arguments.
53#[no_mangle]
54pub unsafe extern "C" fn void_free(ptr: *mut u8, size: usize) {
55    let layout = std::alloc::Layout::from_size_align(size, 1).unwrap();
56    std::alloc::dealloc(ptr, layout)
57}
58
59extern "C" {
60    /// External setup entrypoint implemented in user plugins via the `void_plugin!` macro.
61    fn void_init();
62}
63
64static INIT: std::sync::Once = std::sync::Once::new();
65
66/// Verifies that user plugin registrations are fully initialized.
67pub fn ensure_registered() {
68    INIT.call_once(|| unsafe {
69        void_init();
70    });
71}
72
73/// Core FFI entrypoint invoked by the Host.
74/// Parses the JSON string input, executes the registered function, and returns the response pointer.
75#[no_mangle]
76pub extern "C" fn void_invoke(input: *const c_char) -> *mut c_char {
77    ensure_registered();
78
79    if input.is_null() {
80        return to_c_string_err("null input");
81    }
82
83    let c_str = unsafe { CStr::from_ptr(input) };
84    let str_slice = match c_str.to_str() {
85        Ok(s) => s,
86        Err(e) => return to_c_string_err(&format!("invalid utf-8 string: {}", e)),
87    };
88
89    let payload: serde_json::Value = match serde_json::from_str(str_slice) {
90        Ok(p) => p,
91        Err(e) => return to_c_string_err(&format!("invalid JSON payload: {}", e)),
92    };
93
94    let fn_name = match payload.get("fn").and_then(|v| v.as_str()) {
95        Some(name) => name,
96        None => return to_c_string_err("invalid or missing fn"),
97    };
98
99    // Reflect list of all registered functions
100    if fn_name == "__list_functions__" {
101        if let Ok(reg) = get_registry().lock() {
102            let keys: Vec<String> = reg.keys().cloned().collect();
103            return to_c_string_ok(&serde_json::Value::from(keys));
104        } else {
105            return to_c_string_err("failed to lock registry");
106        }
107    }
108
109    let data = payload.get("data")
110        .and_then(|v| v.as_object())
111        .map(|o| {
112            o.iter()
113                .map(|(k, v)| (k.clone(), v.clone()))
114                .collect::<HashMap<String, serde_json::Value>>()
115        })
116        .unwrap_or_default();
117
118    let reg = match get_registry().lock() {
119        Ok(r) => r,
120        Err(_) => return to_c_string_err("failed to lock registry"),
121    };
122
123    let func = match reg.get(fn_name) {
124        Some(f) => f,
125        None => return to_c_string_err(&format!("function '{}' not found", fn_name)),
126    };
127
128    match func(data) {
129        Ok(val) => to_c_string_ok(&val),
130        Err(e) => to_c_string_err(&e),
131    }
132}
133
134/// Frees returned output strings allocated on the heap.
135/// This is called by the Host loader once JSON responses are fully parsed.
136#[no_mangle]
137pub unsafe extern "C" fn void_free_string(ptr: *mut c_char) {
138    if !ptr.is_null() {
139        let _ = CString::from_raw(ptr);
140    }
141}
142
143fn to_c_string_ok(val: &serde_json::Value) -> *mut c_char {
144    let response = serde_json::json!({
145        "ok": true,
146        "value": val
147    });
148    let s = serde_json::to_string(&response).unwrap();
149    CString::new(s).unwrap().into_raw()
150}
151
152fn to_c_string_err(err_msg: &str) -> *mut c_char {
153    let response = serde_json::json!({
154        "ok": false,
155        "error": err_msg
156    });
157    let s = serde_json::to_string(&response).unwrap();
158    CString::new(s).unwrap().into_raw()
159}
160
161/// Extracts a string parameter value from the arguments map.
162/// 
163/// Returns `Err` if the key is missing or not a String.
164pub fn get_string(m: &HashMap<String, Value>, key: &str) -> Result<String, String> {
165    match m.get(key) {
166        Some(Value::String(s)) => Ok(s.clone()),
167        Some(_) => Err(format!("invalid type for field '{}', expected String", key)),
168        None => Err(format!("missing required field '{}'", key)),
169    }
170}
171
172/// Extracts a boolean parameter value from the arguments map.
173/// 
174/// Returns default if the key is missing or not a Bool.
175pub fn get_bool(m: &HashMap<String, Value>, key: &str, def: bool) -> bool {
176    match m.get(key) {
177        Some(Value::Bool(b)) => *b,
178        _ => def,
179    }
180}
181
182/// Extracts a 64-bit integer parameter value from the arguments map.
183/// 
184/// Returns `Err` if the key is missing or not a Number.
185pub fn get_int(m: &HashMap<String, Value>, key: &str) -> Result<i64, String> {
186    match m.get(key) {
187        Some(Value::Number(n)) => {
188            if let Some(i) = n.as_i64() {
189                Ok(i)
190            } else if let Some(f) = n.as_f64() {
191                Ok(f as i64)
192            } else {
193                Err(format!("invalid number format for field '{}'", key))
194            }
195        }
196        Some(_) => Err(format!("invalid type for field '{}', expected Integer", key)),
197        None => Err(format!("missing required field '{}'", key)),
198    }
199}
200
201/// Extracts a 64-bit float parameter value from the arguments map.
202/// 
203/// Returns `Err` if the key is missing or not a Number.
204pub fn get_float(m: &HashMap<String, Value>, key: &str) -> Result<f64, String> {
205    match m.get(key) {
206        Some(Value::Number(n)) => {
207            if let Some(f) = n.as_f64() {
208                Ok(f)
209            } else {
210                Err(format!("invalid number format for field '{}'", key))
211            }
212        }
213        Some(_) => Err(format!("invalid type for field '{}', expected Float", key)),
214        None => Err(format!("missing required field '{}'", key)),
215    }
216}
217
218/// Extracts an Array parameter value from the arguments map.
219/// 
220/// Returns `Err` if the key is missing or not an Array.
221pub fn get_array(m: &HashMap<String, Value>, key: &str) -> Result<Vec<Value>, String> {
222    match m.get(key) {
223        Some(Value::Array(a)) => Ok(a.clone()),
224        Some(_) => Err(format!("invalid type for field '{}', expected Array", key)),
225        None => Err(format!("missing required field '{}'", key)),
226    }
227}
228
229/// Extracts an Object parameter value from the arguments map.
230/// 
231/// Returns `Err` if the key is missing or not an Object.
232pub fn get_object(m: &HashMap<String, Value>, key: &str) -> Result<serde_json::Map<String, Value>, String> {
233    match m.get(key) {
234        Some(Value::Object(o)) => Ok(o.clone()),
235        Some(_) => Err(format!("invalid type for field '{}', expected Object", key)),
236        None => Err(format!("missing required field '{}'", key)),
237    }
238}
239
240/// Extracts a raw JSON `Value` parameter value from the arguments map.
241/// 
242/// Returns `Err` if the key is missing.
243pub fn get_value(m: &HashMap<String, Value>, key: &str) -> Result<Value, String> {
244    match m.get(key) {
245        Some(v) => Ok(v.clone()),
246        None => Err(format!("missing required field '{}'", key)),
247    }
248}
249
250/// Verifies that a field inside the map is a JSON `Null` type.
251/// 
252/// Returns `Err` if the key is missing or not Null.
253pub fn get_null(m: &HashMap<String, Value>, key: &str) -> Result<(), String> {
254    match m.get(key) {
255        Some(Value::Null) => Ok(()),
256        Some(_) => Err(format!("invalid type for field '{}', expected Null", key)),
257        None => Err(format!("missing required field '{}'", key)),
258    }
259}
260
261/// Exports standard WebAssembly FFI functions (`void_invoke`, `void_free_string`, etc.)
262/// and invokes the user's plugin registration function on initialization.
263/// 
264/// # Example
265/// ```rust
266/// use void_sdk_rust::{register, void_plugin};
267/// 
268/// fn init_registrations() {
269///     register("greet", greet_func);
270/// }
271/// 
272/// void_plugin!(init_registrations);
273/// ```
274#[macro_export]
275macro_rules! void_plugin {
276    ($init_fn:ident) => {
277        #[no_mangle]
278        pub extern "C" fn void_init() {
279            $init_fn();
280        }
281    };
282}