uni_plugin_wasm/
multi_version.rs1use std::collections::HashMap;
23use std::sync::Arc;
24
25use parking_lot::RwLock;
26use uni_plugin::AbiRange;
27use wasmtime::Engine;
28use wasmtime::component::Linker;
29
30use crate::error::WasmError;
31use crate::host_state::HostState;
32use crate::linker::{build_scalar_linker_v1, build_scalar_linker_v2};
33
34pub const SUPPORTED_MAJORS: &[u64] = &[1, 2];
41
42type CacheKey = (u64, String);
45
46pub(crate) fn major_for_abi(abi: &AbiRange) -> Result<u64, WasmError> {
56 SUPPORTED_MAJORS
57 .iter()
58 .copied()
59 .find(|m| abi.matches(*m))
60 .ok_or_else(|| WasmError::AbiUnsupported {
61 requested: abi.as_str().to_owned(),
62 supported: SUPPORTED_MAJORS.to_vec(),
63 })
64}
65
66pub struct MultiVersionLinker {
71 engine: Engine,
72 cache: RwLock<HashMap<CacheKey, Arc<Linker<HostState>>>>,
73}
74
75impl std::fmt::Debug for MultiVersionLinker {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("MultiVersionLinker")
78 .field("cached_entries", &self.cache.read().len())
79 .finish_non_exhaustive()
80 }
81}
82
83impl MultiVersionLinker {
84 #[must_use]
86 pub fn new(engine: Engine) -> Self {
87 Self {
88 engine,
89 cache: RwLock::new(HashMap::new()),
90 }
91 }
92
93 pub fn linker_for(
106 &self,
107 abi: &AbiRange,
108 effective_caps: &uni_plugin::CapabilitySet,
109 ) -> Result<Arc<Linker<HostState>>, WasmError> {
110 let major = major_for_abi(abi)?;
111 let key: CacheKey = (major, caps_signature(effective_caps));
112 if let Some(cached) = self.cache.read().get(&key) {
113 return Ok(Arc::clone(cached));
114 }
115 let mut cache = self.cache.write();
118 if let Some(cached) = cache.get(&key) {
119 return Ok(Arc::clone(cached));
120 }
121 let built = match major {
122 1 => build_scalar_linker_v1(&self.engine, effective_caps)?,
123 2 => build_scalar_linker_v2(&self.engine, effective_caps)?,
124 _ => {
125 return Err(WasmError::AbiUnsupported {
126 requested: abi.as_str().to_owned(),
127 supported: SUPPORTED_MAJORS.to_vec(),
128 });
129 }
130 };
131 let arc = Arc::new(built);
132 cache.insert(key, Arc::clone(&arc));
133 Ok(arc)
134 }
135
136 pub fn clear_cache(&self) {
139 self.cache.write().clear();
140 }
141}
142
143fn caps_signature(caps: &uni_plugin::CapabilitySet) -> String {
150 serde_json::to_string(caps).unwrap_or_default()
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 fn engine() -> Engine {
158 let mut cfg = wasmtime::Config::new();
159 cfg.wasm_component_model(true);
160 Engine::new(&cfg).expect("engine")
161 }
162
163 #[test]
164 fn linker_for_v1_matches_caret_one() {
165 let mv = MultiVersionLinker::new(engine());
166 let abi = AbiRange::parse("^1").unwrap();
167 let l = mv
168 .linker_for(&abi, &uni_plugin::CapabilitySet::new())
169 .expect("v1 selected");
170 assert!(Arc::strong_count(&l) >= 2, "cache holds an Arc clone");
171 }
172
173 #[test]
174 fn linker_for_v2_matches_caret_two() {
175 let mv = MultiVersionLinker::new(engine());
176 let abi = AbiRange::parse("^2").unwrap();
177 let _ = mv
178 .linker_for(&abi, &uni_plugin::CapabilitySet::new())
179 .expect("v2 selected");
180 }
181
182 #[test]
183 fn linker_for_rejects_unsupported_major() {
184 let mv = MultiVersionLinker::new(engine());
185 let abi = AbiRange::parse("^99").unwrap();
186 let err = match mv.linker_for(&abi, &uni_plugin::CapabilitySet::new()) {
187 Ok(_) => panic!("expected AbiUnsupported"),
188 Err(e) => e,
189 };
190 match err {
191 WasmError::AbiUnsupported {
192 requested,
193 supported,
194 } => {
195 assert_eq!(requested, "^99");
196 assert_eq!(supported, vec![1, 2]);
197 }
198 other => panic!("expected AbiUnsupported, got {other:?}"),
199 }
200 }
201
202 #[test]
203 fn cache_returns_same_arc_on_repeat_lookup() {
204 let mv = MultiVersionLinker::new(engine());
205 let abi = AbiRange::parse("^1").unwrap();
206 let a = mv
207 .linker_for(&abi, &uni_plugin::CapabilitySet::new())
208 .unwrap();
209 let b = mv
210 .linker_for(&abi, &uni_plugin::CapabilitySet::new())
211 .unwrap();
212 assert!(Arc::ptr_eq(&a, &b), "expected cache hit to return same Arc");
213 }
214
215 #[test]
216 fn caps_signature_is_order_invariant() {
217 use uni_plugin::{Capability, CapabilitySet};
218 let a = CapabilitySet::from_iter_of([Capability::ScalarFn, Capability::Procedure]);
221 let b = CapabilitySet::from_iter_of([Capability::Procedure, Capability::ScalarFn]);
222 assert_eq!(caps_signature(&a), caps_signature(&b));
223 let net1 = CapabilitySet::from_iter_of([Capability::Network {
224 allow: vec!["https://a/**".into()],
225 }]);
226 let net2 = CapabilitySet::from_iter_of([Capability::Network {
227 allow: vec!["https://b/**".into()],
228 }]);
229 assert_ne!(
230 caps_signature(&net1),
231 caps_signature(&net2),
232 "different allow-lists must key distinct linkers"
233 );
234 }
235}