Skip to main content

reifydb_engine/procedure/
wasm.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (c) 2025 ReifyDB
3
4//! WASM procedure implementation that executes WebAssembly modules as stored procedures
5
6use postcard::to_stdvec;
7use reifydb_core::value::column::columns::Columns;
8use reifydb_sdk::{error::FFIError, marshal::wasm::unmarshal_columns_from_bytes};
9use reifydb_transaction::transaction::Transaction;
10use reifydb_type::Result;
11use reifydb_wasm::{Engine, SpawnBinary, module::value::Value, source};
12
13use super::{Procedure, context::ProcedureContext};
14
15/// WASM procedure that loads and executes a `.wasm` module.
16///
17/// Each WASM module must export:
18/// - `alloc(size: i32) -> i32` — allocate `size` bytes, return pointer
19/// - `dealloc(ptr: i32, size: i32)` — free memory
20/// - `procedure(params_ptr: i32, params_len: i32) -> i32` — pointer to output (first 4 bytes at output pointer = output
21///   length as LE u32)
22pub struct WasmProcedure {
23	name: String,
24	wasm_bytes: Vec<u8>,
25}
26
27impl WasmProcedure {
28	pub fn new(name: impl Into<String>, wasm_bytes: Vec<u8>) -> Self {
29		Self {
30			name: name.into(),
31			wasm_bytes,
32		}
33	}
34
35	pub fn name(&self) -> &str {
36		&self.name
37	}
38}
39
40// SAFETY: WasmProcedure only holds inert data (name + bytes).
41// A fresh Engine is created per invocation, so no shared mutable state.
42unsafe impl Send for WasmProcedure {}
43unsafe impl Sync for WasmProcedure {}
44
45impl Procedure for WasmProcedure {
46	fn call(&self, ctx: &ProcedureContext, _tx: &mut Transaction<'_>) -> Result<Columns> {
47		let params_bytes = to_stdvec(ctx.params).map_err(|e| {
48			FFIError::Other(format!("WASM procedure '{}' failed to serialize params: {}", self.name, e))
49		})?;
50
51		let mut engine = Engine::default();
52		engine.spawn(source::binary::bytes(&self.wasm_bytes)).map_err(|e| {
53			FFIError::Other(format!("WASM procedure '{}' failed to load: {:?}", self.name, e))
54		})?;
55
56		// Allocate space in WASM linear memory
57		let alloc_result = engine.invoke("alloc", &[Value::I32(params_bytes.len() as i32)]).map_err(|e| {
58			FFIError::Other(format!("WASM procedure '{}' alloc failed: {:?}", self.name, e))
59		})?;
60
61		let params_ptr = match alloc_result.first() {
62			Some(Value::I32(v)) => *v,
63			_ => {
64				return Err(FFIError::Other(format!(
65					"WASM procedure '{}': alloc returned unexpected result",
66					self.name
67				))
68				.into());
69			}
70		};
71
72		// Write params data into WASM memory
73		engine.write_memory(params_ptr as usize, &params_bytes).map_err(|e| {
74			FFIError::Other(format!("WASM procedure '{}' write_memory failed: {:?}", self.name, e))
75		})?;
76
77		// Call procedure
78		let result = engine
79			.invoke("procedure", &[Value::I32(params_ptr), Value::I32(params_bytes.len() as i32)])
80			.map_err(|e| {
81				FFIError::Other(format!(
82					"WASM procedure '{}' procedure call failed: {:?}",
83					self.name, e
84				))
85			})?;
86
87		let output_ptr = match result.first() {
88			Some(Value::I32(v)) => *v as usize,
89			_ => {
90				return Err(FFIError::Other(format!(
91					"WASM procedure '{}': procedure returned unexpected result",
92					self.name
93				))
94				.into());
95			}
96		};
97
98		// Read output length (first 4 bytes at output_ptr)
99		let len_bytes = engine.read_memory(output_ptr, 4).map_err(|e| {
100			FFIError::Other(format!("WASM procedure '{}' read output length failed: {:?}", self.name, e))
101		})?;
102
103		let output_len = u32::from_le_bytes([len_bytes[0], len_bytes[1], len_bytes[2], len_bytes[3]]) as usize;
104
105		// Read full output data
106		let output_bytes = engine.read_memory(output_ptr + 4, output_len).map_err(|e| {
107			FFIError::Other(format!("WASM procedure '{}' read output data failed: {:?}", self.name, e))
108		})?;
109
110		Ok(unmarshal_columns_from_bytes(&output_bytes))
111	}
112}