use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::RwLock;
use uni_plugin::AbiRange;
use wasmtime::Engine;
use wasmtime::component::Linker;
use crate::error::WasmError;
use crate::host_state::HostState;
use crate::linker::{build_scalar_linker_v1, build_scalar_linker_v2};
pub const SUPPORTED_MAJORS: &[u64] = &[1, 2];
type CacheKey = (u64, String);
pub(crate) fn major_for_abi(abi: &AbiRange) -> Result<u64, WasmError> {
SUPPORTED_MAJORS
.iter()
.copied()
.find(|m| abi.matches(*m))
.ok_or_else(|| WasmError::AbiUnsupported {
requested: abi.as_str().to_owned(),
supported: SUPPORTED_MAJORS.to_vec(),
})
}
pub struct MultiVersionLinker {
engine: Engine,
cache: RwLock<HashMap<CacheKey, Arc<Linker<HostState>>>>,
}
impl std::fmt::Debug for MultiVersionLinker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MultiVersionLinker")
.field("cached_entries", &self.cache.read().len())
.finish_non_exhaustive()
}
}
impl MultiVersionLinker {
#[must_use]
pub fn new(engine: Engine) -> Self {
Self {
engine,
cache: RwLock::new(HashMap::new()),
}
}
pub fn linker_for(
&self,
abi: &AbiRange,
effective_caps: &uni_plugin::CapabilitySet,
) -> Result<Arc<Linker<HostState>>, WasmError> {
let major = major_for_abi(abi)?;
let key: CacheKey = (major, caps_signature(effective_caps));
if let Some(cached) = self.cache.read().get(&key) {
return Ok(Arc::clone(cached));
}
let mut cache = self.cache.write();
if let Some(cached) = cache.get(&key) {
return Ok(Arc::clone(cached));
}
let built = match major {
1 => build_scalar_linker_v1(&self.engine, effective_caps)?,
2 => build_scalar_linker_v2(&self.engine, effective_caps)?,
_ => {
return Err(WasmError::AbiUnsupported {
requested: abi.as_str().to_owned(),
supported: SUPPORTED_MAJORS.to_vec(),
});
}
};
let arc = Arc::new(built);
cache.insert(key, Arc::clone(&arc));
Ok(arc)
}
pub fn clear_cache(&self) {
self.cache.write().clear();
}
}
fn caps_signature(caps: &uni_plugin::CapabilitySet) -> String {
serde_json::to_string(caps).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
fn engine() -> Engine {
let mut cfg = wasmtime::Config::new();
cfg.wasm_component_model(true);
Engine::new(&cfg).expect("engine")
}
#[test]
fn linker_for_v1_matches_caret_one() {
let mv = MultiVersionLinker::new(engine());
let abi = AbiRange::parse("^1").unwrap();
let l = mv
.linker_for(&abi, &uni_plugin::CapabilitySet::new())
.expect("v1 selected");
assert!(Arc::strong_count(&l) >= 2, "cache holds an Arc clone");
}
#[test]
fn linker_for_v2_matches_caret_two() {
let mv = MultiVersionLinker::new(engine());
let abi = AbiRange::parse("^2").unwrap();
let _ = mv
.linker_for(&abi, &uni_plugin::CapabilitySet::new())
.expect("v2 selected");
}
#[test]
fn linker_for_rejects_unsupported_major() {
let mv = MultiVersionLinker::new(engine());
let abi = AbiRange::parse("^99").unwrap();
let err = match mv.linker_for(&abi, &uni_plugin::CapabilitySet::new()) {
Ok(_) => panic!("expected AbiUnsupported"),
Err(e) => e,
};
match err {
WasmError::AbiUnsupported {
requested,
supported,
} => {
assert_eq!(requested, "^99");
assert_eq!(supported, vec![1, 2]);
}
other => panic!("expected AbiUnsupported, got {other:?}"),
}
}
#[test]
fn cache_returns_same_arc_on_repeat_lookup() {
let mv = MultiVersionLinker::new(engine());
let abi = AbiRange::parse("^1").unwrap();
let a = mv
.linker_for(&abi, &uni_plugin::CapabilitySet::new())
.unwrap();
let b = mv
.linker_for(&abi, &uni_plugin::CapabilitySet::new())
.unwrap();
assert!(Arc::ptr_eq(&a, &b), "expected cache hit to return same Arc");
}
#[test]
fn caps_signature_is_order_invariant() {
use uni_plugin::{Capability, CapabilitySet};
let a = CapabilitySet::from_iter_of([Capability::ScalarFn, Capability::Procedure]);
let b = CapabilitySet::from_iter_of([Capability::Procedure, Capability::ScalarFn]);
assert_eq!(caps_signature(&a), caps_signature(&b));
let net1 = CapabilitySet::from_iter_of([Capability::Network {
allow: vec!["https://a/**".into()],
}]);
let net2 = CapabilitySet::from_iter_of([Capability::Network {
allow: vec!["https://b/**".into()],
}]);
assert_ne!(
caps_signature(&net1),
caps_signature(&net2),
"different allow-lists must key distinct linkers"
);
}
}