use sen_plugin_api::{Effect, EffectResult, ExecuteResult, PluginManifest, API_VERSION};
use thiserror::Error;
use wasmtime::*;
#[derive(Debug, Error)]
pub enum LoaderError {
#[error("Engine creation failed: {0}")]
EngineCreation(#[source] anyhow::Error),
#[error("Module compilation failed: {0}")]
ModuleCompilation(#[source] anyhow::Error),
#[error("Instantiation failed: {0}")]
Instantiation(#[source] anyhow::Error),
#[error("Function not found: {0}")]
FunctionNotFound(String),
#[error("Function call failed: {function} - {source}")]
FunctionCall {
function: &'static str,
#[source]
source: anyhow::Error,
},
#[error("API version mismatch: expected {expected}, got {actual}")]
ApiVersionMismatch { expected: u32, actual: u32 },
#[error("Deserialization failed: {0}")]
Deserialization(#[source] rmp_serde::decode::Error),
#[error("Memory access error: {0}")]
MemoryAccess(String),
#[error("Fuel exhausted (CPU limit exceeded)")]
FuelExhausted,
#[error("Store configuration failed: {0}")]
StoreConfig(String),
}
pub struct PluginLoader {
engine: Engine,
}
pub struct LoadedPlugin {
pub manifest: PluginManifest,
pub instance: PluginInstance,
}
pub struct PluginInstance {
store: Store<()>,
instance: Instance,
memory: Memory,
alloc_fn: TypedFunc<i32, i32>,
dealloc_fn: TypedFunc<(i32, i32), ()>,
}
#[inline]
fn unpack_ptr_len(packed: i64) -> (i32, i32) {
let ptr = (packed >> 32) as i32;
let len = (packed & 0xFFFFFFFF) as i32;
(ptr, len)
}
impl PluginLoader {
pub fn new() -> Result<Self, LoaderError> {
let mut config = Config::new();
config.consume_fuel(true);
config.max_wasm_stack(1024 * 1024);
config.wasm_memory64(false);
let engine = Engine::new(&config).map_err(LoaderError::EngineCreation)?;
Ok(Self { engine })
}
pub fn load(&self, wasm_bytes: &[u8]) -> Result<LoadedPlugin, LoaderError> {
let module =
Module::new(&self.engine, wasm_bytes).map_err(LoaderError::ModuleCompilation)?;
let mut store = Store::new(&self.engine, ());
store
.set_fuel(10_000_000)
.map_err(|e| LoaderError::StoreConfig(format!("Failed to set fuel: {}", e)))?;
let linker = Linker::new(&self.engine);
let instance = linker
.instantiate(&mut store, &module)
.map_err(LoaderError::Instantiation)?;
let memory = instance
.get_memory(&mut store, "memory")
.ok_or_else(|| LoaderError::FunctionNotFound("memory".to_string()))?;
let alloc_fn = instance
.get_typed_func::<i32, i32>(&mut store, "plugin_alloc")
.map_err(|_| LoaderError::FunctionNotFound("plugin_alloc".to_string()))?;
let dealloc_fn = instance
.get_typed_func::<(i32, i32), ()>(&mut store, "plugin_dealloc")
.map_err(|_| LoaderError::FunctionNotFound("plugin_dealloc".to_string()))?;
let manifest_fn = instance
.get_typed_func::<(), i64>(&mut store, "plugin_manifest")
.map_err(|_| LoaderError::FunctionNotFound("plugin_manifest".to_string()))?;
let packed = manifest_fn.call(&mut store, ()).map_err(|e| {
if e.downcast_ref::<Trap>()
.is_some_and(|t| *t == Trap::OutOfFuel)
{
LoaderError::FuelExhausted
} else {
LoaderError::FunctionCall {
function: "plugin_manifest",
source: e,
}
}
})?;
let (ptr, len) = unpack_ptr_len(packed);
if ptr < 0 || len < 0 {
return Err(LoaderError::MemoryAccess(format!(
"Invalid manifest pointer/length: ptr={}, len={}",
ptr, len
)));
}
let manifest_bytes = Self::read_memory(&store, &memory, ptr as usize, len as usize)?;
let manifest: PluginManifest =
rmp_serde::from_slice(&manifest_bytes).map_err(LoaderError::Deserialization)?;
if manifest.api_version != API_VERSION {
return Err(LoaderError::ApiVersionMismatch {
expected: API_VERSION,
actual: manifest.api_version,
});
}
dealloc_fn
.call(&mut store, (ptr, len))
.map_err(|e| LoaderError::FunctionCall {
function: "plugin_dealloc",
source: e,
})?;
Ok(LoadedPlugin {
manifest,
instance: PluginInstance {
store,
instance,
memory,
alloc_fn,
dealloc_fn,
},
})
}
fn read_memory(
store: &Store<()>,
memory: &Memory,
ptr: usize,
len: usize,
) -> Result<Vec<u8>, LoaderError> {
let data = memory.data(store);
let end = ptr.checked_add(len).ok_or_else(|| {
LoaderError::MemoryAccess(format!("Integer overflow: ptr={}, len={}", ptr, len))
})?;
if end > data.len() {
return Err(LoaderError::MemoryAccess(format!(
"Out of bounds: ptr={}, len={}, memory_size={}",
ptr,
len,
data.len()
)));
}
Ok(data[ptr..end].to_vec())
}
}
impl PluginInstance {
pub fn execute(&mut self, args: &[String]) -> Result<ExecuteResult, LoaderError> {
let args_bytes = rmp_serde::to_vec(args)
.map_err(|e| LoaderError::MemoryAccess(format!("Failed to serialize args: {}", e)))?;
let args_len: i32 = args_bytes.len().try_into().map_err(|_| {
LoaderError::MemoryAccess(format!(
"Arguments too large: {} bytes exceeds i32::MAX",
args_bytes.len()
))
})?;
let args_ptr = self.alloc_fn.call(&mut self.store, args_len).map_err(|e| {
LoaderError::FunctionCall {
function: "plugin_alloc",
source: e,
}
})?;
self.memory
.write(&mut self.store, args_ptr as usize, &args_bytes)
.map_err(|e| LoaderError::MemoryAccess(format!("Failed to write args: {}", e)))?;
let execute_fn = self
.instance
.get_typed_func::<(i32, i32), i64>(&mut self.store, "plugin_execute")
.map_err(|_| LoaderError::FunctionNotFound("plugin_execute".to_string()))?;
self.store
.set_fuel(10_000_000)
.map_err(|e| LoaderError::StoreConfig(format!("Failed to reset fuel: {}", e)))?;
let packed = execute_fn
.call(&mut self.store, (args_ptr, args_len))
.map_err(|e| {
if e.downcast_ref::<Trap>()
.is_some_and(|t| *t == Trap::OutOfFuel)
{
LoaderError::FuelExhausted
} else {
LoaderError::FunctionCall {
function: "plugin_execute",
source: e,
}
}
})?;
let (result_ptr, result_len) = unpack_ptr_len(packed);
if result_ptr < 0 || result_len < 0 {
return Err(LoaderError::MemoryAccess(format!(
"Invalid result pointer/length: ptr={}, len={}",
result_ptr, result_len
)));
}
let result_bytes = PluginLoader::read_memory(
&self.store,
&self.memory,
result_ptr as usize,
result_len as usize,
)?;
let result: ExecuteResult =
rmp_serde::from_slice(&result_bytes).map_err(LoaderError::Deserialization)?;
if let Err(e) = self.dealloc_fn.call(&mut self.store, (args_ptr, args_len)) {
tracing::warn!(error = %e, ptr = args_ptr, len = args_len, "Failed to deallocate args memory in plugin");
}
if let Err(e) = self
.dealloc_fn
.call(&mut self.store, (result_ptr, result_len))
{
tracing::warn!(error = %e, ptr = result_ptr, len = result_len, "Failed to deallocate result memory in plugin");
}
Ok(result)
}
pub fn resume(
&mut self,
effect_id: u32,
result: &EffectResult,
) -> Result<ExecuteResult, LoaderError> {
let result_bytes = rmp_serde::to_vec_named(result).map_err(|e| {
LoaderError::MemoryAccess(format!("Failed to serialize effect result: {}", e))
})?;
let result_len: i32 = result_bytes.len().try_into().map_err(|_| {
LoaderError::MemoryAccess(format!(
"Effect result too large: {} bytes exceeds i32::MAX",
result_bytes.len()
))
})?;
let result_ptr = self
.alloc_fn
.call(&mut self.store, result_len)
.map_err(|e| LoaderError::FunctionCall {
function: "plugin_alloc",
source: e,
})?;
self.memory
.write(&mut self.store, result_ptr as usize, &result_bytes)
.map_err(|e| {
LoaderError::MemoryAccess(format!("Failed to write effect result: {}", e))
})?;
let resume_fn = self
.instance
.get_typed_func::<(u32, i32, i32), i64>(&mut self.store, "plugin_resume")
.map_err(|_| LoaderError::FunctionNotFound("plugin_resume".to_string()))?;
self.store
.set_fuel(10_000_000)
.map_err(|e| LoaderError::StoreConfig(format!("Failed to reset fuel: {}", e)))?;
let packed = resume_fn
.call(&mut self.store, (effect_id, result_ptr, result_len))
.map_err(|e| {
if e.downcast_ref::<Trap>()
.is_some_and(|t| *t == Trap::OutOfFuel)
{
LoaderError::FuelExhausted
} else {
LoaderError::FunctionCall {
function: "plugin_resume",
source: e,
}
}
})?;
let (exec_result_ptr, exec_result_len) = unpack_ptr_len(packed);
if exec_result_ptr < 0 || exec_result_len < 0 {
return Err(LoaderError::MemoryAccess(format!(
"Invalid result pointer/length: ptr={}, len={}",
exec_result_ptr, exec_result_len
)));
}
let exec_result_bytes = PluginLoader::read_memory(
&self.store,
&self.memory,
exec_result_ptr as usize,
exec_result_len as usize,
)?;
let exec_result: ExecuteResult =
rmp_serde::from_slice(&exec_result_bytes).map_err(LoaderError::Deserialization)?;
if let Err(e) = self
.dealloc_fn
.call(&mut self.store, (result_ptr, result_len))
{
tracing::warn!(error = %e, ptr = result_ptr, len = result_len, "Failed to deallocate effect result memory");
}
if let Err(e) = self
.dealloc_fn
.call(&mut self.store, (exec_result_ptr, exec_result_len))
{
tracing::warn!(error = %e, ptr = exec_result_ptr, len = exec_result_len, "Failed to deallocate resume result memory");
}
Ok(exec_result)
}
pub fn supports_effects(&mut self) -> bool {
self.instance
.get_typed_func::<(u32, i32, i32), i64>(&mut self.store, "plugin_resume")
.is_ok()
}
}
#[async_trait::async_trait]
pub trait EffectHandler: Send + Sync {
async fn handle(&self, effect: Effect) -> EffectResult;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_loader_creation() {
let loader = PluginLoader::new();
assert!(loader.is_ok());
}
#[test]
fn test_pack_unpack() {
let ptr = 0x12345678_i32;
let len = 0x00000100_i32;
let packed = ((ptr as i64) << 32) | (len as i64 & 0xFFFFFFFF);
let (up, ul) = unpack_ptr_len(packed);
assert_eq!(up, ptr);
assert_eq!(ul, len);
}
}