numcodecs_wasm_host/
component.rs

1use std::sync::Arc;
2
3use schemars::Schema;
4use serde::Deserializer;
5use wasm_component_layer::{
6    AsContextMut, ComponentList, ExportInstance, Func, Instance, TypedFunc, Value,
7};
8
9use crate::{
10    codec::WasmCodec,
11    error::RuntimeError,
12    wit::{guest_error_from_wasm, NumcodecsWitInterfaces},
13};
14
15/// WebAssembly component that exports the `numcodecs:abc/codec` interface.
16///
17/// `WasmCodecComponent` does not implement the
18/// [`DynCodecType`][numcodecs::DynCodecType] trait itself so that it can expose
19/// un-opinionated bindings. However, it provides methods with that can be used
20/// to implement the trait on a wrapper.
21pub struct WasmCodecComponent {
22    // precomputed properties
23    pub(crate) codec_id: Arc<str>,
24    pub(crate) codec_config_schema: Arc<Schema>,
25    // wit functions
26    // FIXME: make typed instead
27    pub(crate) from_config: Func,
28    pub(crate) encode: Func,
29    pub(crate) decode: Func,
30    pub(crate) decode_into: Func,
31    pub(crate) get_config: Func,
32    // wasm component instance
33    pub(crate) instance: Instance,
34}
35
36impl WasmCodecComponent {
37    // NOTE: the WasmCodecComponent never calls Instance::drop
38    /// Import the `numcodecs:abc/codec` interface from a WebAssembly component
39    /// `instance`.
40    ///
41    /// The `ctx` must refer to the same store in which the `instance` was
42    /// instantiated.
43    ///
44    /// # Warning
45    /// The `WasmCodecComponent` does *not* own the provided `instance` and
46    /// *never* calls [`Instance::drop`]. It is the responsibility of the code
47    /// creating the `WasmCodecComponent` to destroy the `instance` after the
48    /// component, and all codecs created from it, have been destroyed.
49    ///
50    /// # Errors
51    ///
52    /// Errors if the `instance` does not export the `numcodecs:abc/codec`
53    /// interface or if interacting with the component fails.
54    pub fn new(mut ctx: impl AsContextMut, instance: Instance) -> Result<Self, RuntimeError> {
55        fn load_func(interface: &ExportInstance, name: &str) -> Result<Func, RuntimeError> {
56            let Some(func) = interface.func(name) else {
57                return Err(RuntimeError::from(anyhow::Error::msg(format!(
58                    "WASM component interface does not contain a function named `{name}`"
59                ))));
60            };
61
62            Ok(func)
63        }
64
65        fn load_typed_func<P: ComponentList, R: ComponentList>(
66            interface: &ExportInstance,
67            name: &str,
68        ) -> Result<TypedFunc<P, R>, RuntimeError> {
69            load_func(interface, name)?
70                .typed()
71                .map_err(RuntimeError::from)
72        }
73
74        let interfaces = NumcodecsWitInterfaces::get();
75
76        let Some(codecs_interface) = instance.exports().instance(&interfaces.codec) else {
77            return Err(RuntimeError::from(anyhow::Error::msg(format!(
78                "WASM component does not contain an interface named `{}`",
79                interfaces.codec
80            ))));
81        };
82
83        let codec_id = load_typed_func(codecs_interface, "codec-id")?;
84        let codec_id = codec_id.call(&mut ctx, ())?;
85
86        let codec_config_schema = load_typed_func(codecs_interface, "codec-config-schema")?;
87        let codec_config_schema: Arc<str> = codec_config_schema.call(&mut ctx, ())?;
88        let codec_config_schema: Schema =
89            serde_json::from_str(&codec_config_schema).map_err(anyhow::Error::new)?;
90
91        Ok(Self {
92            codec_id,
93            codec_config_schema: Arc::new(codec_config_schema),
94            from_config: load_func(codecs_interface, "[static]codec.from-config")?,
95            encode: load_func(codecs_interface, "[method]codec.encode")?,
96            decode: load_func(codecs_interface, "[method]codec.decode")?,
97            decode_into: load_func(codecs_interface, "[method]codec.decode-into")?,
98            get_config: load_func(codecs_interface, "[method]codec.get-config")?,
99            instance,
100        })
101    }
102}
103
104/// Methods for implementing the [`DynCodecType`][numcodecs::DynCodecType] trait
105impl WasmCodecComponent {
106    /// Codec identifier.
107    #[must_use]
108    pub fn codec_id(&self) -> &str {
109        &self.codec_id
110    }
111
112    /// JSON schema for the codec's configuration.
113    #[must_use]
114    pub fn codec_config_schema(&self) -> &Schema {
115        &self.codec_config_schema
116    }
117
118    /// Instantiate a codec of this type from a serialized `config`uration.
119    ///
120    /// The `config` must *not* contain an `id` field. If the `config` *may*
121    /// contain one, use the
122    /// [`codec_from_config_with_id`][numcodecs::codec_from_config_with_id]
123    /// helper function.
124    ///
125    /// The `config` *must* be compatible with JSON encoding.
126    ///
127    /// # Errors
128    ///
129    /// Errors if constructing the codec or interacting with the component
130    /// fails.
131    pub fn codec_from_config<'de, D: Deserializer<'de>>(
132        &self,
133        mut ctx: impl AsContextMut,
134        config: D,
135    ) -> Result<WasmCodec, D::Error> {
136        let mut config_bytes = Vec::new();
137        serde_transcode::transcode(config, &mut serde_json::Serializer::new(&mut config_bytes))
138            .map_err(serde::de::Error::custom)?;
139        let config = String::from_utf8(config_bytes).map_err(serde::de::Error::custom)?;
140
141        let args = Value::String(config.into());
142        let mut result = Value::U8(0);
143
144        self.from_config
145            .call(
146                &mut ctx,
147                std::slice::from_ref(&args),
148                std::slice::from_mut(&mut result),
149            )
150            .map_err(serde::de::Error::custom)?;
151
152        let codec = match result {
153            Value::Result(result) => match &*result {
154                Ok(Some(Value::Own(resource))) => WasmCodec {
155                    resource: resource.clone(),
156                    codec_id: self.codec_id.clone(),
157                    codec_config_schema: self.codec_config_schema.clone(),
158                    from_config: self.from_config.clone(),
159                    encode: self.encode.clone(),
160                    decode: self.decode.clone(),
161                    decode_into: self.decode_into.clone(),
162                    get_config: self.get_config.clone(),
163                    instance: self.instance.clone(),
164                },
165                Err(err) => match guest_error_from_wasm(err.as_ref()) {
166                    Ok(err) => return Err(serde::de::Error::custom(err)),
167                    Err(err) => return Err(serde::de::Error::custom(err)),
168                },
169                result => {
170                    return Err(serde::de::Error::custom(format!(
171                        "unexpected from-config result value {result:?}"
172                    )))
173                }
174            },
175            value => {
176                return Err(serde::de::Error::custom(format!(
177                    "unexpected from-config result value {value:?}"
178                )))
179            }
180        };
181
182        Ok(codec)
183    }
184}