Skip to content

Commit 741955a

Browse files
feat: add custom store state
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent 70b4ca7 commit 741955a

11 files changed

Lines changed: 302 additions & 171 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ This release adds garbage collection and typed function reference support, singl
4141

4242
### Added
4343

44+
- Optional store-owned host state through `Store::with_state`, `Store::state`, and `Store::state_mut`.
4445
- Support for the typed function references, garbage collection, exception handling, and compact import section proposals.
4546
- `ResourceLimiter` callbacks for memory and table allocation or growth.
4647
- A default-enabled `validate` Cargo feature and a parser option to skip WebAssembly validation.

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ See the [examples](./examples) directory and [documentation](https://docs.rs/tin
5656
- **`archive`:** Enables serialization and deserialization of the internal `twasm` format. Enabled by default.
5757
- **`log`:** Enables integration with the `log` crate. Enabled by default.
5858
- **`send`:** Makes stores and store-local handles movable across threads.
59+
- **`state`:** Adds store-owned host state for access from host callbacks.
5960
- **`portable-atomic`:** Supports targets without native atomic compare-and-swap.
6061
- **`canonicalize-nans`:** Uses a [canonical NaN](https://en.wikipedia.org/wiki/NaN#Canonical_NaN) for normalized NaN results. Enabled by default.
6162
- **`debug`:** Derives `Debug` for runtime types. Enabled by default.

‎crates/tinywasm/Cargo.toml‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ keywords.workspace = true
1111
categories.workspace = true
1212

1313
[package.metadata.docs.rs]
14-
features = ["std", "validate", "parser", "archive", "log", "canonicalize-nans", "debug", "guest-debug"]
14+
features = ["std", "validate", "parser", "archive", "log", "canonicalize-nans", "state", "debug", "guest-debug"]
1515
rustdoc-args = ["--cfg", "docsrs"]
1616

1717
[lib]
@@ -90,14 +90,17 @@ wat.workspace = true
9090

9191
[features]
9292
default = ["full", "std"]
93-
full = ["archive", "debug", "parallel-parser", "parser", "validate"]
93+
full = ["archive", "debug", "parallel-parser", "parser", "state", "validate"]
9494

9595
log = ["dep:log", "tinywasm-parser?/log"]
9696
std = ["tinywasm-parser?/std", "tinywasm-types/std"]
9797

9898
# make stores, instances, references, and host callbacks movable across threads
9999
send = []
100100

101+
# store host state for access from host callbacks
102+
state = []
103+
101104
# support for parsing WebAssembly
102105
parser = ["dep:tinywasm-parser"]
103106

‎crates/tinywasm/src/store/mod.rs‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
#[cfg(feature = "state")]
2+
use alloc::collections::BTreeMap;
13
use alloc::{boxed::Box, format, vec::Vec};
4+
#[cfg(feature = "state")]
5+
use core::any::{Any, TypeId};
26
use core::hint::cold_path;
37
use tinywasm_types::*;
48

@@ -110,6 +114,25 @@ pub trait ResourceLimiter: Send + Sync {
110114
}
111115
}
112116

117+
/// A value stored as host state.
118+
#[doc(hidden)]
119+
#[cfg(all(feature = "state", not(feature = "send")))]
120+
pub trait StoreState: Any {}
121+
#[cfg(all(feature = "state", not(feature = "send")))]
122+
impl<T: Any> StoreState for T {}
123+
124+
/// A value stored as host state.
125+
#[doc(hidden)]
126+
#[cfg(all(feature = "state", feature = "send"))]
127+
pub trait StoreState: Any + Send {}
128+
#[cfg(all(feature = "state", feature = "send"))]
129+
impl<T: Any + Send> StoreState for T {}
130+
131+
#[cfg(all(feature = "state", not(feature = "send")))]
132+
type StoredState = dyn Any;
133+
#[cfg(all(feature = "state", feature = "send"))]
134+
type StoredState = dyn Any + Send;
135+
113136
/// Runtime state used by WebAssembly instances and host functions.
114137
///
115138
/// ## Example
@@ -133,6 +156,8 @@ pub struct Store {
133156
pub(crate) call_stack: CallStack,
134157
pub(crate) value_stack: ValueStack,
135158
value_scratch: ValueScratch,
159+
#[cfg(feature = "state")]
160+
host_state: BTreeMap<TypeId, Box<StoredState>>,
136161
}
137162

138163
#[derive(Default)]
@@ -238,9 +263,47 @@ impl Store {
238263
engine,
239264
execution_fuel: 0,
240265
execution_active: false,
266+
#[cfg(feature = "state")]
267+
host_state: BTreeMap::new(),
241268
}
242269
}
243270

