use std::hash::{Hash, Hasher};
use std::sync::Arc;
use wasmtime::{Engine, Linker, Module};
use crate::engine::HostState;
use crate::engine::HotEngineConfig;
use crate::error::WasmtimeRuntimeError;
use cc_lb_plugin_wire::schema::HookKind;
use crate::inspect::{ModuleInspection, inspect_wasm, inspect_wasm_agnostic};
use crate::probe::probe_hook_dispatch;
pub(crate) const VALIDATION_POLICY_VERSION: u32 = 1;
const WASMTIME_CACHE_COMPATIBILITY_VERSION: &str = "wasmtime-46.0.1";
pub(crate) fn compute_content_hash(
engine: &Engine,
config: &HotEngineConfig,
wasm_bytes: &[u8],
) -> [u8; 32] {
compute_content_hash_with_policy_version(engine, config, wasm_bytes, VALIDATION_POLICY_VERSION)
}
fn compute_content_hash_with_policy_version(
engine: &Engine,
config: &HotEngineConfig,
wasm_bytes: &[u8],
validation_policy_version: u32,
) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(b"cc-lb-compiled-wasm-v1");
hasher.update(WASMTIME_CACHE_COMPATIBILITY_VERSION.as_bytes());
hasher.update(&validation_policy_version.to_le_bytes());
let mut compatibility_hasher = std::collections::hash_map::DefaultHasher::new();
engine
.precompile_compatibility_hash()
.hash(&mut compatibility_hasher);
hasher.update(&compatibility_hasher.finish().to_le_bytes());
hasher.update(&config.memory_max_pages.to_le_bytes());
hasher.update(&config.max_wasm_stack.to_le_bytes());
hasher.update(&config.pool_total_memories.to_le_bytes());
hasher.update(&config.pool_total_core_instances.to_le_bytes());
hasher.update(&config.memory_reservation_bytes.to_le_bytes());
hasher.update(&config.memory_guard_bytes.to_le_bytes());
hasher.update(&config.wire_bounds.output_body_bytes.to_le_bytes());
hasher.update(&config.wire_bounds.max_headers.to_le_bytes());
hasher.update(&config.wire_bounds.max_header_value_bytes.to_le_bytes());
hasher.update(&config.wire_bounds.reason_bytes.to_le_bytes());
hasher.update(&[config.cookie_redaction.into()]);
hasher.update(wasm_bytes);
*hasher.finalize().as_bytes()
}
pub fn compile_module(
engine: &Engine,
linker: &Linker<HostState>,
kind: HookKind,
wasm_bytes: &[u8],
) -> Result<(Arc<wasmtime::InstancePre<HostState>>, ModuleInspection), WasmtimeRuntimeError> {
let inspection = inspect_wasm(kind, wasm_bytes)?;
let (instance_pre, _) = compile_instance_pre(engine, linker, wasm_bytes)?;
Ok((instance_pre, inspection))
}
#[allow(unsafe_code)]
fn compile_instance_pre(
engine: &Engine,
linker: &Linker<HostState>,
wasm_bytes: &[u8],
) -> Result<(Arc<wasmtime::InstancePre<HostState>>, usize), WasmtimeRuntimeError> {
let cwasm = engine
.precompile_module(wasm_bytes)
.map_err(|e| WasmtimeRuntimeError::ModuleCompile(anyhow::Error::from(e)))?;
let compiled_bytes = cwasm.len();
let module = unsafe {
Module::deserialize(engine, &cwasm)
.map_err(|e| WasmtimeRuntimeError::ModuleCompile(anyhow::Error::from(e)))?
};
let instance_pre = linker
.instantiate_pre(&module)
.map_err(|e| WasmtimeRuntimeError::InstantiateFailed(anyhow::Error::from(e)))?;
Ok((Arc::new(instance_pre), compiled_bytes))
}
pub fn admit_wasm(
engine: &Engine,
linker: &Linker<HostState>,
kind: HookKind,
wasm_bytes: &[u8],
config: &HotEngineConfig,
) -> Result<(Arc<wasmtime::InstancePre<HostState>>, ModuleInspection), WasmtimeRuntimeError> {
let inspection = inspect_wasm(kind, wasm_bytes)?;
admit_inspected_wasm(engine, linker, wasm_bytes, config, inspection)
}
pub fn admit_wasm_agnostic(
engine: &Engine,
linker: &Linker<HostState>,
wasm_bytes: &[u8],
config: &HotEngineConfig,
) -> Result<(Arc<wasmtime::InstancePre<HostState>>, ModuleInspection), WasmtimeRuntimeError> {
let inspection = inspect_wasm_agnostic(wasm_bytes)?;
admit_inspected_wasm(engine, linker, wasm_bytes, config, inspection)
}
fn admit_inspected_wasm(
engine: &Engine,
linker: &Linker<HostState>,
wasm_bytes: &[u8],
config: &HotEngineConfig,
inspection: ModuleInspection,
) -> Result<(Arc<wasmtime::InstancePre<HostState>>, ModuleInspection), WasmtimeRuntimeError> {
let content_hash = compute_content_hash(engine, config, wasm_bytes);
let compiled_modules = crate::compiled_cache::process_wide_compiled_module_cache();
let instance_pre = compiled_modules.get_or_compile(content_hash, || {
compile_instance_pre(engine, linker, wasm_bytes)
})?;
for (hook, wire_version) in &inspection.hook_versions {
probe_hook_dispatch(
Arc::clone(&instance_pre),
*hook,
*wire_version,
&inspection.metadata,
config.memory_max_pages,
)?;
}
Ok((instance_pre, inspection))
}
#[cfg(test)]
mod agnostic_tests;
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use wasmtime::Linker;
use super::{compile_instance_pre, compute_content_hash_with_policy_version};
use crate::compiled_cache::CompiledModuleCache;
use crate::engine::{HostState, HotEngineConfig, build_hot_engine};
#[test]
fn compiled_module_cache_reuses_identical_bytes_without_recompiling() {
let engine = build_hot_engine(&HotEngineConfig::default()).expect("engine");
let linker = Linker::<HostState>::new(&engine);
let cache = CompiledModuleCache::new_for_test();
let wasm = wat::parse_str("(module)").expect("valid wasm");
let key = compute_content_hash_with_policy_version(
&engine,
&HotEngineConfig::default(),
&wasm,
1,
);
let compile_count = AtomicUsize::new(0);
let first = cache
.get_or_compile(key, || {
compile_count.fetch_add(1, Ordering::Relaxed);
compile_instance_pre(&engine, &linker, &wasm)
})
.expect("first compilation");
let second = cache
.get_or_compile(key, || {
compile_count.fetch_add(1, Ordering::Relaxed);
compile_instance_pre(&engine, &linker, &wasm)
})
.expect("cached compilation");
assert!(std::sync::Arc::ptr_eq(&first, &second));
assert_eq!(compile_count.load(Ordering::Relaxed), 1);
}
#[test]
fn compiled_module_cache_misses_for_different_bytes_or_validation_policy() {
let config = HotEngineConfig::default();
let engine = build_hot_engine(&config).expect("engine");
let linker = Linker::<HostState>::new(&engine);
let cache = CompiledModuleCache::new_for_test();
let first_wasm = wat::parse_str("(module)").expect("valid first wasm");
let changed_wasm = wat::parse_str("(module (func))").expect("valid changed wasm");
let first_key = compute_content_hash_with_policy_version(&engine, &config, &first_wasm, 1);
let changed_bytes_key =
compute_content_hash_with_policy_version(&engine, &config, &changed_wasm, 1);
let changed_policy_key =
compute_content_hash_with_policy_version(&engine, &config, &first_wasm, 2);
let compile_count = AtomicUsize::new(0);
cache
.get_or_compile(first_key, || {
compile_count.fetch_add(1, Ordering::Relaxed);
compile_instance_pre(&engine, &linker, &first_wasm)
})
.expect("first compilation");
cache
.get_or_compile(changed_bytes_key, || {
compile_count.fetch_add(1, Ordering::Relaxed);
compile_instance_pre(&engine, &linker, &changed_wasm)
})
.expect("changed bytes compilation");
cache
.get_or_compile(changed_policy_key, || {
compile_count.fetch_add(1, Ordering::Relaxed);
compile_instance_pre(&engine, &linker, &first_wasm)
})
.expect("changed policy compilation");
assert_eq!(compile_count.load(Ordering::Relaxed), 3);
}
}