numcodecs_wasm_host_reproducible/
codec.rs1use std::num::Wrapping;
2use std::sync::{Arc, Mutex};
3
4use numcodecs::{
5 AnyArray, AnyArrayView, AnyArrayViewMut, AnyCowArray, Codec, DynCodec, DynCodecType,
6};
7use numcodecs_registry::{EmptyRegistry, Registry};
8use numcodecs_wasm_host::{CodecError, RuntimeError, WasmCodec, WasmCodecComponent};
9use schemars::Schema;
10use serde::Serializer;
11use wasm_component_layer::{AsContextMut, Component, Instance, Linker, Store, TypedFunc};
12use wasm_runtime_layer::{Engine, backend::WasmEngine};
13
14use crate::transform::instcnt::PerfWitInterfaces;
15use crate::transform::transform_wasm_component;
16use crate::{engine::ReproducibleEngine, logging, stdio};
17
18#[derive(Debug, thiserror::Error)]
19pub enum ReproducibleWasmCodecError {
21 #[error("{codec_id} codec's lock was poisoned")]
23 Poisoned {
24 codec_id: Arc<str>,
26 },
27 #[error("{codec_id} codec's WebAssembly runtime raised an error")]
29 Runtime {
30 codec_id: Arc<str>,
32 source: RuntimeError,
34 },
35 #[error("{codec_id} codec's implementation raised an error")]
37 Codec {
38 codec_id: Arc<str>,
40 source: CodecError,
42 },
43}
44
45pub struct ReproducibleWasmCodec<E: WasmEngine>
51where
52 Store<(), ReproducibleEngine<E>>: Send,
53{
54 store: Mutex<Store<(), ReproducibleEngine<E>>>,
55 instance: Instance,
56 codec: WasmCodec,
57 ty: ReproducibleWasmCodecType<E>,
58 instruction_counter: TypedFunc<(), u64>,
59}
60
61impl<E: WasmEngine> ReproducibleWasmCodec<E>
62where
63 Store<(), ReproducibleEngine<E>>: Send,
64{
65 pub fn try_clone(&self) -> Result<Self, serde_json::Error> {
75 let mut config = self.get_config(serde_json::value::Serializer)?;
76
77 if let Some(config) = config.as_object_mut() {
78 config.remove("id");
79 }
80
81 let codec: Self = self.ty.codec_from_config(config)?;
82
83 Ok(codec)
84 }
85
86 pub fn try_drop(mut self) -> Result<(), ReproducibleWasmCodecError> {
97 let mut store = self
99 .store
100 .get_mut()
101 .map_err(|_| ReproducibleWasmCodecError::Poisoned {
102 codec_id: self.ty.codec_id.clone(),
103 })?;
104
105 let result =
106 self.codec
107 .try_drop(&mut store)
108 .map_err(|source| ReproducibleWasmCodecError::Runtime {
109 codec_id: self.ty.codec_id.clone(),
110 source,
111 });
112 let results = try_drop_instance(store, &self.instance, &self.ty.codec_id);
113
114 result.and(results)
115 }
116
117 #[expect(clippy::significant_drop_tightening)]
118 pub fn instruction_counter(&self) -> Result<Wrapping<u64>, ReproducibleWasmCodecError> {
129 let mut store = self
130 .store
131 .lock()
132 .map_err(|_| ReproducibleWasmCodecError::Poisoned {
133 codec_id: self.ty.codec_id.clone(),
134 })?;
135
136 let cnt = self
137 .instruction_counter
138 .call(store.as_context_mut(), ())
139 .map_err(|err| ReproducibleWasmCodecError::Runtime {
140 codec_id: self.ty.codec_id.clone(),
141 source: RuntimeError::from(err),
142 })?;
143
144 Ok(Wrapping(cnt))
145 }
146}
147
148impl<E: WasmEngine> Clone for ReproducibleWasmCodec<E>
149where
150 Store<(), ReproducibleEngine<E>>: Send,
151{
152 fn clone(&self) -> Self {
153 #[expect(clippy::expect_used)]
154 self.try_clone()
155 .expect("cloning a wasm codec should not fail")
156 }
157}
158
159impl<E: WasmEngine> Drop for ReproducibleWasmCodec<E>
160where
161 Store<(), ReproducibleEngine<E>>: Send,
162{
163 fn drop(&mut self) {
164 let Ok(mut store) = self.store.get_mut() else {
166 return;
167 };
168
169 let result = self.codec.try_drop(&mut store);
170 std::mem::drop(result);
171
172 let results = self.instance.drop(store);
173 std::mem::drop(results);
174 }
175}
176
177impl<E: WasmEngine> Codec for ReproducibleWasmCodec<E>
178where
179 Store<(), ReproducibleEngine<E>>: Send,
180{
181 type Error = ReproducibleWasmCodecError;
182
183 #[expect(clippy::significant_drop_tightening)]
184 fn encode(&self, data: AnyCowArray) -> Result<AnyArray, Self::Error> {
185 let mut store = self
186 .store
187 .lock()
188 .map_err(|_| ReproducibleWasmCodecError::Poisoned {
189 codec_id: self.ty.codec_id.clone(),
190 })?;
191
192 let encoded = self
193 .codec
194 .encode(store.as_context_mut(), data)
195 .map_err(|err| ReproducibleWasmCodecError::Runtime {
196 codec_id: self.ty.codec_id.clone(),
197 source: err,
198 })?
199 .map_err(|err| ReproducibleWasmCodecError::Codec {
200 codec_id: self.ty.codec_id.clone(),
201 source: err,
202 })?;
203
204 Ok(encoded)
205 }
206
207 #[expect(clippy::significant_drop_tightening)]
208 fn decode(&self, encoded: AnyCowArray) -> Result<AnyArray, Self::Error> {
209 let mut store = self
210 .store
211 .lock()
212 .map_err(|_| ReproducibleWasmCodecError::Poisoned {
213 codec_id: self.ty.codec_id.clone(),
214 })?;
215
216 let decoded = self
217 .codec
218 .decode(store.as_context_mut(), encoded)
219 .map_err(|err| ReproducibleWasmCodecError::Runtime {
220 codec_id: self.ty.codec_id.clone(),
221 source: err,
222 })?
223 .map_err(|err| ReproducibleWasmCodecError::Codec {
224 codec_id: self.ty.codec_id.clone(),
225 source: err,
226 })?;
227
228 Ok(decoded)
229 }
230
231 #[expect(clippy::significant_drop_tightening)]
232 fn decode_into(
233 &self,
234 encoded: AnyArrayView,
235 decoded: AnyArrayViewMut,
236 ) -> Result<(), Self::Error> {
237 let mut store = self
238 .store
239 .lock()
240 .map_err(|_| ReproducibleWasmCodecError::Poisoned {
241 codec_id: self.ty.codec_id.clone(),
242 })?;
243
244 self.codec
245 .decode_into(store.as_context_mut(), encoded, decoded)
246 .map_err(|err| ReproducibleWasmCodecError::Runtime {
247 codec_id: self.ty.codec_id.clone(),
248 source: err,
249 })?
250 .map_err(|err| ReproducibleWasmCodecError::Codec {
251 codec_id: self.ty.codec_id.clone(),
252 source: err,
253 })?;
254
255 Ok(())
256 }
257}
258
259impl<E: WasmEngine> DynCodec for ReproducibleWasmCodec<E>
260where
261 Store<(), ReproducibleEngine<E>>: Send,
262{
263 type Type = ReproducibleWasmCodecType<E>;
264
265 fn ty(&self) -> Self::Type {
266 ReproducibleWasmCodecType {
267 codec_id: self.ty.codec_id.clone(),
268 codec_config_schema: self.ty.codec_config_schema.clone(),
269 component: self.ty.component.clone(),
270 component_instantiater: self.ty.component_instantiater.clone(),
271 }
272 }
273
274 fn get_config<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
275 let mut store = self
276 .store
277 .lock()
278 .map_err(|_| ReproducibleWasmCodecError::Poisoned {
279 codec_id: self.ty.codec_id.clone(),
280 })
281 .map_err(serde::ser::Error::custom)?;
282
283 self.codec.get_config(store.as_context_mut(), serializer)
284 }
285}
286
287pub struct ReproducibleWasmCodecType<E: WasmEngine>
289where
290 Store<(), ReproducibleEngine<E>>: Send,
291{
292 pub(super) codec_id: Arc<str>,
293 pub(super) codec_config_schema: Arc<Schema>,
294 pub(super) component: Component,
295 #[expect(clippy::type_complexity)]
296 pub(super) component_instantiater: Arc<
297 dyn Send
298 + Sync
299 + Fn(
300 &Component,
301 &str,
302 ) -> Result<
303 (
304 Store<(), ReproducibleEngine<E>>,
305 Instance,
306 WasmCodecComponent,
307 ),
308 ReproducibleWasmCodecError,
309 >,
310 >,
311}
312
313impl<E: WasmEngine> ReproducibleWasmCodecType<E>
314where
315 Store<(), ReproducibleEngine<E>>: Send,
316{
317 pub fn new(
325 engine: E,
326 wasm_component: impl Into<Vec<u8>>,
327 ) -> Result<Self, ReproducibleWasmCodecError>
328 where
329 E: Send + Sync,
330 Store<(), ReproducibleEngine<E>>: Send + Sync,
331 {
332 Self::new_with_registry(engine, wasm_component, EmptyRegistry)
333 }
334
335 pub fn new_with_registry(
344 engine: E,
345 wasm_component: impl Into<Vec<u8>>,
346 registry: impl Registry,
347 ) -> Result<Self, ReproducibleWasmCodecError>
348 where
349 E: Send + Sync,
350 Store<(), ReproducibleEngine<E>>: Send + Sync,
351 {
352 let wasm_component = transform_wasm_component(wasm_component).map_err(|err| {
353 ReproducibleWasmCodecError::Runtime {
354 codec_id: Arc::from("<unknown>"),
355 source: RuntimeError::from(err),
356 }
357 })?;
358
359 let engine = Engine::new(ReproducibleEngine::new(engine));
360 let component = Component::new(&engine, &wasm_component).map_err(|err| {
361 ReproducibleWasmCodecError::Runtime {
362 codec_id: Arc::from("<unknown>"),
363 source: RuntimeError::from(err),
364 }
365 })?;
366
367 let registry = Arc::new(registry);
368
369 let component_instantiater = Arc::new(move |component: &Component, codec_id: &str| {
370 let mut store = Store::new(&engine, ());
371
372 let mut linker = Linker::default();
373 stdio::add_to_linker(&mut linker, &mut store).map_err(|err| {
374 ReproducibleWasmCodecError::Runtime {
375 codec_id: Arc::from(codec_id),
376 source: RuntimeError::from(err),
377 }
378 })?;
379 logging::add_to_linker(&mut linker, &mut store).map_err(|err| {
380 ReproducibleWasmCodecError::Runtime {
381 codec_id: Arc::from(codec_id),
382 source: RuntimeError::from(err),
383 }
384 })?;
385 numcodecs_wasm_host::add_registry_to_linker(&mut linker, &mut store, registry.clone())
386 .map_err(|err| ReproducibleWasmCodecError::Runtime {
387 codec_id: Arc::from(codec_id),
388 source: RuntimeError::from(err),
389 })?;
390
391 let instance = linker.instantiate(&mut store, component).map_err(|err| {
392 ReproducibleWasmCodecError::Runtime {
393 codec_id: Arc::from(codec_id),
394 source: RuntimeError::from(err),
395 }
396 })?;
397
398 let component =
399 WasmCodecComponent::new(&mut store, instance.clone()).map_err(|source| {
400 ReproducibleWasmCodecError::Runtime {
401 codec_id: Arc::from(codec_id),
402 source,
403 }
404 })?;
405
406 Ok((store, instance, component))
407 });
408
409 let (codec_id, codec_config_schema) = {
410 let (mut store, instance, ty): (_, _, WasmCodecComponent) =
411 (component_instantiater)(&component, "<unknown>")?;
412
413 let codec_id = Arc::from(ty.codec_id());
414 let codec_config_schema = Arc::from(ty.codec_config_schema().clone());
415
416 try_drop_instance(&mut store, &instance, &codec_id)?;
417
418 (codec_id, codec_config_schema)
419 };
420
421 Ok(Self {
422 codec_id,
423 codec_config_schema,
424 component,
425 component_instantiater,
426 })
427 }
428}
429
430impl<E: WasmEngine> DynCodecType for ReproducibleWasmCodecType<E>
431where
432 Store<(), ReproducibleEngine<E>>: Send,
433{
434 type Codec = ReproducibleWasmCodec<E>;
435
436 fn codec_id(&self) -> &str {
437 &self.codec_id
438 }
439
440 fn codec_from_config<'de, D: serde::Deserializer<'de>>(
441 &self,
442 config: D,
443 ) -> Result<Self::Codec, D::Error> {
444 let (mut store, instance, component) =
445 (self.component_instantiater)(&self.component, &self.codec_id)
446 .map_err(serde::de::Error::custom)?;
447 let codec = component.codec_from_config(store.as_context_mut(), config)?;
448
449 let PerfWitInterfaces {
450 perf: perf_interface,
451 instruction_counter,
452 } = PerfWitInterfaces::get();
453 let Some(perf_interface) = instance.exports().instance(perf_interface) else {
454 return Err(serde::de::Error::custom(
455 "WASM component does not contain an interface to read the instruction counter",
456 ));
457 };
458 let Some(instruction_counter) = perf_interface.func(instruction_counter) else {
459 return Err(serde::de::Error::custom(
460 "WASM component interface does not contain a function to read the instruction counter",
461 ));
462 };
463 let instruction_counter = instruction_counter.typed().map_err(|err| {
464 serde::de::Error::custom(format!(
465 "WASM component instruction counter function has the wrong signature: {err}"
466 ))
467 })?;
468
469 Ok(ReproducibleWasmCodec {
470 store: Mutex::new(store),
471 instance,
472 codec,
473 ty: Self {
474 codec_id: self.codec_id.clone(),
475 codec_config_schema: self.codec_config_schema.clone(),
476 component: self.component.clone(),
477 component_instantiater: self.component_instantiater.clone(),
478 },
479 instruction_counter,
480 })
481 }
482
483 fn codec_config_schema(&self) -> Schema {
484 (*self.codec_config_schema).clone()
485 }
486}
487
488fn try_drop_instance<T, E: WasmEngine>(
489 store: &mut Store<T, E>,
490 instance: &Instance,
491 codec_id: &str,
492) -> Result<(), ReproducibleWasmCodecError> {
493 let mut errors = instance
494 .drop(store)
495 .map_err(|err| ReproducibleWasmCodecError::Runtime {
496 codec_id: Arc::from(codec_id),
497 source: RuntimeError::from(err),
498 })?;
499
500 let Some(mut err) = errors.pop() else {
501 return Ok(());
502 };
503
504 if !errors.is_empty() {
505 err = err.context(format!("showing one of {} errors", errors.len() + 1));
506 }
507
508 Err(ReproducibleWasmCodecError::Runtime {
509 codec_id: Arc::from(codec_id),
510 source: RuntimeError::from(
511 err.context("dropping instance and all of its resources failed"),
512 ),
513 })
514}