Skip to main content

numcodecs_wasm_host_reproducible/
codec.rs

1use 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)]
19/// Errors that can occur when using the [`ReproducibleWasmCodec`]
20pub enum ReproducibleWasmCodecError {
21    /// The codec's lock was poisoned
22    #[error("{codec_id} codec's lock was poisoned")]
23    Poisoned {
24        /// The codec's id
25        codec_id: Arc<str>,
26    },
27    /// The codec's WebAssembly runtime raised an error
28    #[error("{codec_id} codec's WebAssembly runtime raised an error")]
29    Runtime {
30        /// The codec's id
31        codec_id: Arc<str>,
32        /// The runtime error
33        source: RuntimeError,
34    },
35    /// The codec's implementation raised an error
36    #[error("{codec_id} codec's implementation raised an error")]
37    Codec {
38        /// The codec's id
39        codec_id: Arc<str>,
40        /// The codec error
41        source: CodecError,
42    },
43}
44
45/// Codec instantiated inside a WebAssembly component.
46///
47/// The codec is loaded such that its execution is reproducible across any
48/// platform. Importantly, each codec owns its own component instance such that
49/// two codecs cannot interfere.
50pub 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    /// Try cloning the codec by recreating it from its configuration.
66    ///
67    /// `ReproducibleWasmCodec` implements [`Clone`] by calling this method and
68    /// panicking if it fails.
69    ///
70    /// # Errors
71    ///
72    /// Errors if serializing the codec configuration, constructing the new
73    /// codec, or interacting with the component fails.
74    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    /// Try dropping the codec.
87    ///
88    /// `ReproducibleWasmCodec` implements [`Drop`] by calling this method and
89    /// ignoring any errors.
90    // That's not quite true but the effect is the same
91    ///
92    /// # Errors
93    ///
94    /// Errors if dropping the codec's resource or component, or interacting
95    /// with the component fails.
96    pub fn try_drop(mut self) -> Result<(), ReproducibleWasmCodecError> {
97        // keep in sync with drop
98        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    /// Read the codec's instruction counter, which is based on the number of
119    /// WebAssembly bytecode instructions executed.
120    ///
121    /// The instruction counter is never reset and wraps around. Comparisons of
122    /// the counter values before and after, e.g. a call to [`Self::encode`],
123    /// should thus use wrapping arithmetic.
124    ///
125    /// # Errors
126    ///
127    /// Errors if interacting with the component fails.
128    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        // keep in sync with try_drop
165        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
287/// Type object for a codec instantiated inside a WebAssembly component.
288pub 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    /// Load a [`DynCodecType`] from a binary `wasm_component`, which will be
318    /// executed in the provided core WebAssembly `engine`.
319    ///
320    /// # Errors
321    ///
322    /// Errors if the `wasm_component` does not export the `numcodecs:abc/codec`
323    /// interface or if interacting with the component fails.
324    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    /// Load a [`DynCodecType`] from a binary `wasm_component`, which will be
336    /// executed in the provided core WebAssembly `engine` and have access to
337    /// the provided external codec `registry`.
338    ///
339    /// # Errors
340    ///
341    /// Errors if the `wasm_component` does not export the `numcodecs:abc/codec`
342    /// interface or if interacting with the component fails.
343    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}