rustant_plugins/
wasm_loader.rs1use crate::{Plugin, PluginError, PluginMetadata, PluginToolDef};
6use async_trait::async_trait;
7use std::path::Path;
8
9pub struct WasmPluginLoader;
11
12impl WasmPluginLoader {
13 pub fn new() -> Self {
15 Self
16 }
17
18 pub fn load_from_bytes(
20 &self,
21 name: &str,
22 wasm_bytes: &[u8],
23 ) -> Result<Box<dyn Plugin>, PluginError> {
24 let engine = wasmi::Engine::default();
26 let module = wasmi::Module::new(&engine, wasm_bytes).map_err(|e| {
27 PluginError::LoadFailed(format!("Invalid WASM module '{}': {}", name, e))
28 })?;
29
30 Ok(Box::new(WasmPlugin {
31 name: name.into(),
32 engine,
33 module,
34 store: None,
35 }))
36 }
37
38 pub fn load_from_file(&self, path: &Path) -> Result<Box<dyn Plugin>, PluginError> {
40 let bytes = std::fs::read(path).map_err(|e| {
41 PluginError::LoadFailed(format!("Failed to read '{}': {}", path.display(), e))
42 })?;
43 let name = path
44 .file_stem()
45 .and_then(|s| s.to_str())
46 .unwrap_or("unknown");
47 self.load_from_bytes(name, &bytes)
48 }
49}
50
51impl Default for WasmPluginLoader {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57struct WasmPlugin {
59 name: String,
60 #[allow(dead_code)]
61 engine: wasmi::Engine,
62 #[allow(dead_code)]
63 module: wasmi::Module,
64 #[allow(dead_code)]
65 store: Option<wasmi::Store<()>>,
66}
67
68unsafe impl Send for WasmPlugin {}
70unsafe impl Sync for WasmPlugin {}
71
72#[async_trait]
73impl Plugin for WasmPlugin {
74 fn metadata(&self) -> PluginMetadata {
75 PluginMetadata {
76 name: self.name.clone(),
77 version: "0.1.0".into(),
78 description: format!("WASM plugin: {}", self.name),
79 author: None,
80 min_core_version: None,
81 capabilities: vec![],
82 }
83 }
84
85 async fn on_load(&mut self) -> Result<(), PluginError> {
86 self.store = Some(wasmi::Store::new(&self.engine, ()));
88 tracing::info!(plugin = %self.name, "WASM plugin loaded");
89 Ok(())
90 }
91
92 async fn on_unload(&mut self) -> Result<(), PluginError> {
93 self.store = None;
94 tracing::info!(plugin = %self.name, "WASM plugin unloaded");
95 Ok(())
96 }
97
98 fn tools(&self) -> Vec<PluginToolDef> {
99 Vec::new()
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 const MINIMAL_WASM: &[u8] = &[
111 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ];
114
115 #[test]
116 fn test_wasm_loader_from_bytes() {
117 let loader = WasmPluginLoader::new();
118 let result = loader.load_from_bytes("test", MINIMAL_WASM);
119 assert!(result.is_ok());
120
121 let plugin = result.unwrap();
122 let meta = plugin.metadata();
123 assert_eq!(meta.name, "test");
124 }
125
126 #[test]
127 fn test_wasm_loader_invalid_bytes() {
128 let loader = WasmPluginLoader::new();
129 let result = loader.load_from_bytes("bad", &[0x00, 0x01, 0x02]);
130 assert!(result.is_err());
131 }
132
133 #[tokio::test]
134 async fn test_wasm_plugin_lifecycle() {
135 let loader = WasmPluginLoader::new();
136 let mut plugin = loader.load_from_bytes("lifecycle", MINIMAL_WASM).unwrap();
137
138 plugin.on_load().await.unwrap();
139 assert_eq!(plugin.metadata().name, "lifecycle");
140
141 let tools = plugin.tools();
142 assert!(tools.is_empty());
143
144 plugin.on_unload().await.unwrap();
145 }
146
147 #[test]
148 fn test_wasm_loader_from_file_not_found() {
149 let loader = WasmPluginLoader::new();
150 let result = loader.load_from_file(Path::new("/nonexistent/plugin.wasm"));
151 assert!(result.is_err());
152 }
153
154 #[test]
155 fn test_wasm_loader_from_file() {
156 let dir = tempfile::TempDir::new().unwrap();
157 let wasm_path = dir.path().join("test.wasm");
158 std::fs::write(&wasm_path, MINIMAL_WASM).unwrap();
159
160 let loader = WasmPluginLoader::new();
161 let result = loader.load_from_file(&wasm_path);
162 assert!(result.is_ok());
163 }
164}