Skip to main content

surrealism_runtime/
controller.rs

1//! WASM execution runtime and controller.
2//!
3//! # Architecture
4//!
5//! - **`Runtime`**: Compiled WASM module. Thread-safe, shareable (Arc<Runtime>). Compile once,
6//!   instantiate many times.
7//!
8//! - **`Controller`**: Per-execution instance. Single-threaded, created from Runtime. Cheap to
9//!   create, can be done per-request or pooled.
10//!
11//! # Concurrency Patterns
12//!
13//! ```no_run
14//! use std::sync::Arc;
15//! use surrealism_runtime::{controller::Runtime, package::SurrealismPackage};
16//!
17//! // Compile once (expensive)
18//! let runtime = Arc::new(Runtime::new(package)?);
19//!
20//! // For each concurrent request:
21//! let runtime = runtime.clone();
22//! tokio::spawn(async move {
23//!     let context = Box::new(MyContext::new());
24//!     let mut controller = runtime.new_controller(context).await?;
25//!     controller.invoke(None, args).await
26//! });
27//! # Ok::<(), anyhow::Error>(())
28//! ```
29
30use std::fmt;
31use std::sync::Arc;
32
33use anyhow::Result;
34use async_trait::async_trait;
35use surrealism_types::args::Args;
36use surrealism_types::err::PrefixError;
37use surrealism_types::transfer::AsyncTransfer;
38use wasmtime::*;
39use wasmtime_wasi::p1::{self, WasiP1Ctx};
40
41use crate::config::SurrealismConfig;
42use crate::host::{InvocationContext, implement_host_functions};
43use crate::package::SurrealismPackage;
44
45/// Store data for WASM execution. Each Controller has its own isolated StoreData.
46pub struct StoreData {
47	pub wasi: WasiP1Ctx,
48	pub config: Arc<SurrealismConfig>,
49	pub(crate) context: Box<dyn InvocationContext>,
50}
51
52impl fmt::Debug for StoreData {
53	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54		write!(f, "StoreData {{ wasi: ?, context: ?, config: {:?} }}", self.config)?;
55		Ok(())
56	}
57}
58
59/// Compiled WASM runtime. Thread-safe, can be shared across threads via Arc.
60/// Compiles WASM once, then each controller gets its own isolated Store/Instance.
61/// The Engine, Module, and Linker are immutable and safely shared.
62#[derive(Debug)]
63pub struct Runtime {
64	engine: Engine,
65	module: Module,
66	linker: Linker<StoreData>,
67	config: Arc<SurrealismConfig>,
68}
69
70impl Runtime {
71	/// Compile the WASM module and prepare the runtime.
72	/// This is expensive - do it once and share via Arc<Runtime>.
73	/// The compiled artifacts (Engine, Module, Linker) are immutable and thread-safe.
74	pub fn new(
75		SurrealismPackage {
76			wasm,
77			config,
78		}: SurrealismPackage,
79	) -> Result<Self> {
80		// Configure engine for fast compilation in debug, optimized runtime in release
81		let mut engine_config = Config::new();
82		// Enable async support for async host functions
83		engine_config.async_support(true);
84		#[cfg(debug_assertions)]
85		{
86			// Use Winch baseline compiler for extremely fast compilation in debug builds
87			// Falls back to Cranelift if Winch doesn't support the WASM features used
88			engine_config.strategy(Strategy::Winch);
89		}
90		#[cfg(not(debug_assertions))]
91		{
92			// Optimize for runtime performance in release builds
93			engine_config.cranelift_opt_level(OptLevel::Speed);
94		}
95		let engine = Engine::new(&engine_config)?;
96		let module =
97			Module::new(&engine, wasm).prefix_err(|| "Failed to construct module from bytes")?;
98
99		let mut linker: Linker<StoreData> = Linker::new(&engine);
100		p1::add_to_linker_async(&mut linker, |data| &mut data.wasi)
101			.prefix_err(|| "failed to add WASI to linker")?;
102		implement_host_functions(&mut linker)
103			.prefix_err(|| "failed to implement host functions")?;
104
105		Ok(Self {
106			engine,
107			module,
108			linker,
109			config: Arc::new(config),
110		})
111	}
112
113	/// Create a new Controller with its own isolated Store and Instance.
114	/// This is cheap (relative to compilation) - the expensive compilation is shared.
115	/// Each controller has its own mutable Store, ensuring no shared mutable state.
116	/// Safe for concurrent execution: no mutable state is shared between controllers.
117	pub async fn new_controller(&self, context: Box<dyn InvocationContext>) -> Result<Controller> {
118		let wasi_ctx = super::wasi_context::build()?;
119
120		let store_data = StoreData {
121			wasi: wasi_ctx,
122			config: self.config.clone(),
123			context,
124		};
125		let mut store = Store::new(&self.engine, store_data);
126		let instance = self
127			.linker
128			.instantiate_async(&mut store, &self.module)
129			.await
130			.prefix_err(|| "failed to instantiate WASM module")?;
131		let memory = instance
132			.get_memory(&mut store, "memory")
133			.prefix_err(|| "WASM module must export 'memory'")?;
134
135		Ok(Controller {
136			store,
137			instance,
138			memory,
139		})
140	}
141}
142
143/// Per-execution controller. Not thread-safe - create one per concurrent call.
144/// Lightweight, created from Runtime. Each controller has its own isolated Store and Instance.
145#[derive(Debug)]
146pub struct Controller {
147	pub(super) store: Store<StoreData>,
148	pub(super) instance: Instance,
149	pub(super) memory: Memory,
150}
151
152impl Controller {
153	pub async fn alloc(&mut self, len: u32) -> Result<u32> {
154		let alloc = self.instance.get_typed_func::<(u32,), i32>(&mut self.store, "__sr_alloc")?;
155		let result = alloc.call_async(&mut self.store, (len,)).await?;
156		if result == -1 {
157			anyhow::bail!("Memory allocation failed");
158		}
159		Ok(result as u32)
160	}
161
162	pub async fn free(&mut self, ptr: u32, len: u32) -> Result<()> {
163		let free = self.instance.get_typed_func::<(u32, u32), i32>(&mut self.store, "__sr_free")?;
164		let result = free.call_async(&mut self.store, (ptr, len)).await?;
165		if result == -1 {
166			anyhow::bail!("Memory deallocation failed");
167		}
168		Ok(())
169	}
170
171	pub async fn init(&mut self) -> Result<()> {
172		let init: Option<Extern> = self.instance.get_export(&mut self.store, "__sr_init");
173		if init.is_none() {
174			return Ok(());
175		}
176
177		let init = self.instance.get_typed_func::<(), ()>(&mut self.store, "__sr_init")?;
178		init.call_async(&mut self.store, ()).await
179	}
180
181	pub async fn invoke<A: Args>(
182		&mut self,
183		name: Option<String>,
184		args: A,
185	) -> Result<surrealdb_types::Value> {
186		let name = format!("__sr_fnc__{}", name.unwrap_or_default());
187		let args = AsyncTransfer::transfer(args.to_values(), self).await?;
188		let invoke = self.instance.get_typed_func::<(u32,), (i32,)>(&mut self.store, &name)?;
189		let (ptr,) = invoke.call_async(&mut self.store, (*args,)).await?;
190		if ptr == -1 {
191			anyhow::bail!("WASM function returned error (-1)");
192		}
193		let ptr_u32: u32 = ptr.try_into()?;
194		let result: Result<surrealdb_types::Value, String> =
195			AsyncTransfer::receive(ptr_u32.into(), self).await?;
196		result.map_err(|e| anyhow::anyhow!("WASM function returned error: {}", e))
197	}
198
199	pub async fn args(&mut self, name: Option<String>) -> Result<Vec<surrealdb_types::Kind>> {
200		let name = format!("__sr_args__{}", name.unwrap_or_default());
201		let args = self.instance.get_typed_func::<(), (i32,)>(&mut self.store, &name)?;
202		let (ptr,) = args.call_async(&mut self.store, ()).await?;
203		AsyncTransfer::receive(ptr.try_into()?, self).await
204	}
205
206	pub async fn returns(&mut self, name: Option<String>) -> Result<surrealdb_types::Kind> {
207		let name = format!("__sr_returns__{}", name.unwrap_or_default());
208		let returns = self.instance.get_typed_func::<(), (i32,)>(&mut self.store, &name)?;
209		let (ptr,) = returns.call_async(&mut self.store, ()).await?;
210		if ptr == -1 {
211			anyhow::bail!("WASM function returned error (-1)");
212		}
213		AsyncTransfer::receive(ptr.try_into()?, self).await
214	}
215
216	pub fn list(&mut self) -> Result<Vec<String>> {
217		// scan the exported functions and return a list of available functions
218		let mut functions = Vec::new();
219
220		// First, collect all export names that start with __sr_fnc__
221		let function_names: Vec<String> = {
222			let exports = self.instance.exports(&mut self.store);
223			exports
224				.filter_map(|export| {
225					let name = export.name();
226					if name.starts_with("__sr_fnc__") {
227						Some(name.to_string())
228					} else {
229						None
230					}
231				})
232				.collect()
233		};
234
235		// Then check each one to see if it's actually a function
236		for name in function_names {
237			if let Some(export) = self.instance.get_export(&mut self.store, &name)
238				&& let ExternType::Func(_) = export.ty(&self.store)
239			{
240				// strip the prefix
241				let function_name = name.strip_prefix("__sr_fnc__").unwrap_or(&name).to_string();
242				functions.push(function_name);
243			}
244		}
245
246		Ok(functions)
247	}
248}
249
250#[async_trait]
251impl surrealism_types::controller::AsyncMemoryController for Controller {
252	async fn alloc(&mut self, len: u32) -> Result<u32> {
253		Controller::alloc(self, len).await
254	}
255
256	async fn free(&mut self, ptr: u32, len: u32) -> Result<()> {
257		Controller::free(self, ptr, len).await
258	}
259
260	fn mut_mem(&mut self, ptr: u32, len: u32) -> Result<&mut [u8]> {
261		let mem = self.memory.data_mut(&mut self.store);
262		let start = ptr as usize;
263		let end = start
264			.checked_add(len as usize)
265			.ok_or_else(|| anyhow::anyhow!("Memory access overflow: ptr={ptr}, len={len}"))?;
266
267		if end > mem.len() {
268			anyhow::bail!(
269				"Memory access out of bounds: attempting to access [{start}..{end}), but memory size is {}",
270				mem.len()
271			);
272		}
273
274		Ok(&mut mem[start..end])
275	}
276}