1use sen_plugin_api::{Effect, EffectResult, ExecuteResult, PluginManifest, API_VERSION};
6use thiserror::Error;
7use wasmtime::*;
8
9#[derive(Debug, Error)]
11pub enum LoaderError {
12 #[error("Engine creation failed: {0}")]
13 EngineCreation(#[source] anyhow::Error),
14
15 #[error("Module compilation failed: {0}")]
16 ModuleCompilation(#[source] anyhow::Error),
17
18 #[error("Instantiation failed: {0}")]
19 Instantiation(#[source] anyhow::Error),
20
21 #[error("Function not found: {0}")]
22 FunctionNotFound(String),
23
24 #[error("Function call failed: {function} - {source}")]
25 FunctionCall {
26 function: &'static str,
27 #[source]
28 source: anyhow::Error,
29 },
30
31 #[error("API version mismatch: expected {expected}, got {actual}")]
32 ApiVersionMismatch { expected: u32, actual: u32 },
33
34 #[error("Deserialization failed: {0}")]
35 Deserialization(#[source] rmp_serde::decode::Error),
36
37 #[error("Memory access error: {0}")]
38 MemoryAccess(String),
39
40 #[error("Fuel exhausted (CPU limit exceeded)")]
41 FuelExhausted,
42
43 #[error("Store configuration failed: {0}")]
44 StoreConfig(String),
45}
46
47pub struct PluginLoader {
49 engine: Engine,
50}
51
52pub struct LoadedPlugin {
54 pub manifest: PluginManifest,
56
57 pub instance: PluginInstance,
59}
60
61pub struct PluginInstance {
63 store: Store<()>,
64 instance: Instance,
65 memory: Memory,
66 alloc_fn: TypedFunc<i32, i32>,
67 dealloc_fn: TypedFunc<(i32, i32), ()>,
68}
69
70#[inline]
72fn unpack_ptr_len(packed: i64) -> (i32, i32) {
73 let ptr = (packed >> 32) as i32;
74 let len = (packed & 0xFFFFFFFF) as i32;
75 (ptr, len)
76}
77
78impl PluginLoader {
79 pub fn new() -> Result<Self, LoaderError> {
86 let mut config = Config::new();
87
88 config.consume_fuel(true);
90
91 config.max_wasm_stack(1024 * 1024);
93
94 config.wasm_memory64(false);
96
97 let engine = Engine::new(&config).map_err(LoaderError::EngineCreation)?;
98
99 Ok(Self { engine })
100 }
101
102 pub fn load(&self, wasm_bytes: &[u8]) -> Result<LoadedPlugin, LoaderError> {
104 let module =
106 Module::new(&self.engine, wasm_bytes).map_err(LoaderError::ModuleCompilation)?;
107
108 let mut store = Store::new(&self.engine, ());
110 store
111 .set_fuel(10_000_000)
112 .map_err(|e| LoaderError::StoreConfig(format!("Failed to set fuel: {}", e)))?;
113
114 let linker = Linker::new(&self.engine);
116
117 let instance = linker
119 .instantiate(&mut store, &module)
120 .map_err(LoaderError::Instantiation)?;
121
122 let memory = instance
124 .get_memory(&mut store, "memory")
125 .ok_or_else(|| LoaderError::FunctionNotFound("memory".to_string()))?;
126
127 let alloc_fn = instance
129 .get_typed_func::<i32, i32>(&mut store, "plugin_alloc")
130 .map_err(|_| LoaderError::FunctionNotFound("plugin_alloc".to_string()))?;
131
132 let dealloc_fn = instance
133 .get_typed_func::<(i32, i32), ()>(&mut store, "plugin_dealloc")
134 .map_err(|_| LoaderError::FunctionNotFound("plugin_dealloc".to_string()))?;
135
136 let manifest_fn = instance
138 .get_typed_func::<(), i64>(&mut store, "plugin_manifest")
139 .map_err(|_| LoaderError::FunctionNotFound("plugin_manifest".to_string()))?;
140
141 let packed = manifest_fn.call(&mut store, ()).map_err(|e| {
142 if e.downcast_ref::<Trap>()
143 .is_some_and(|t| *t == Trap::OutOfFuel)
144 {
145 LoaderError::FuelExhausted
146 } else {
147 LoaderError::FunctionCall {
148 function: "plugin_manifest",
149 source: e,
150 }
151 }
152 })?;
153
154 let (ptr, len) = unpack_ptr_len(packed);
155
156 if ptr < 0 || len < 0 {
158 return Err(LoaderError::MemoryAccess(format!(
159 "Invalid manifest pointer/length: ptr={}, len={}",
160 ptr, len
161 )));
162 }
163
164 let manifest_bytes = Self::read_memory(&store, &memory, ptr as usize, len as usize)?;
166 let manifest: PluginManifest =
167 rmp_serde::from_slice(&manifest_bytes).map_err(LoaderError::Deserialization)?;
168
169 if manifest.api_version != API_VERSION {
171 return Err(LoaderError::ApiVersionMismatch {
172 expected: API_VERSION,
173 actual: manifest.api_version,
174 });
175 }
176
177 dealloc_fn
179 .call(&mut store, (ptr, len))
180 .map_err(|e| LoaderError::FunctionCall {
181 function: "plugin_dealloc",
182 source: e,
183 })?;
184
185 Ok(LoadedPlugin {
186 manifest,
187 instance: PluginInstance {
188 store,
189 instance,
190 memory,
191 alloc_fn,
192 dealloc_fn,
193 },
194 })
195 }
196
197 fn read_memory(
198 store: &Store<()>,
199 memory: &Memory,
200 ptr: usize,
201 len: usize,
202 ) -> Result<Vec<u8>, LoaderError> {
203 let data = memory.data(store);
204 let end = ptr.checked_add(len).ok_or_else(|| {
205 LoaderError::MemoryAccess(format!("Integer overflow: ptr={}, len={}", ptr, len))
206 })?;
207 if end > data.len() {
208 return Err(LoaderError::MemoryAccess(format!(
209 "Out of bounds: ptr={}, len={}, memory_size={}",
210 ptr,
211 len,
212 data.len()
213 )));
214 }
215 Ok(data[ptr..end].to_vec())
216 }
217}
218
219impl PluginInstance {
220 pub fn execute(&mut self, args: &[String]) -> Result<ExecuteResult, LoaderError> {
222 let args_bytes = rmp_serde::to_vec(args)
224 .map_err(|e| LoaderError::MemoryAccess(format!("Failed to serialize args: {}", e)))?;
225
226 let args_len: i32 = args_bytes.len().try_into().map_err(|_| {
228 LoaderError::MemoryAccess(format!(
229 "Arguments too large: {} bytes exceeds i32::MAX",
230 args_bytes.len()
231 ))
232 })?;
233 let args_ptr = self.alloc_fn.call(&mut self.store, args_len).map_err(|e| {
234 LoaderError::FunctionCall {
235 function: "plugin_alloc",
236 source: e,
237 }
238 })?;
239
240 self.memory
242 .write(&mut self.store, args_ptr as usize, &args_bytes)
243 .map_err(|e| LoaderError::MemoryAccess(format!("Failed to write args: {}", e)))?;
244
245 let execute_fn = self
247 .instance
248 .get_typed_func::<(i32, i32), i64>(&mut self.store, "plugin_execute")
249 .map_err(|_| LoaderError::FunctionNotFound("plugin_execute".to_string()))?;
250
251 self.store
253 .set_fuel(10_000_000)
254 .map_err(|e| LoaderError::StoreConfig(format!("Failed to reset fuel: {}", e)))?;
255
256 let packed = execute_fn
257 .call(&mut self.store, (args_ptr, args_len))
258 .map_err(|e| {
259 if e.downcast_ref::<Trap>()
260 .is_some_and(|t| *t == Trap::OutOfFuel)
261 {
262 LoaderError::FuelExhausted
263 } else {
264 LoaderError::FunctionCall {
265 function: "plugin_execute",
266 source: e,
267 }
268 }
269 })?;
270
271 let (result_ptr, result_len) = unpack_ptr_len(packed);
272
273 if result_ptr < 0 || result_len < 0 {
275 return Err(LoaderError::MemoryAccess(format!(
276 "Invalid result pointer/length: ptr={}, len={}",
277 result_ptr, result_len
278 )));
279 }
280
281 let result_bytes = PluginLoader::read_memory(
283 &self.store,
284 &self.memory,
285 result_ptr as usize,
286 result_len as usize,
287 )?;
288
289 let result: ExecuteResult =
290 rmp_serde::from_slice(&result_bytes).map_err(LoaderError::Deserialization)?;
291
292 if let Err(e) = self.dealloc_fn.call(&mut self.store, (args_ptr, args_len)) {
294 tracing::warn!(error = %e, ptr = args_ptr, len = args_len, "Failed to deallocate args memory in plugin");
295 }
296 if let Err(e) = self
297 .dealloc_fn
298 .call(&mut self.store, (result_ptr, result_len))
299 {
300 tracing::warn!(error = %e, ptr = result_ptr, len = result_len, "Failed to deallocate result memory in plugin");
301 }
302
303 Ok(result)
304 }
305
306 pub fn resume(
315 &mut self,
316 effect_id: u32,
317 result: &EffectResult,
318 ) -> Result<ExecuteResult, LoaderError> {
319 let result_bytes = rmp_serde::to_vec_named(result).map_err(|e| {
321 LoaderError::MemoryAccess(format!("Failed to serialize effect result: {}", e))
322 })?;
323
324 let result_len: i32 = result_bytes.len().try_into().map_err(|_| {
326 LoaderError::MemoryAccess(format!(
327 "Effect result too large: {} bytes exceeds i32::MAX",
328 result_bytes.len()
329 ))
330 })?;
331 let result_ptr = self
332 .alloc_fn
333 .call(&mut self.store, result_len)
334 .map_err(|e| LoaderError::FunctionCall {
335 function: "plugin_alloc",
336 source: e,
337 })?;
338
339 self.memory
341 .write(&mut self.store, result_ptr as usize, &result_bytes)
342 .map_err(|e| {
343 LoaderError::MemoryAccess(format!("Failed to write effect result: {}", e))
344 })?;
345
346 let resume_fn = self
348 .instance
349 .get_typed_func::<(u32, i32, i32), i64>(&mut self.store, "plugin_resume")
350 .map_err(|_| LoaderError::FunctionNotFound("plugin_resume".to_string()))?;
351
352 self.store
354 .set_fuel(10_000_000)
355 .map_err(|e| LoaderError::StoreConfig(format!("Failed to reset fuel: {}", e)))?;
356
357 let packed = resume_fn
358 .call(&mut self.store, (effect_id, result_ptr, result_len))
359 .map_err(|e| {
360 if e.downcast_ref::<Trap>()
361 .is_some_and(|t| *t == Trap::OutOfFuel)
362 {
363 LoaderError::FuelExhausted
364 } else {
365 LoaderError::FunctionCall {
366 function: "plugin_resume",
367 source: e,
368 }
369 }
370 })?;
371
372 let (exec_result_ptr, exec_result_len) = unpack_ptr_len(packed);
373
374 if exec_result_ptr < 0 || exec_result_len < 0 {
376 return Err(LoaderError::MemoryAccess(format!(
377 "Invalid result pointer/length: ptr={}, len={}",
378 exec_result_ptr, exec_result_len
379 )));
380 }
381
382 let exec_result_bytes = PluginLoader::read_memory(
384 &self.store,
385 &self.memory,
386 exec_result_ptr as usize,
387 exec_result_len as usize,
388 )?;
389
390 let exec_result: ExecuteResult =
391 rmp_serde::from_slice(&exec_result_bytes).map_err(LoaderError::Deserialization)?;
392
393 if let Err(e) = self
395 .dealloc_fn
396 .call(&mut self.store, (result_ptr, result_len))
397 {
398 tracing::warn!(error = %e, ptr = result_ptr, len = result_len, "Failed to deallocate effect result memory");
399 }
400 if let Err(e) = self
401 .dealloc_fn
402 .call(&mut self.store, (exec_result_ptr, exec_result_len))
403 {
404 tracing::warn!(error = %e, ptr = exec_result_ptr, len = exec_result_len, "Failed to deallocate resume result memory");
405 }
406
407 Ok(exec_result)
408 }
409
410 pub fn supports_effects(&mut self) -> bool {
412 self.instance
413 .get_typed_func::<(u32, i32, i32), i64>(&mut self.store, "plugin_resume")
414 .is_ok()
415 }
416}
417
418#[async_trait::async_trait]
423pub trait EffectHandler: Send + Sync {
424 async fn handle(&self, effect: Effect) -> EffectResult;
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn test_loader_creation() {
434 let loader = PluginLoader::new();
435 assert!(loader.is_ok());
436 }
437
438 #[test]
439 fn test_pack_unpack() {
440 let ptr = 0x12345678_i32;
441 let len = 0x00000100_i32;
442 let packed = ((ptr as i64) << 32) | (len as i64 & 0xFFFFFFFF);
443 let (up, ul) = unpack_ptr_len(packed);
444 assert_eq!(up, ptr);
445 assert_eq!(ul, len);
446 }
447}