Skip to main content

surrealism_runtime/
host.rs

1//! InvocationContext trait and WIT host implementation.
2//!
3//! Implementations supply `sql`/`run`/`kv`/stdio per call. Host functions
4//! decode FlatBuffers, call the context, and encode results.
5
6use std::sync::Arc;
7
8use anyhow::{Result, bail};
9use async_trait::async_trait;
10use surrealism_types::err::{PrefixErr, SurrealismResult};
11use wasmtime::StoreContextMut;
12
13use crate::config::SurrealismConfig;
14use crate::kv::KVStore;
15use crate::store::StoreData;
16
17// ============================================================================
18// InvocationContext trait
19// ============================================================================
20
21/// Context provided for each WASM function invocation.
22/// Created per-call with borrowed execution context (stack, query context, etc).
23#[async_trait]
24pub trait InvocationContext: Send + Sync {
25	async fn sql(
26		&mut self,
27		config: &SurrealismConfig,
28		query: String,
29		vars: surrealdb_types::Object,
30	) -> Result<surrealdb_types::Value>;
31	async fn run(
32		&mut self,
33		config: &SurrealismConfig,
34		fnc: String,
35		version: Option<String>,
36		args: Vec<surrealdb_types::Value>,
37	) -> Result<surrealdb_types::Value>;
38
39	fn kv(&mut self) -> Result<&dyn KVStore>;
40
41	fn stdout(&mut self, output: &str) -> Result<()> {
42		print!("{}", output);
43		Ok(())
44	}
45
46	fn stderr(&mut self, output: &str) -> Result<()> {
47		eprint!("{}", output);
48		Ok(())
49	}
50
51	/// Returns a self-contained callback for forwarding WASI stdout output.
52	///
53	/// This is used by the WASI output stream to route guest `println!` / C `printf`
54	/// output through the same path as the WIT `stdout` import. Override this to
55	/// capture structured context (module name, namespace, database, etc.) inside
56	/// the closure so the callback can be invoked independently of `&mut self`.
57	///
58	/// The returned `Arc` is cheap to clone and allows the WASI stream to
59	/// snapshot the callback without holding a lock during invocation.
60	fn stdout_callback(&self) -> Arc<dyn Fn(&str) + Send + Sync> {
61		Arc::new(|output| print!("{}", output))
62	}
63
64	/// Same as [`stdout_callback`](Self::stdout_callback) but for stderr.
65	fn stderr_callback(&self) -> Arc<dyn Fn(&str) + Send + Sync> {
66		Arc::new(|output| eprint!("{}", output))
67	}
68}
69
70// ============================================================================
71// NullContext — placeholder for pooled controllers with no active invocation
72// ============================================================================
73
74pub(crate) struct NullContext;
75
76#[async_trait]
77impl InvocationContext for NullContext {
78	async fn sql(
79		&mut self,
80		_config: &SurrealismConfig,
81		_query: String,
82		_vars: surrealdb_types::Object,
83	) -> Result<surrealdb_types::Value> {
84		bail!("no active invocation context")
85	}
86
87	async fn run(
88		&mut self,
89		_config: &SurrealismConfig,
90		_fnc: String,
91		_version: Option<String>,
92		_args: Vec<surrealdb_types::Value>,
93	) -> Result<surrealdb_types::Value> {
94		bail!("no active invocation context")
95	}
96
97	fn kv(&mut self) -> Result<&dyn KVStore> {
98		bail!("no active invocation context")
99	}
100}
101
102// ============================================================================
103// Helper
104// ============================================================================
105
106fn decode_range_bounds(
107	bytes: &[u8],
108) -> Result<(std::ops::Bound<String>, std::ops::Bound<String>), String> {
109	surrealdb_types::decode_string_range(bytes).map_err(|e| e.to_string())
110}
111
112fn stringify<E: std::fmt::Display>(e: E) -> String {
113	e.to_string()
114}
115
116// ============================================================================
117// Component model host functions (FlatBuffers serialization)
118// ============================================================================
119
120/// Register a host function with the common `func_wrap_async` boilerplate.
121///
122/// All host functions follow the same pattern: receive args from the WASM
123/// component, run an async body that returns `Result<T, String>`, and wrap
124/// the result in `Ok((inner,))` for the component model.
125macro_rules! register_host_fn {
126	($host:ident, $name:literal,
127	 |$store:ident, ($($arg:ident : $ty:ty),* $(,)?)| -> Result<$ret:ty> $body:block
128	) => {
129		$host
130			.func_wrap_async(
131				$name,
132				|mut $store: StoreContextMut<'_, StoreData>,
133				 ($($arg,)*): ($($ty,)*)| {
134					Box::new(async move {
135						let inner: Result<$ret, String> = async $body.await;
136						Ok((inner,))
137					})
138				},
139			)
140			.prefix_err(|| concat!("failed to register ", $name))?;
141	};
142}
143
144pub fn implement_host_functions(
145	linker: &mut wasmtime::component::Linker<StoreData>,
146) -> SurrealismResult<()> {
147	let mut root = linker.root();
148	let mut host =
149		root.instance("surrealism:plugin/host").prefix_err(|| "failed to define host instance")?;
150
151	register_host_fn!(host, "sql",
152		|store, (query: String, vars_bytes: Vec<u8>)| -> Result<Vec<u8>> {
153			let vars_vec = surrealdb_types::decode_string_key_values(&vars_bytes)
154				.map_err(stringify)?;
155			let vars = surrealdb_types::Object::from_iter(vars_vec.into_iter());
156			let config = Arc::clone(&store.data().config);
157			let val = store.data_mut().context.sql(&config, query, vars).await.map_err(stringify)?;
158			surrealdb_types::encode(&val).map_err(stringify)
159		}
160	);
161
162	register_host_fn!(host, "run",
163		|store, (fnc: String, version: Option<String>, args_bytes: Vec<u8>)| -> Result<Vec<u8>> {
164			let args = surrealdb_types::decode_value_list(&args_bytes).map_err(stringify)?;
165			let config = Arc::clone(&store.data().config);
166			let val = store.data_mut().context.run(&config, fnc, version, args).await.map_err(stringify)?;
167			surrealdb_types::encode(&val).map_err(stringify)
168		}
169	);
170
171	register_host_fn!(host, "kv-get",
172		|store, (key: String)| -> Result<Option<Vec<u8>>> {
173			let kv = store.data_mut().context.kv().map_err(stringify)?;
174			match kv.get(key).await.map_err(stringify)? {
175				Some(v) => surrealdb_types::encode(&v).map(Some).map_err(stringify),
176				None => Ok(None),
177			}
178		}
179	);
180
181	register_host_fn!(host, "kv-set",
182		|store, (key: String, value_bytes: Vec<u8>)| -> Result<()> {
183			let value: surrealdb_types::Value =
184				surrealdb_types::decode(&value_bytes).map_err(stringify)?;
185			let kv = store.data_mut().context.kv().map_err(stringify)?;
186			kv.set(key, value).await.map_err(stringify)
187		}
188	);
189
190	register_host_fn!(host, "kv-del",
191		|store, (key: String)| -> Result<()> {
192			let kv = store.data_mut().context.kv().map_err(stringify)?;
193			kv.del(key).await.map_err(stringify)
194		}
195	);
196
197	register_host_fn!(host, "kv-exists",
198		|store, (key: String)| -> Result<bool> {
199			let kv = store.data_mut().context.kv().map_err(stringify)?;
200			kv.exists(key).await.map_err(stringify)
201		}
202	);
203
204	register_host_fn!(host, "kv-del-rng",
205		|store, (range_bytes: Vec<u8>)| -> Result<()> {
206			let (start, end) = decode_range_bounds(&range_bytes)?;
207			let kv = store.data_mut().context.kv().map_err(stringify)?;
208			kv.del_rng(start, end).await.map_err(stringify)
209		}
210	);
211
212	register_host_fn!(host, "kv-get-batch",
213		|store, (keys: Vec<String>)| -> Result<Vec<u8>> {
214			let kv = store.data_mut().context.kv().map_err(stringify)?;
215			let vals = kv.get_batch(keys).await.map_err(stringify)?;
216			surrealdb_types::encode_optional_values(&vals).map_err(stringify)
217		}
218	);
219
220	register_host_fn!(host, "kv-set-batch",
221		|store, (entries_bytes: Vec<u8>)| -> Result<()> {
222			let entries = surrealdb_types::decode_string_key_values(&entries_bytes)
223				.map_err(stringify)?;
224			let kv = store.data_mut().context.kv().map_err(stringify)?;
225			kv.set_batch(entries).await.map_err(stringify)
226		}
227	);
228
229	register_host_fn!(host, "kv-del-batch",
230		|store, (keys: Vec<String>)| -> Result<()> {
231			let kv = store.data_mut().context.kv().map_err(stringify)?;
232			kv.del_batch(keys).await.map_err(stringify)
233		}
234	);
235
236	register_host_fn!(host, "kv-keys",
237		|store, (range_bytes: Vec<u8>)| -> Result<Vec<String>> {
238			let (start, end) = decode_range_bounds(&range_bytes)?;
239			let kv = store.data_mut().context.kv().map_err(stringify)?;
240			kv.keys(start, end).await.map_err(stringify)
241		}
242	);
243
244	register_host_fn!(host, "kv-values",
245		|store, (range_bytes: Vec<u8>)| -> Result<Vec<u8>> {
246			let (start, end) = decode_range_bounds(&range_bytes)?;
247			let kv = store.data_mut().context.kv().map_err(stringify)?;
248			let vals = kv.values(start, end).await.map_err(stringify)?;
249			surrealdb_types::encode_value_list(&vals).map_err(stringify)
250		}
251	);
252
253	register_host_fn!(host, "kv-entries",
254		|store, (range_bytes: Vec<u8>)| -> Result<Vec<u8>> {
255			let (start, end) = decode_range_bounds(&range_bytes)?;
256			let kv = store.data_mut().context.kv().map_err(stringify)?;
257			let entries = kv.entries(start, end).await.map_err(stringify)?;
258			surrealdb_types::encode_string_key_values(&entries).map_err(stringify)
259		}
260	);
261
262	register_host_fn!(host, "kv-count",
263		|store, (range_bytes: Vec<u8>)| -> Result<u64> {
264			let (start, end) = decode_range_bounds(&range_bytes)?;
265			let kv = store.data_mut().context.kv().map_err(stringify)?;
266			kv.count(start, end).await.map_err(stringify)
267		}
268	);
269
270	Ok(())
271}