use crate::traits::*;
use anyhow::{Context, Result};
use std::path::Path;
use wasmtime::*;
pub struct WasmPlugin {
engine: Engine,
module: Module,
metadata: PluginMetadata,
context: Option<PluginContext>,
}
impl WasmPlugin {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let engine = Engine::default();
let module = Module::from_file(&engine, path.as_ref())
.context("Failed to load WASM module")?;
let metadata = Self::extract_metadata(&module)?;
Ok(Self {
engine,
module,
metadata,
context: None,
})
}
pub fn from_bytes(wasm_bytes: &[u8]) -> Result<Self> {
let engine = Engine::default();
let module = Module::new(&engine, wasm_bytes)
.context("Failed to load WASM module from bytes")?;
let metadata = Self::extract_metadata(&module)?;
Ok(Self {
engine,
module,
metadata,
context: None,
})
}
fn extract_metadata(_module: &Module) -> Result<PluginMetadata> {
Ok(PluginMetadata {
name: "wasm-plugin".to_string(),
version: "0.1.0".to_string(),
author: "unknown".to_string(),
description: "WASM plugin".to_string(),
hooks: vec![
PluginHook::OnIssueCreated,
PluginHook::OnIssueUpdated,
],
})
}
fn call_wasm_hook(&self, hook_name: &str, data: &serde_json::Value) -> Result<serde_json::Value> {
let mut store = Store::new(&self.engine, ());
let instance = Instance::new(&mut store, &self.module, &[])
.context("Failed to instantiate WASM module")?;
let memory = instance.get_memory(&mut store, "memory")
.context("WASM module must export 'memory'")?;
let data_bytes = serde_json::to_vec(data)?;
let data_len = data_bytes.len() as i32;
let alloc_fn = instance.get_typed_func::<i32, i32>(&mut store, "alloc")
.context("WASM module must export 'alloc' function")?;
let data_ptr = alloc_fn.call(&mut store, data_len)
.context("Failed to allocate memory in WASM")?;
memory.write(&mut store, data_ptr as usize, &data_bytes)
.context("Failed to write data to WASM memory")?;
let hook_fn = instance.get_typed_func::<(i32, i32), i32>(&mut store, hook_name)
.context(format!("WASM module must export '{}' function", hook_name))?;
let result_ptr = hook_fn.call(&mut store, (data_ptr, data_len))
.context(format!("Failed to call WASM hook '{}'", hook_name))?;
let mut len_bytes = [0u8; 4];
memory.read(&store, result_ptr as usize, &mut len_bytes)
.context("Failed to read result length from WASM memory")?;
let result_len = i32::from_le_bytes(len_bytes) as usize;
let mut result_bytes = vec![0u8; result_len];
memory.read(&store, (result_ptr + 4) as usize, &mut result_bytes)
.context("Failed to read result data from WASM memory")?;
let result: serde_json::Value = serde_json::from_slice(&result_bytes)
.context("Failed to deserialize WASM result")?;
let free_fn = instance.get_typed_func::<i32, ()>(&mut store, "free")
.context("WASM module must export 'free' function")?;
free_fn.call(&mut store, data_ptr)
.context("Failed to free data memory in WASM")?;
free_fn.call(&mut store, result_ptr)
.context("Failed to free result memory in WASM")?;
Ok(result)
}
}
impl Plugin for WasmPlugin {
fn metadata(&self) -> &PluginMetadata {
&self.metadata
}
fn init(&mut self, context: &PluginContext) -> PluginResult<()> {
self.context = Some(context.clone());
let mut store = Store::new(&self.engine, ());
if let Ok(instance) = Instance::new(&mut store, &self.module, &[]) {
if let Ok(init_fn) = instance.get_typed_func::<(), ()>(&mut store, "init") {
init_fn.call(&mut store, ())
.map_err(|e| PluginError::InitError(e.to_string()))?;
}
}
Ok(())
}
fn execute_hook(&mut self, hook: &PluginHook, data: &serde_json::Value) -> PluginResult<serde_json::Value> {
if !self.supports_hook(hook) {
return Err(PluginError::UnsupportedHook(hook.clone()));
}
let hook_name = match hook {
PluginHook::OnIssueCreated => "on_issue_created",
PluginHook::OnIssueUpdated => "on_issue_updated",
PluginHook::OnIssueDeleted => "on_issue_deleted",
PluginHook::OnStatusChanged => "on_status_changed",
PluginHook::OnSyncPush => "on_sync_push",
PluginHook::OnSyncPull => "on_sync_pull",
PluginHook::OnMergeRequestCreated => "on_merge_request_created",
PluginHook::OnCommand(cmd) => cmd.as_str(),
};
self.call_wasm_hook(hook_name, data)
.map_err(|e| PluginError::ExecutionError(e.to_string()))
}
}
impl IssuePlugin for WasmPlugin {}
impl SyncPlugin for WasmPlugin {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wasm_plugin_metadata() {
let engine = Engine::default();
assert!(engine.config().wasm_simd());
}
}