#[cfg(feature = "wasm-plugins")]
use wasmi::{
Caller, Config, Engine, Extern, Func, Instance, Linker, Memory, MemoryType, Module, Store,
TypedFunc,
};
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use super::error::PluginError;
use super::host::{HostFunctions, HostState, LogLevel};
use super::types::{PluginContext, PluginLimits, PluginManifest};
#[cfg(feature = "wasm-plugins")]
pub struct WasmPlugin {
name: String,
engine: Engine,
store: Store<HostState>,
instance: Instance,
manifest: PluginManifest,
invocation_count: u64,
total_execution_us: u64,
}
#[cfg(feature = "wasm-plugins")]
impl WasmPlugin {
pub fn load(name: &str, wasm_bytes: &[u8]) -> Result<Self, PluginError> {
Self::load_with_limits(name, wasm_bytes, PluginLimits::default())
}
pub fn load_with_limits(
name: &str,
wasm_bytes: &[u8],
limits: PluginLimits,
) -> Result<Self, PluginError> {
let mut config = Config::default();
config.consume_fuel(true);
let engine = Engine::new(&config);
let module = Module::new(&engine, wasm_bytes)
.map_err(|e| PluginError::CompilationFailed(alloc::format!("{:?}", e)))?;
let host_state = HostState {
limits,
..Default::default()
};
let mut store = Store::new(&engine, host_state);
let fuel = store.data().limits.max_execution_ms * 1000;
store.set_fuel(fuel).ok();
let mut linker: Linker<HostState> = Linker::new(&engine);
Self::register_host_functions(&mut linker)?;
let instance = linker
.instantiate(&mut store, &module)
.map_err(|e| PluginError::InstantiationFailed(alloc::format!("{:?}", e)))?
.start(&mut store)
.map_err(|e| PluginError::InstantiationFailed(alloc::format!("{:?}", e)))?;
let init_result = Self::call_init(&instance, &mut store)?;
if init_result != 0 {
return Err(PluginError::InitFailed(init_result));
}
let manifest = Self::read_manifest(&instance, &mut store)?;
store.data_mut().manifest = manifest.clone();
Ok(Self {
name: name.into(),
engine,
store,
instance,
manifest,
invocation_count: 0,
total_execution_us: 0,
})
}
fn register_host_functions(linker: &mut Linker<HostState>) -> Result<(), PluginError> {
linker
.func_wrap(
"env",
"host_log",
|mut caller: Caller<HostState>, level: i32, msg_ptr: i32, msg_len: i32| {
if let Some(memory) = get_memory(&caller) {
if let Ok(msg) = read_string(&memory, &caller, msg_ptr, msg_len) {
HostFunctions::host_log(caller.data_mut(), level, &msg);
}
}
},
)
.map_err(|e| PluginError::Internal(alloc::format!("linker error: {:?}", e)))?;
linker
.func_wrap(
"env",
"host_get_config",
|mut caller: Caller<HostState>,
key_ptr: i32,
key_len: i32,
out_ptr: i32,
out_cap: i32|
-> i32 {
let memory = match get_memory(&caller) {
Some(m) => m,
None => return -1,
};
let key = match read_string(&memory, &caller, key_ptr, key_len) {
Ok(k) => k,
Err(_) => return -1,
};
let value = match caller.data().config.get(&key) {
Some(v) => v.clone(),
None => return -1,
};
let bytes = value.as_bytes();
let copy_len = bytes.len().min(out_cap as usize);
if write_bytes(&memory, &mut caller, out_ptr, &bytes[..copy_len]).is_err() {
return -1;
}
copy_len as i32
},
)
.map_err(|e| PluginError::Internal(alloc::format!("linker error: {:?}", e)))?;
linker
.func_wrap(
"env",
"host_read_file",
|mut caller: Caller<HostState>,
path_ptr: i32,
path_len: i32,
out_ptr: i32,
out_cap: i32|
-> i32 {
let memory = match get_memory(&caller) {
Some(m) => m,
None => return -1,
};
let path = match read_string(&memory, &caller, path_ptr, path_len) {
Ok(p) => p,
Err(_) => return -1,
};
let mut buf = vec![0u8; out_cap as usize];
match HostFunctions::host_read_file(caller.data(), &path, &mut buf) {
Ok(len) => {
if write_bytes(&memory, &mut caller, out_ptr, &buf[..len]).is_err() {
return -1;
}
len as i32
}
Err(_) => -1,
}
},
)
.map_err(|e| PluginError::Internal(alloc::format!("linker error: {:?}", e)))?;
linker
.func_wrap(
"env",
"host_file_exists",
|caller: Caller<HostState>, path_ptr: i32, path_len: i32| -> i32 {
let memory = match get_memory(&caller) {
Some(m) => m,
None => return -1,
};
let path = match read_string(&memory, &caller, path_ptr, path_len) {
Ok(p) => p,
Err(_) => return -1,
};
HostFunctions::host_file_exists(caller.data(), &path)
},
)
.map_err(|e| PluginError::Internal(alloc::format!("linker error: {:?}", e)))?;
linker
.func_wrap(
"env",
"host_file_size",
|caller: Caller<HostState>, path_ptr: i32, path_len: i32| -> i64 {
let memory = match get_memory(&caller) {
Some(m) => m,
None => return -1,
};
let path = match read_string(&memory, &caller, path_ptr, path_len) {
Ok(p) => p,
Err(_) => return -1,
};
HostFunctions::host_file_size(caller.data(), &path)
},
)
.map_err(|e| PluginError::Internal(alloc::format!("linker error: {:?}", e)))?;
Ok(())
}
fn call_init(instance: &Instance, store: &mut Store<HostState>) -> Result<i32, PluginError> {
let init: TypedFunc<(), i32> = instance
.get_typed_func(store, "plugin_init")
.map_err(|_| PluginError::MissingExport("plugin_init".into()))?;
init.call(store, ())
.map_err(|e| PluginError::ExecutionFailed(alloc::format!("{:?}", e)))
}
fn read_manifest(
instance: &Instance,
store: &mut Store<HostState>,
) -> Result<PluginManifest, PluginError> {
let manifest_fn: TypedFunc<(i32, i32), i32> = instance
.get_typed_func(store, "plugin_manifest")
.map_err(|_| PluginError::MissingExport("plugin_manifest".into()))?;
let memory = instance
.get_memory(store, "memory")
.ok_or_else(|| PluginError::MemoryError("no memory export".into()))?;
let alloc_result = instance.get_typed_func::<i32, i32>(store, "alloc");
let (manifest_ptr, manifest_cap) = if let Ok(alloc) = alloc_result {
let ptr = alloc.call(store, 4096).map_err(|e| {
PluginError::ExecutionFailed(alloc::format!("alloc failed: {:?}", e))
})?;
(ptr, 4096)
} else {
(0x10000, 4096)
};
let len = manifest_fn
.call(store, (manifest_ptr, manifest_cap))
.map_err(|e| PluginError::ExecutionFailed(alloc::format!("{:?}", e)))?;
if len < 0 {
return Err(PluginError::ExecutionFailed(
"plugin_manifest returned error".into(),
));
}
let mut buf = vec![0u8; len as usize];
memory
.read(store, manifest_ptr as usize, &mut buf)
.map_err(|e| PluginError::MemoryError(alloc::format!("{:?}", e)))?;
let json = String::from_utf8(buf).map_err(|_| {
PluginError::InvalidManifest(super::types::ManifestError::InvalidFormat)
})?;
PluginManifest::from_json(&json).map_err(PluginError::InvalidManifest)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn manifest(&self) -> &PluginManifest {
&self.manifest
}
pub fn host_state_mut(&mut self) -> &mut HostState {
self.store.data_mut()
}
pub fn host_state(&self) -> &HostState {
self.store.data()
}
pub fn process(&mut self, ctx: &PluginContext, data: &[u8]) -> Result<Vec<u8>, PluginError> {
if self.store.get_fuel().unwrap_or(0) == 0 {
return Err(PluginError::ResourceLimitExceeded("execution time".into()));
}
let memory = self
.instance
.get_memory(&self.store, "memory")
.ok_or_else(|| PluginError::MemoryError("no memory export".into()))?;
let mem_pages = memory.size(&self.store);
let mem_bytes = mem_pages as usize * 65536; if mem_bytes > self.store.data().limits.max_memory {
return Err(PluginError::ResourceLimitExceeded("memory".into()));
}
let ctx_json = ctx.to_json();
let ctx_bytes = ctx_json.as_bytes();
let out_cap = (data.len() * 2).max(4096);
if out_cap > self.store.data().limits.max_output_size {
return Err(PluginError::ResourceLimitExceeded("output size".into()));
}
let total_needed = ctx_bytes.len() + data.len() + out_cap;
let (ctx_ptr, data_ptr, out_ptr) = if let Ok(alloc) = self
.instance
.get_typed_func::<i32, i32>(&self.store, "alloc")
{
let ctx_ptr = alloc
.call(&mut self.store, ctx_bytes.len() as i32)
.map_err(|e| PluginError::ExecutionFailed(alloc::format!("{:?}", e)))?;
let data_ptr = alloc
.call(&mut self.store, data.len() as i32)
.map_err(|e| PluginError::ExecutionFailed(alloc::format!("{:?}", e)))?;
let out_ptr = alloc
.call(&mut self.store, out_cap as i32)
.map_err(|e| PluginError::ExecutionFailed(alloc::format!("{:?}", e)))?;
(ctx_ptr, data_ptr, out_ptr)
} else {
let ctx_ptr = 0x10000i32;
let data_ptr = ctx_ptr + ctx_bytes.len() as i32 + 16;
let out_ptr = data_ptr + data.len() as i32 + 16;
(ctx_ptr, data_ptr, out_ptr)
};
memory
.write(&mut self.store, ctx_ptr as usize, ctx_bytes)
.map_err(|e| PluginError::MemoryError(alloc::format!("{:?}", e)))?;
memory
.write(&mut self.store, data_ptr as usize, data)
.map_err(|e| PluginError::MemoryError(alloc::format!("{:?}", e)))?;
let process: TypedFunc<(i32, i32, i32, i32, i32, i32), i32> = self
.instance
.get_typed_func(&self.store, "plugin_process")
.map_err(|_| PluginError::MissingExport("plugin_process".into()))?;
let result_len = process
.call(
&mut self.store,
(
ctx_ptr,
ctx_bytes.len() as i32,
data_ptr,
data.len() as i32,
out_ptr,
out_cap as i32,
),
)
.map_err(|e| {
if alloc::format!("{:?}", e).contains("fuel") {
PluginError::ResourceLimitExceeded("execution time".into())
} else {
PluginError::ExecutionFailed(alloc::format!("{:?}", e))
}
})?;
if result_len < 0 {
return Err(PluginError::PluginReturnedError(result_len));
}
let mut output = vec![0u8; result_len as usize];
memory
.read(&self.store, out_ptr as usize, &mut output)
.map_err(|e| PluginError::MemoryError(alloc::format!("{:?}", e)))?;
self.invocation_count += 1;
Ok(output)
}
pub fn destroy(&mut self) -> Result<(), PluginError> {
if let Ok(destroy) = self
.instance
.get_typed_func::<(), ()>(&self.store, "plugin_destroy")
{
destroy
.call(&mut self.store, ())
.map_err(|e| PluginError::ExecutionFailed(alloc::format!("{:?}", e)))?;
}
Ok(())
}
pub fn invocation_count(&self) -> u64 {
self.invocation_count
}
}
#[cfg(feature = "wasm-plugins")]
fn get_memory<T>(caller: &Caller<T>) -> Option<Memory> {
caller.get_export("memory")?.into_memory()
}
#[cfg(feature = "wasm-plugins")]
fn read_string<T>(
memory: &Memory,
caller: &Caller<T>,
ptr: i32,
len: i32,
) -> Result<String, PluginError> {
let mut buf = vec![0u8; len as usize];
memory
.read(caller, ptr as usize, &mut buf)
.map_err(|e| PluginError::MemoryError(alloc::format!("{:?}", e)))?;
String::from_utf8(buf).map_err(|_| PluginError::MemoryError("invalid utf8".into()))
}
#[cfg(feature = "wasm-plugins")]
fn write_bytes<T>(
memory: &Memory,
caller: &mut Caller<T>,
ptr: i32,
data: &[u8],
) -> Result<(), PluginError> {
memory
.write(caller, ptr as usize, data)
.map_err(|e| PluginError::MemoryError(alloc::format!("{:?}", e)))
}
#[cfg(not(feature = "wasm-plugins"))]
pub struct WasmPlugin {
name: String,
manifest: PluginManifest,
}
#[cfg(not(feature = "wasm-plugins"))]
impl WasmPlugin {
pub fn load(_name: &str, _wasm_bytes: &[u8]) -> Result<Self, PluginError> {
Err(PluginError::Internal(
"WASM plugins feature not enabled".into(),
))
}
pub fn load_with_limits(
_name: &str,
_wasm_bytes: &[u8],
_limits: PluginLimits,
) -> Result<Self, PluginError> {
Err(PluginError::Internal(
"WASM plugins feature not enabled".into(),
))
}
pub fn name(&self) -> &str {
&self.name
}
pub fn manifest(&self) -> &PluginManifest {
&self.manifest
}
pub fn host_state_mut(&mut self) -> &mut HostState {
unreachable!("WASM plugins feature not enabled")
}
pub fn process(&mut self, _ctx: &PluginContext, _data: &[u8]) -> Result<Vec<u8>, PluginError> {
Err(PluginError::Internal(
"WASM plugins feature not enabled".into(),
))
}
pub fn destroy(&mut self) -> Result<(), PluginError> {
Ok(())
}
pub fn invocation_count(&self) -> u64 {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(not(feature = "wasm-plugins"))]
fn test_wasm_disabled() {
let result = WasmPlugin::load("test", &[]);
assert!(result.is_err());
}
}