271+
/// Adds host state to this store.
272+
///
273+
/// Host callbacks can access the value through [`Store::state`] and
274+
/// [`Store::state_mut`]. If state of the same type already exists, this
275+
/// method replaces it.
276+
///
277+
/// # Example
278+
///
279+
/// ```rust
280+
/// use tinywasm::Store;
281+
///
282+
/// struct AppState {
283+
/// requests: usize,
284+
/// }
285+
///
286+
/// let store = Store::default().with_state(AppState { requests: 0 });
287+
/// assert_eq!(store.state::<AppState>().unwrap().requests, 0);
288+
/// ```
289+
#[cfg(feature = "state")]
290+
pub fn with_state<T: StoreState>(mut self, state: T) -> Self {
291+
self.host_state.insert(TypeId::of::<T>(), Box::new(state));
292+
self
293+
}
294+
295+
/// Get host state.
296+
#[cfg(feature = "state")]
297+
pub fn state<T: StoreState>(&self) -> Option<&T> {
298+
self.host_state.get(&TypeId::of::<T>()).and_then(|state| state.downcast_ref())
299+
}
300+
301+
/// Get mutable access to host state.
302+
#[cfg(feature = "state")]
303+
pub fn state_mut<T: StoreState>(&mut self) -> Option<&mut T> {
304+
self.host_state.get_mut(&TypeId::of::<T>()).and_then(|state| state.downcast_mut())
305+
}
306+
244307
pub(crate) const fn id(&self) -> StoreId {
245308
self.id
246309
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#![cfg(feature = "state")]
2+
3+
use tinywasm::types::WasmValue;
4+
use tinywasm::{HostFunction, Store};
5+
6+
struct Counter(i32);
7+
struct Label(&'static str);
8+
9+
#[test]
10+
fn stores_state_by_type() {
11+
let mut store = Store::default().with_state(Counter(1)).with_state(Label("first")).with_state(Counter(2));
12+
13+
assert_eq!(store.state::<Counter>().unwrap().0, 2);
14+
assert_eq!(store.state::<Label>().unwrap().0, "first");
15+
16+
store.state_mut::<Counter>().unwrap().0 = 3;
17+
assert_eq!(store.state::<Counter>().unwrap().0, 3);
18+
}
19+
20+
#[test]
21+
fn host_function_mutates_store_state() -> tinywasm::Result<()> {
22+
let mut store = Store::default().with_state(Counter(40));
23+
let function = HostFunction::from(|mut ctx, value: i32| {
24+
let counter = ctx.state_mut::<Counter>().expect("counter state");
25+
counter.0 += value;
26+
Ok(counter.0)
27+
})
28+
.instantiate(&mut store)?;
29+
30+
let mut results = [WasmValue::I32(0)];
31+
function.call(&mut store, &[WasmValue::I32(2)], &mut results)?;
32+
33+
assert_eq!(results, [WasmValue::I32(42)]);
34+
assert_eq!(store.state::<Counter>().unwrap().0, 42);
35+
Ok(())
36+
}

‎examples/doom/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ softbuffer = "0.4"
1414
tinywasm = { path = "../../crates/tinywasm", features = [
1515
"archive",
1616
"canonicalize-nans",
17+
"state",
1718
"debug",
1819
"log",
1920
"parallel-parser",

‎examples/doom/src/main.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ impl ApplicationHandler for DoomApp {
129129
window.request_redraw();
130130
}
131131

132-
if self.runtime.host_state.lock().unwrap().exit_code.is_some() {
132+
if self.runtime.exited() {
133133
event_loop.exit();
134134
}
135135
}

0 commit comments

Comments
 (0)