diff --git a/.gitignore b/.gitignore index cd712c9..8b6c886 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,6 @@ java/.DS_Store extism-maturin/src/extism.h runtime/*.log libextism/example -libextism/extism*.pc \ No newline at end of file +libextism/extism*.pc +*.cwasm +test-cache diff --git a/manifest/src/lib.rs b/manifest/src/lib.rs index 8ca620f..de6f456 100644 --- a/manifest/src/lib.rs +++ b/manifest/src/lib.rs @@ -181,6 +181,18 @@ impl Wasm { Wasm::Url { req: _, meta } => meta, } } + + /// Update Wasm module name + pub fn with_name(mut self, name: impl Into) -> Self { + self.meta_mut().name = Some(name.into()); + self + } + + /// Update Wasm module hash + pub fn with_hash(mut self, hash: impl Into) -> Self { + self.meta_mut().hash = Some(hash.into()); + self + } } #[cfg(feature = "json_schema")] @@ -237,6 +249,11 @@ impl Manifest { } } + pub fn with_wasm(mut self, wasm: impl Into) -> Self { + self.wasm.push(wasm.into()); + self + } + /// Disallow HTTP requests to all hosts pub fn disallow_all_hosts(mut self) -> Self { self.allowed_hosts = Some(vec![]); @@ -329,7 +346,7 @@ mod base64 { use serde::{Deserializer, Serializer}; pub fn serialize(v: &Vec, s: S) -> Result { - let base64 = general_purpose::STANDARD.encode(v); + let base64 = general_purpose::STANDARD.encode(v.as_slice()); String::serialize(&base64, s) } diff --git a/runtime/README.md b/runtime/README.md index a6259e7..faafa64 100644 --- a/runtime/README.md +++ b/runtime/README.md @@ -24,6 +24,7 @@ There are a few environment variables that can be used for debugging purposes: - `EXTISM_COREDUMP=extism.core`: write [coredump](https://github.com/WebAssembly/tool-conventions/blob/main/Coredump.md) to a file when a WebAssembly function traps - `EXTISM_DEBUG=1`: generate debug information - `EXTISM_PROFILE=perf|jitdump|vtune`: enable Wasmtime profiling +- `EXTISM_CACHE_CONFIG=path/to/config.toml`: enable Wasmtime cache, see [the docs](https://docs.wasmtime.dev/cli-cache.html) for details about configuration. Setting this to an empty string will disable caching. > *Note*: The debug and coredump info will only be written if the plug-in has an error. diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 529d426..d489aa1 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -23,11 +23,10 @@ pub use current_plugin::CurrentPlugin; pub use extism_convert::{FromBytes, FromBytesOwned, ToBytes}; pub use extism_manifest::{Manifest, Wasm, WasmMetadata}; pub use function::{Function, UserData, Val, ValType, PTR}; -pub use plugin::{CancelHandle, Plugin, EXTISM_ENV_MODULE, EXTISM_USER_MODULE}; -pub use plugin_builder::PluginBuilder; +pub use plugin::{CancelHandle, Plugin, WasmInput, EXTISM_ENV_MODULE, EXTISM_USER_MODULE}; +pub use plugin_builder::{DebugOptions, PluginBuilder}; pub(crate) use internal::{Internal, Wasi}; -pub(crate) use plugin_builder::DebugOptions; pub(crate) use timer::{Timer, TimerAction}; pub(crate) use tracing::{debug, error, trace, warn}; diff --git a/runtime/src/manifest.rs b/runtime/src/manifest.rs index 4ef1d7b..ca2365f 100644 --- a/runtime/src/manifest.rs +++ b/runtime/src/manifest.rs @@ -4,6 +4,7 @@ use std::io::Read; use sha2::Digest; +use crate::plugin::WasmInput; use crate::*; fn hex(data: &[u8]) -> String { @@ -14,32 +15,9 @@ fn hex(data: &[u8]) -> String { s } -#[allow(unused)] -fn cache_add_file(hash: &str, data: &[u8]) -> Result<(), Error> { - let cache_dir = std::env::temp_dir().join("extism-cache"); - let _ = std::fs::create_dir(&cache_dir); - let file = cache_dir.join(hash); - if file.exists() { - return Ok(()); - } - std::fs::write(file, data)?; - Ok(()) -} - -fn cache_get_file(hash: &str) -> Result>, Error> { - let cache_dir = std::env::temp_dir().join("extism-cache"); - let file = cache_dir.join(hash); - if file.exists() { - let r = std::fs::read(file)?; - return Ok(Some(r)); - } - - Ok(None) -} - -fn check_hash(hash: &Option, data: &[u8]) -> Result<(), Error> { +fn check_hash(hash: &Option, data: &[u8]) -> Result, Error> { match hash { - None => Ok(()), + None => Ok(None), Some(hash) => { let digest = sha2::Sha256::digest(data); let hex = hex(&digest); @@ -50,7 +28,7 @@ fn check_hash(hash: &Option, data: &[u8]) -> Result<(), Error> { hash )); } - Ok(()) + Ok(Some(hex)) } } } @@ -80,7 +58,6 @@ fn to_module(engine: &Engine, wasm: &extism_manifest::Wasm) -> Result<(String, M file.read_to_end(&mut buf)?; check_hash(&meta.hash, &buf)?; - Ok((name, Module::new(engine, buf)?)) } extism_manifest::Wasm::Data { meta, data } => { @@ -117,17 +94,9 @@ fn to_module(engine: &Engine, wasm: &extism_manifest::Wasm) -> Result<(String, M } }; - if let Some(h) = &meta.hash { - if let Ok(Some(data)) = cache_get_file(h) { - check_hash(&meta.hash, &data)?; - let module = Module::new(engine, data)?; - return Ok((name.to_string(), module)); - } - } - #[cfg(not(feature = "register-http"))] { - return Err(anyhow::format_err!("HTTP registration is disabled")); + return anyhow::bail!("HTTP registration is disabled"); } #[cfg(feature = "register-http")] @@ -144,15 +113,12 @@ fn to_module(engine: &Engine, wasm: &extism_manifest::Wasm) -> Result<(String, M let mut data = Vec::new(); r.read_to_end(&mut data)?; - // Try to cache file - if let Some(hash) = &meta.hash { - cache_add_file(hash, &data); - } - + // Check hash against manifest check_hash(&meta.hash, &data)?; // Convert fetched data to module let module = Module::new(engine, data)?; + Ok((name.to_string(), module)) } } @@ -163,52 +129,64 @@ const WASM_MAGIC: [u8; 4] = [0x00, 0x61, 0x73, 0x6d]; pub(crate) fn load( engine: &Engine, - data: &[u8], + input: WasmInput<'_>, ) -> Result<(extism_manifest::Manifest, BTreeMap), Error> { - let extism_module = Module::new(engine, WASM)?; - let has_magic = data.len() >= 4 && data[0..4] == WASM_MAGIC; - let is_wast = data.starts_with(b"(module") || data.starts_with(b";;"); - if !has_magic && !is_wast { - trace!("Loading manifest"); - if let Ok(s) = std::str::from_utf8(data) { - if let Ok(t) = toml::from_str::(s) { - trace!("Manifest is TOML"); - let mut m = modules(&t, engine)?; - m.insert(EXTISM_ENV_MODULE.to_string(), extism_module); - return Ok((t, m)); + let mut mods = BTreeMap::new(); + mods.insert(EXTISM_ENV_MODULE.to_string(), Module::new(engine, WASM)?); + + match input { + WasmInput::Data(data) => { + let has_magic = data.len() >= 4 && data[0..4] == WASM_MAGIC; + let is_wat = data.starts_with(b"(module") || data.starts_with(b";;"); + if !has_magic && !is_wat { + trace!("Loading manifest"); + if let Ok(s) = std::str::from_utf8(&data) { + let t = if let Ok(t) = toml::from_str::(s) { + trace!("Manifest is TOML"); + modules(engine, &t, &mut mods)?; + t + } else if let Ok(t) = serde_json::from_str::(s) { + trace!("Manifest is JSON"); + modules(engine, &t, &mut mods)?; + t + } else { + anyhow::bail!("Unknown manifest format"); + }; + return Ok((t, mods)); + } } + + let m = Module::new(engine, data)?; + mods.insert("main".to_string(), m); + Ok((Default::default(), mods)) + } + WasmInput::Manifest(m) => { + trace!("Loading from existing manifest"); + modules(engine, &m, &mut mods)?; + Ok((m, mods)) + } + WasmInput::ManifestRef(m) => { + trace!("Loading from existing manifest"); + modules(engine, m, &mut mods)?; + Ok((m.clone(), mods)) } - - let t = serde_json::from_slice::(data)?; - trace!("Manifest is JSON"); - let mut m = modules(&t, engine)?; - m.insert(EXTISM_ENV_MODULE.to_string(), extism_module); - return Ok((t, m)); } - - trace!("Loading WASM module bytes"); - let m = Module::new(engine, data)?; - let mut modules = BTreeMap::new(); - modules.insert(EXTISM_ENV_MODULE.to_string(), extism_module); - modules.insert("main".to_string(), m); - Ok((Default::default(), modules)) } pub(crate) fn modules( - manifest: &extism_manifest::Manifest, engine: &Engine, -) -> Result, Error> { + manifest: &extism_manifest::Manifest, + modules: &mut BTreeMap, +) -> Result<(), Error> { if manifest.wasm.is_empty() { return Err(anyhow::format_err!("No wasm files specified")); } - let mut modules = BTreeMap::new(); - // If there's only one module, it should be called `main` if manifest.wasm.len() == 1 { let (_, m) = to_module(engine, &manifest.wasm[0])?; modules.insert("main".to_string(), m); - return Ok(modules); + return Ok(()); } for f in &manifest.wasm { @@ -217,5 +195,5 @@ pub(crate) fn modules( modules.insert(name, m); } - Ok(modules) + Ok(()) } diff --git a/runtime/src/plugin.rs b/runtime/src/plugin.rs index ab42382..ebf41e1 100644 --- a/runtime/src/plugin.rs +++ b/runtime/src/plugin.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::{collections::BTreeMap, path::PathBuf}; use crate::*; @@ -109,7 +109,7 @@ impl Internal for Plugin { } } -fn profiling_strategy() -> ProfilingStrategy { +pub(crate) fn profiling_strategy() -> ProfilingStrategy { match std::env::var("EXTISM_PROFILE").as_deref() { Ok("perf") => ProfilingStrategy::PerfMap, Ok("jitdump") => ProfilingStrategy::JitDump, @@ -122,55 +122,106 @@ fn profiling_strategy() -> ProfilingStrategy { } } -pub trait WasmInput<'a>: Into> {} +/// Defines an input type for Wasm data. +/// +/// Types that implement `Into` can be passed directly into `Plugin::new` +pub enum WasmInput<'a> { + /// Raw Wasm module + Data(std::borrow::Cow<'a, [u8]>), + /// Owned manifest + Manifest(Manifest), + /// Borrowed manifest + ManifestRef(&'a Manifest), +} -impl<'a> WasmInput<'a> for Manifest {} -impl<'a> WasmInput<'a> for &Manifest {} -impl<'a> WasmInput<'a> for &'a [u8] {} -impl<'a> WasmInput<'a> for Vec {} +impl<'a> From for WasmInput<'a> { + fn from(value: Manifest) -> Self { + WasmInput::Manifest(value) + } +} + +impl<'a> From<&'a Manifest> for WasmInput<'a> { + fn from(value: &'a Manifest) -> Self { + WasmInput::ManifestRef(value) + } +} + +impl<'a> From<&'a mut Manifest> for WasmInput<'a> { + fn from(value: &'a mut Manifest) -> Self { + WasmInput::ManifestRef(value) + } +} + +impl<'a> From<&'a [u8]> for WasmInput<'a> { + fn from(value: &'a [u8]) -> Self { + WasmInput::Data(value.into()) + } +} + +impl<'a> From<&'a str> for WasmInput<'a> { + fn from(value: &'a str) -> Self { + WasmInput::Data(value.as_bytes().into()) + } +} + +impl<'a> From> for WasmInput<'a> { + fn from(value: Vec) -> Self { + WasmInput::Data(value.into()) + } +} + +impl<'a> From<&'a Vec> for WasmInput<'a> { + fn from(value: &'a Vec) -> Self { + WasmInput::Data(value.into()) + } +} impl Plugin { /// Create a new plugin from a Manifest or WebAssembly module, and host functions. The `with_wasi` /// parameter determines whether or not the module should be executed with WASI enabled. pub fn new<'a>( - wasm: impl WasmInput<'a>, + wasm: impl Into>, imports: impl IntoIterator, with_wasi: bool, ) -> Result { - Self::build_new(wasm.into(), imports, with_wasi, Default::default()) + Self::build_new(wasm.into(), imports, with_wasi, Default::default(), None) } pub(crate) fn build_new( - wasm: impl AsRef<[u8]>, + wasm: WasmInput<'_>, imports: impl IntoIterator, with_wasi: bool, - mut debug_options: DebugOptions, + debug_options: DebugOptions, + cache_dir: Option>, ) -> Result { - // Configure debug options - debug_options.debug_info = - debug_options.debug_info || std::env::var("EXTISM_DEBUG").is_ok(); - if let Ok(x) = std::env::var("EXTISM_COREDUMP") { - debug_options.coredump = Some(std::path::PathBuf::from(x)); - }; - if let Ok(x) = std::env::var("EXTISM_MEMDUMP") { - debug_options.memdump = Some(std::path::PathBuf::from(x)); - }; - let profiling_strategy = debug_options - .profiling_strategy - .map_or(ProfilingStrategy::None, |_| profiling_strategy()); - debug_options.profiling_strategy = Some(profiling_strategy); - // Setup wasmtime types - let engine = Engine::new( - Config::new() - .epoch_interruption(true) - .debug_info(debug_options.debug_info) - .coredump_on_trap(debug_options.coredump.is_some()) - .profiler(profiling_strategy) - .wasm_tail_call(true) - .wasm_function_references(true), - )?; - let (manifest, modules) = manifest::load(&engine, wasm.as_ref())?; + let mut config = Config::new(); + config + .epoch_interruption(true) + .debug_info(debug_options.debug_info) + .coredump_on_trap(debug_options.coredump.is_some()) + .profiler(debug_options.profiling_strategy) + .wasm_tail_call(true) + .wasm_function_references(true); + + match cache_dir { + Some(None) => (), + Some(Some(path)) => { + config.cache_config_load(path)?; + } + None => { + if let Ok(env) = std::env::var("EXTISM_CACHE_CONFIG") { + if !env.is_empty() { + config.cache_config_load(&env)?; + } + } else { + config.cache_config_load_default()?; + } + } + } + + let engine = Engine::new(&config)?; + let (manifest, modules) = manifest::load(&engine, wasm)?; let available_pages = manifest.memory.max_pages; debug!("Available pages: {available_pages:?}"); diff --git a/runtime/src/plugin_builder.rs b/runtime/src/plugin_builder.rs index 0289b71..70dfb2c 100644 --- a/runtime/src/plugin_builder.rs +++ b/runtime/src/plugin_builder.rs @@ -1,29 +1,55 @@ +use std::path::PathBuf; + use crate::{plugin::WasmInput, *}; -#[derive(Default, Clone)] -pub(crate) struct DebugOptions { - pub(crate) profiling_strategy: Option, - pub(crate) coredump: Option, - pub(crate) memdump: Option, - pub(crate) debug_info: bool, +#[derive(Clone)] +pub struct DebugOptions { + pub profiling_strategy: wasmtime::ProfilingStrategy, + pub coredump: Option, + pub memdump: Option, + pub debug_info: bool, +} + +impl Default for DebugOptions { + fn default() -> Self { + let debug_info = std::env::var("EXTISM_DEBUG").is_ok(); + let coredump = if let Ok(x) = std::env::var("EXTISM_COREDUMP") { + Some(std::path::PathBuf::from(x)) + } else { + None + }; + let memdump = if let Ok(x) = std::env::var("EXTISM_MEMDUMP") { + Some(std::path::PathBuf::from(x)) + } else { + None + }; + DebugOptions { + profiling_strategy: plugin::profiling_strategy(), + coredump, + memdump, + debug_info, + } + } } /// PluginBuilder is used to configure and create `Plugin` instances pub struct PluginBuilder<'a> { - source: std::borrow::Cow<'a, [u8]>, + source: WasmInput<'a>, wasi: bool, functions: Vec, debug_options: DebugOptions, + cache_config: Option>, } impl<'a> PluginBuilder<'a> { /// Create a new `PluginBuilder` from a `Manifest` or raw Wasm bytes - pub fn new(plugin: impl WasmInput<'a>) -> Self { + pub fn new(plugin: impl Into>) -> Self { PluginBuilder { source: plugin.into(), wasi: false, functions: vec![], debug_options: DebugOptions::default(), + cache_config: None, } } @@ -80,28 +106,56 @@ impl<'a> PluginBuilder<'a> { self } + /// Set profiling strategy pub fn with_profiling_strategy(mut self, p: wasmtime::ProfilingStrategy) -> Self { - self.debug_options.profiling_strategy = Some(p); + self.debug_options.profiling_strategy = p; self } - pub fn with_coredump(mut self, path: impl AsRef) -> Self { - self.debug_options.coredump = Some(path.as_ref().to_path_buf()); + /// Enable Wasmtime coredump on trap + pub fn with_coredump(mut self, path: impl Into) -> Self { + self.debug_options.coredump = Some(path.into()); self } - pub fn with_memdump(mut self, path: impl AsRef) -> Self { - self.debug_options.memdump = Some(path.as_ref().to_path_buf()); + /// Enable Extism memory dump when plugin calls return an error + pub fn with_memdump(mut self, path: impl Into) -> Self { + self.debug_options.memdump = Some(path.into()); self } + /// Compile with debug info pub fn with_debug_info(mut self) -> Self { self.debug_options.debug_info = true; self } + /// Configure debug options + pub fn with_debug_options(mut self, options: DebugOptions) -> Self { + self.debug_options = options; + self + } + + /// Set wasmtime compilation cache config path + pub fn with_cache_config(mut self, dir: impl Into) -> Self { + self.cache_config = Some(Some(dir.into())); + self + } + + /// Turn wasmtime compilation caching off + pub fn with_cache_disabled(mut self) -> Self { + self.cache_config = Some(None); + self + } + /// Generate a new plugin with the configured settings pub fn build(self) -> Result { - Plugin::build_new(self.source, self.functions, self.wasi, self.debug_options) + Plugin::build_new( + self.source, + self.functions, + self.wasi, + self.debug_options, + self.cache_config, + ) } } diff --git a/runtime/src/tests/runtime.rs b/runtime/src/tests/runtime.rs index 6c45568..df009bd 100644 --- a/runtime/src/tests/runtime.rs +++ b/runtime/src/tests/runtime.rs @@ -539,3 +539,36 @@ fn test_http_post() { assert!(!res.is_empty()); assert!(res.contains(&data)); } + +#[test] +fn test_disable_cache() { + // Warmup cache + let _plugin: CountVowelsPlugin = PluginBuilder::new(WASM_NO_FUNCTIONS) + .build() + .unwrap() + .try_into() + .unwrap(); + + // This should be fast + let start = std::time::Instant::now(); + let mut plugin: CountVowelsPlugin = PluginBuilder::new(WASM_NO_FUNCTIONS) + .build() + .unwrap() + .try_into() + .unwrap(); + let t = std::time::Instant::now() - start; + let _output: Json = plugin.count_vowels("abc123").unwrap(); + + // This should take longer than the first run + let start = std::time::Instant::now(); + let mut plugin: CountVowelsPlugin = PluginBuilder::new(WASM_NO_FUNCTIONS) + .with_cache_disabled() + .build() + .unwrap() + .try_into() + .unwrap(); + let t1 = std::time::Instant::now() - start; + let _output: Json = plugin.count_vowels("abc123").unwrap(); + + assert!(t < t1); +}