use crate::error::EvenframeError;
use std::collections::BTreeMap;
use std::path::Path;
use tracing::{debug, info};
use wasmtime::*;
use super::plugin_types::{
PluginFieldInput, PluginFieldOutput, PluginTableInput, PluginTableOutput,
};
struct LoadedPlugin {
store: Store<()>,
instance: Instance,
memory: Memory,
}
impl LoadedPlugin {
fn alloc(&mut self, size: i32) -> Result<i32, EvenframeError> {
let func = self
.instance
.get_typed_func::<i32, i32>(&mut self.store, "alloc")
.map_err(|e| EvenframeError::plugin(format!("Missing 'alloc' export: {}", e)))?;
func.call(&mut self.store, size)
.map_err(|e| EvenframeError::plugin(format!("alloc failed: {}", e)))
}
fn dealloc(&mut self, ptr: i32, len: i32) -> Result<(), EvenframeError> {
let func = self
.instance
.get_typed_func::<(i32, i32), ()>(&mut self.store, "dealloc")
.map_err(|e| EvenframeError::plugin(format!("Missing 'dealloc' export: {}", e)))?;
func.call(&mut self.store, (ptr, len))
.map_err(|e| EvenframeError::plugin(format!("dealloc failed: {}", e)))
}
fn call_plugin_fn(
&mut self,
fn_name: &str,
input_json: &[u8],
) -> Result<String, EvenframeError> {
let input_len = input_json.len() as i32;
let input_ptr = self.alloc(input_len)?;
let mem_data = self.memory.data_mut(&mut self.store);
let start = input_ptr as usize;
let end = start + input_json.len();
if end > mem_data.len() {
return Err(EvenframeError::plugin(format!(
"WASM memory too small: need {} bytes at offset {}, have {}",
input_json.len(),
start,
mem_data.len()
)));
}
mem_data[start..end].copy_from_slice(input_json);
let func = self
.instance
.get_typed_func::<(i32, i32), i64>(&mut self.store, fn_name)
.map_err(|e| EvenframeError::plugin(format!("Missing '{}' export: {}", fn_name, e)))?;
let packed = func
.call(&mut self.store, (input_ptr, input_len))
.map_err(|e| {
EvenframeError::plugin(format!("Plugin function '{}' trapped: {}", fn_name, e))
})?;
let out_ptr = (packed >> 32) as i32;
let out_len = (packed & 0xFFFF_FFFF) as i32;
let mem_data = self.memory.data(&self.store);
let out_start = out_ptr as usize;
let out_end = out_start + out_len as usize;
if out_end > mem_data.len() {
return Err(EvenframeError::plugin(format!(
"Plugin returned out-of-bounds pointer: {}+{} > {}",
out_start,
out_len,
mem_data.len()
)));
}
let output_bytes = mem_data[out_start..out_end].to_vec();
let _ = self.dealloc(out_ptr, out_len);
String::from_utf8(output_bytes)
.map_err(|e| EvenframeError::plugin(format!("Plugin returned invalid UTF-8: {}", e)))
}
}
pub struct PluginManager {
_engine: Engine,
plugins: BTreeMap<String, LoadedPlugin>,
}
impl std::fmt::Debug for PluginManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PluginManager")
.field("plugins", &self.plugins.keys().collect::<Vec<_>>())
.finish()
}
}
impl PluginManager {
pub fn new(
plugin_configs: &BTreeMap<String, crate::schemasync::config::PluginConfig>,
project_root: &Path,
) -> Result<Self, EvenframeError> {
let engine = Engine::default();
let mut plugins = BTreeMap::new();
for (name, config) in plugin_configs {
let wasm_path = project_root.join(&config.path);
if !wasm_path.exists() {
return Err(EvenframeError::plugin(format!(
"Plugin '{}': WASM file not found at {}",
name,
wasm_path.display()
)));
}
info!(
"Loading WASM plugin '{}' from {}",
name,
wasm_path.display()
);
let module = Module::from_file(&engine, &wasm_path).map_err(|e| {
EvenframeError::plugin(format!("Plugin '{}': failed to compile WASM: {}", name, e))
})?;
let mut store = Store::new(&engine, ());
let linker = Linker::new(&engine);
let instance = linker.instantiate(&mut store, &module).map_err(|e| {
EvenframeError::plugin(format!("Plugin '{}': failed to instantiate: {}", name, e))
})?;
let memory = instance.get_memory(&mut store, "memory").ok_or_else(|| {
EvenframeError::plugin(format!("Plugin '{}': missing 'memory' export", name))
})?;
instance
.get_typed_func::<i32, i32>(&mut store, "alloc")
.map_err(|_| {
EvenframeError::plugin(format!("Plugin '{}': missing 'alloc' export", name))
})?;
instance
.get_typed_func::<(i32, i32), ()>(&mut store, "dealloc")
.map_err(|_| {
EvenframeError::plugin(format!("Plugin '{}': missing 'dealloc' export", name))
})?;
let has_field = instance
.get_typed_func::<(i32, i32), i64>(&mut store, "generate_field")
.is_ok();
let has_table = instance
.get_typed_func::<(i32, i32), i64>(&mut store, "generate_table")
.is_ok();
if !has_field && !has_table {
return Err(EvenframeError::plugin(format!(
"Plugin '{}': exports neither 'generate_field' nor 'generate_table'",
name
)));
}
debug!(
"Plugin '{}' loaded: generate_field={}, generate_table={}",
name, has_field, has_table
);
plugins.insert(
name.clone(),
LoadedPlugin {
store,
instance,
memory,
},
);
}
info!("Loaded {} WASM plugin(s)", plugins.len());
Ok(Self {
_engine: engine,
plugins,
})
}
pub fn generate_field_value(
&mut self,
plugin_name: &str,
input: &PluginFieldInput,
) -> Result<String, EvenframeError> {
let plugin = self
.plugins
.get_mut(plugin_name)
.ok_or_else(|| EvenframeError::plugin(format!("Plugin '{}' not found", plugin_name)))?;
let input_json = serde_json::to_vec(input)
.map_err(|e| EvenframeError::plugin(format!("Failed to serialize input: {}", e)))?;
let output_str = plugin.call_plugin_fn("generate_field", &input_json)?;
let output: PluginFieldOutput = serde_json::from_str(&output_str).map_err(|e| {
EvenframeError::plugin(format!(
"Plugin '{}' returned invalid JSON: {} (raw: {})",
plugin_name, e, output_str
))
})?;
if let Some(err) = output.error {
return Err(EvenframeError::plugin(format!(
"Plugin '{}' error: {}",
plugin_name, err
)));
}
output.value.ok_or_else(|| {
EvenframeError::plugin(format!(
"Plugin '{}' returned neither value nor error",
plugin_name
))
})
}
pub fn generate_table_values(
&mut self,
plugin_name: &str,
input: &PluginTableInput,
) -> Result<BTreeMap<String, String>, EvenframeError> {
let plugin = self
.plugins
.get_mut(plugin_name)
.ok_or_else(|| EvenframeError::plugin(format!("Plugin '{}' not found", plugin_name)))?;
let input_json = serde_json::to_vec(input)
.map_err(|e| EvenframeError::plugin(format!("Failed to serialize input: {}", e)))?;
let output_str = plugin.call_plugin_fn("generate_table", &input_json)?;
let output: PluginTableOutput = serde_json::from_str(&output_str).map_err(|e| {
EvenframeError::plugin(format!(
"Plugin '{}' returned invalid JSON: {} (raw: {})",
plugin_name, e, output_str
))
})?;
if let Some(err) = output.error {
return Err(EvenframeError::plugin(format!(
"Plugin '{}' error: {}",
plugin_name, err
)));
}
output.fields.ok_or_else(|| {
EvenframeError::plugin(format!(
"Plugin '{}' returned neither fields nor error",
plugin_name
))
})
}
}