Skip to main content

embedded_wg_display/runtime/
mod.rs

1//! Wasmtime WebAssembly Component Model runtime for widget execution.
2//!
3//! Widgets are precompiled WASM components that export a standard WIT interface.
4//! Each widget execution creates a fresh [`Runtime`] instance — no WASM state
5//! persists between runs.
6//!
7//! ## Precompiled Widgets
8//!
9//! [`Runtime::load_module`] expects a **precompiled** WASM component for the xtensa architecture
10//! This is done via the [precompiler](https://github.com/Siryll/wg_display_embedded_precompiler) script.
11//! All of this will be done automaticall when the [widget-template](https://github.com/Siryll/wg_display_embedded_widget_template) is used to create a widget.
12//!
13mod platform;
14
15mod host_api;
16
17pub mod http_sync;
18
19use common::models::WidgetInstallationData;
20use wasmtime::component::{Component, HasSelf, Linker};
21use wasmtime::{Config, Engine, Precompiled, Result, Store};
22
23use alloc::string::{String, ToString};
24
25use crate::runtime::widget::widget::types::Datetime;
26
27use crate::globals;
28
29use hashbrown::HashMap;
30
31use defmt::warn;
32
33// links wit finctions, implementations in host_api
34wasmtime::component::bindgen!({ path: "src/runtime/host_api/wit" });
35
36/// Struct to how potential object states that are passed and useable inside of the host function.
37/// Currently empty.
38pub struct WidgetState {}
39
40impl WidgetState {
41    fn new() -> Self {
42        Self {}
43    }
44}
45
46pub struct Runtime {
47    engine: Engine,
48    linker: Linker<WidgetState>,
49    last_run: HashMap<String, Datetime>,
50}
51
52impl Runtime {
53    /// Creates a new runtime
54    pub fn new() -> Self {
55        defmt::info!("Initializing Wasmtime runtime");
56
57        let mut config = Config::new();
58        config.wasm_component_model(true);
59
60        // disable many optional features: https://github.com/bytecodealliance/wasmtime/blob/main/examples/min-platform/embedding/wasmtime-platform.h
61        config.wasm_bulk_memory(true);
62        config.wasm_simd(false);
63        config.wasm_relaxed_simd(false);
64        config.wasm_multi_memory(false);
65        config.gc_support(false);
66
67        config.signals_based_traps(false);
68        // config.wasm_multi_value(true);
69        config.wasm_multi_value(false);
70        // config.wasm_tail_call(true);
71        config.wasm_tail_call(false);
72
73        config.memory_reservation(0);
74        // config.memory_reservation(0);
75        config.memory_guard_size(0);
76        config.memory_init_cow(false);
77        config.concurrency_support(false);
78
79        let engine = Engine::new(&config).expect("Failed to create Wasmtime engine");
80
81        let mut linker = Linker::<WidgetState>::new(&engine);
82        // Use the HasSelf wrapper type for component model
83        Widget::add_to_linker::<WidgetState, HasSelf<WidgetState>>(
84            &mut linker,
85            |state: &mut WidgetState| state,
86        )
87        .expect("Could not link host API");
88
89        defmt::info!("Wasmtime runtime initialized successfully");
90
91        Self {
92            engine,
93            linker,
94            last_run: HashMap::new(),
95        }
96    }
97
98    /// Deserialises a precompiled Wasmtime component from raw bytes.
99    unsafe fn load_module(&self, bytes: &[u8]) -> Result<Component> {
100        defmt::debug!("Loading precompiled module ({} bytes)", bytes.len());
101
102        match Engine::detect_precompiled(bytes) {
103            Some(Precompiled::Component) => {}
104            Some(Precompiled::Module) => {
105                defmt::error!("Precompiled blob is a core module, but runtime expects a component");
106                return Err(wasmtime::Error::msg("expected precompiled component"));
107            }
108            None => {
109                defmt::error!("Input bytes are not recognized as a Wasmtime precompiled artifact");
110                return Err(wasmtime::Error::msg("invalid precompiled artifact"));
111            }
112        }
113
114        // consideret only safe if compiled on device
115        let component = match unsafe { Component::deserialize(&self.engine, bytes) } {
116            Ok(component) => component,
117            Err(err) => {
118                defmt::error!(
119                    "Failed to deserialize component: {:?}",
120                    defmt::Debug2Format(&err)
121                );
122                return Err(err);
123            }
124        };
125
126        defmt::info!("Module loaded successfully");
127        Ok(component)
128    }
129
130    /// Binds host functions and instantiates a loaded component.
131    /// Requires a mutable store, any created store should only live as long as it is needed and should be destroyed after widget executution to free up memory.
132    fn instantiate(
133        &mut self,
134        component: &Component,
135        store: &mut Store<WidgetState>,
136    ) -> Result<Widget> {
137        defmt::debug!("Instantiating component");
138
139        let widget = match Widget::instantiate(store, component, &self.linker) {
140            Ok(widget) => widget,
141            Err(err) => {
142                defmt::error!(
143                    "Failed to instantiate component: {:?}",
144                    defmt::Debug2Format(&err)
145                );
146                return Err(err);
147            }
148        };
149
150        defmt::info!("Component instantiated successfully");
151        Ok(widget)
152    }
153
154    /// Calls the widget's `run` export with the given JSON config string.
155    ///
156    /// Passes a [`WidgetContext`] containing the last-invocation timestamp and
157    /// the widget's current config. Returns the [`WidgetResult`] containing the
158    /// text to display on screen.
159    ///
160    /// Pass the same store as the one passed to [Self::instantiate], otherwise the execution will fail.
161    fn run(
162        &mut self,
163        widget: &Widget,
164        config: String,
165        store: &mut Store<WidgetState>,
166        name: String,
167    ) -> wasmtime::Result<Option<WidgetResult>> {
168        defmt::info!("Running widget with config: {}", config.as_str());
169        let last_invocation =
170            *self
171                .last_run
172                .get(name.as_str())
173                .unwrap_or(&globals::now().unwrap_or(Datetime {
174                    seconds: 0,
175                    nanoseconds: 0,
176                }));
177
178        let context = WidgetContext {
179            last_invocation,
180            config,
181        };
182
183        let result = match widget.call_run(store, &context) {
184            Ok(result) => result,
185            Err(err) => {
186                defmt::error!("Failed to run widget: {:?}", defmt::Debug2Format(&err));
187                return Err(err);
188            }
189        };
190
191        self.last_run.insert(
192            name,
193            globals::now().unwrap_or(Datetime {
194                seconds: 0,
195                nanoseconds: 0,
196            }),
197        );
198
199        defmt::info!("Widget ran successfully result: {}", result.data.as_str());
200        Ok(Some(result))
201    }
202
203    /// Returns the widget's display name (calls `get-name` WIT export).
204    fn get_widget_name(
205        &mut self,
206        widget: &Widget,
207        store: &mut Store<WidgetState>,
208    ) -> wasmtime::Result<String> {
209        widget.call_get_name(store)
210    }
211
212    /// Returns the widget's JSON Schema config string (calls `get-config-schema` WIT export).
213    fn get_config_schema(
214        &mut self,
215        widget: &Widget,
216        store: &mut Store<WidgetState>,
217    ) -> wasmtime::Result<String> {
218        widget.call_get_config_schema(store)
219    }
220
221    /// Returns the widget's semver version string (calls `get-version` WIT export).
222    fn get_widget_version(
223        &mut self,
224        widget: &Widget,
225        store: &mut Store<WidgetState>,
226    ) -> wasmtime::Result<String> {
227        widget.call_get_version(store)
228    }
229
230    /// Returns how often the widget should be run in seconds (calls `get-run-update-cycle-seconds`).
231    fn get_run_update_cycle_seconds(
232        &mut self,
233        widget: &Widget,
234        store: &mut Store<WidgetState>,
235    ) -> wasmtime::Result<u32> {
236        widget.call_get_run_update_cycle_seconds(store)
237    }
238
239    /// Wrapper function for running a widget by name with given json config
240    pub async unsafe fn run_widget(
241        &mut self,
242        widget_name: String,
243        config: String,
244    ) -> wasmtime::Result<Option<WidgetResult>> {
245        let mut store = Store::new(&self.engine, WidgetState::new());
246
247        let wasm_bytes = match globals::with_storage(|s| s.wasm_read(&widget_name)).await {
248            Ok(bytes) => bytes,
249            Err(err) => {
250                warn!(
251                    "Could not read widget '{}': {:?}",
252                    widget_name.as_str(),
253                    defmt::Debug2Format(&err)
254                );
255                return Err(wasmtime::Error::msg("Widget binary missing"));
256            }
257        };
258
259        let component = unsafe { self.load_module(&wasm_bytes)? };
260        let instance = self.instantiate(&component, &mut store)?;
261        self.run(&instance, config, &mut store, widget_name)
262    }
263
264    /// wrapper function to get all widget metadata with the same store
265    ///
266    /// Sets the [WidgetInstallationData::json_config] to `{}`, until the widget gets configured via the UI.
267    pub async unsafe fn get_widget_metadata(
268        &mut self,
269        bytes: &[u8],
270    ) -> wasmtime::Result<WidgetInstallationData> {
271        let mut store = Store::new(&self.engine, WidgetState::new());
272        let component = unsafe { self.load_module(bytes)? };
273        let instance = self.instantiate(&component, &mut store)?;
274        let name = self.get_widget_name(&instance, &mut store)?;
275        let json_config_schema = self.get_config_schema(&instance, &mut store)?;
276        let version = self.get_widget_version(&instance, &mut store)?;
277        let update_cycle_seconds = self.get_run_update_cycle_seconds(&instance, &mut store)?;
278
279        Ok(WidgetInstallationData {
280            name,
281            description: String::new(), // description is not currently stored in the component, could be added as a custom section if needed
282            version,
283            json_config: "{}".to_string(),
284            json_config_schema,
285            update_cycle_seconds,
286        })
287    }
288}