1use nomiscript::SymbolTable;
2use tracing::debug;
3use wasmtime::{Engine, Linker, Module, Store};
4
5use crate::error::HookError;
6use crate::format::{BASE_OFFSET, GlobalHeader, OUTPUT_HEADER_SIZE, OutputHeader};
7use crate::host::{WasmHost, define_host_functions};
8use crate::parser::{OutputParser, ParsedEntity};
9use crate::runtime::{EngineOpts, build_engine};
10
11const DEFAULT_OUTPUT_SIZE: u32 = 64 * 1024;
12const WASM_PAGE_SIZE: u32 = 65536;
13const DEFAULT_EPOCH_DEADLINE_TICKS: u64 = 5;
21const EPOCH_TICK_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
22
23struct EpochTicker {
37 stop: Option<std::sync::mpsc::Sender<()>>,
38 handle: Option<std::thread::JoinHandle<()>>,
39}
40
41impl EpochTicker {
42 fn spawn(engine: Engine, ticks: u64) -> Self {
43 let (stop, rx) = std::sync::mpsc::channel::<()>();
44 let handle = std::thread::spawn(move || {
45 for _ in 0..ticks {
46 match rx.recv_timeout(EPOCH_TICK_INTERVAL) {
47 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => engine.increment_epoch(),
48 _ => return,
49 }
50 }
51 });
52 Self {
53 stop: Some(stop),
54 handle: Some(handle),
55 }
56 }
57}
58
59impl Drop for EpochTicker {
60 fn drop(&mut self) {
61 self.stop.take();
62 if let Some(handle) = self.handle.take() {
63 let _ = handle.join();
64 }
65 }
66}
67
68pub struct ScriptExecutor {
69 host: WasmHost,
70 epoch_deadline_ticks: u64,
71}
72
73impl ScriptExecutor {
74 pub fn try_new() -> Result<Self, HookError> {
81 Ok(Self {
82 host: WasmHost::new(build_engine(EngineOpts::baseline())?, SymbolTable::new()),
83 epoch_deadline_ticks: DEFAULT_EPOCH_DEADLINE_TICKS,
84 })
85 }
86
87 #[must_use]
88 pub fn with_engine(engine: Engine) -> Self {
89 Self {
90 host: WasmHost::new(engine, SymbolTable::new()),
91 epoch_deadline_ticks: DEFAULT_EPOCH_DEADLINE_TICKS,
92 }
93 }
94
95 #[must_use]
101 pub fn with_epoch_deadline_ticks(mut self, ticks: u64) -> Self {
102 self.epoch_deadline_ticks = ticks;
103 self
104 }
105
106 fn get_or_compile_module(&self, bytecode: &[u8]) -> Result<Module, HookError> {
107 self.host
108 .module_cache()
109 .get_or_compile(self.host.engine(), bytecode)
110 .map_err(HookError::from)
111 }
112
113 pub fn execute(
114 &self,
115 bytecode: &[u8],
116 input: &[u8],
117 output_size: Option<u32>,
118 ) -> Result<Vec<ParsedEntity>, HookError> {
119 debug!(bytecode_size = bytecode.len(), "script execution start");
120 let output_size = output_size.unwrap_or(DEFAULT_OUTPUT_SIZE);
121 let module = self.get_or_compile_module(bytecode)?;
122 debug!("module compiled");
123
124 let header = GlobalHeader::from_bytes(input)
125 .ok_or_else(|| HookError::Parse("Invalid input header".to_string()))?;
126
127 let input_offset = BASE_OFFSET;
128 let output_offset = input_offset + input.len() as u32;
129 let strings_offset = header.strings_pool_offset;
130
131 let exec_state = self
132 .host
133 .execution_state(input_offset, output_offset, strings_offset);
134 let mut store = Store::new(self.host.engine(), exec_state);
135 store.set_epoch_deadline(self.epoch_deadline_ticks);
136
137 let _ticker = EpochTicker::spawn(self.host.engine().clone(), self.epoch_deadline_ticks);
138
139 let mut linker = Linker::new(self.host.engine());
140 define_host_functions(&mut linker)?;
141
142 let instance = linker.instantiate(&mut store, &module)?;
143
144 let memory = instance
145 .get_memory(&mut store, "memory")
146 .ok_or(HookError::WASMMem)?;
147
148 store.data_mut().memory = Some(memory);
149
150 let total_size = input.len() + output_size as usize;
151 let required_pages = (BASE_OFFSET as usize + total_size).div_ceil(WASM_PAGE_SIZE as usize);
152 let current_pages = memory.size(&store) as usize;
153
154 if required_pages > current_pages {
155 memory.grow(&mut store, (required_pages - current_pages) as u64)?;
156 }
157
158 let mem_data = memory.data_mut(&mut store);
159 let input_start = BASE_OFFSET as usize;
160 mem_data[input_start..input_start + input.len()].copy_from_slice(input);
161
162 let output_start = output_offset as usize;
163 let input_entity_count = header.input_entity_count;
164 let output_header = OutputHeader::new(input_entity_count);
165 mem_data[output_start..output_start + OUTPUT_HEADER_SIZE]
166 .copy_from_slice(&output_header.to_bytes());
167
168 let should_apply = instance
169 .get_typed_func::<(), i32>(&mut store, "should_apply")
170 .map_err(|e| HookError::Script(format!("Missing should_apply export: {e}")))?;
171
172 let result = should_apply.call(&mut store, ())?;
173 debug!(should_apply = result, "should_apply result");
174 if result == 0 {
175 return Ok(Vec::new());
176 }
177
178 let process = instance
179 .get_typed_func::<(), ()>(&mut store, "process")
180 .map_err(|e| HookError::Script(format!("Missing process export: {e}")))?;
181
182 debug!("calling process");
183 process.call(&mut store, ())?;
184
185 let mem_data = memory.data(&store);
186 let output_data = &mem_data[output_start..output_start + output_size as usize];
187
188 let output_header = OutputHeader::from_bytes(output_data)
189 .ok_or_else(|| HookError::Parse("Invalid output header".to_string()))?;
190
191 let output_strings_offset = { output_header.strings_offset } as usize;
192
193 let parser = OutputParser::new(output_data, output_strings_offset)?;
194 let entities: Result<Vec<ParsedEntity>, HookError> = parser.entities().collect();
195 debug!(
196 entity_count = entities.as_ref().map_or(0, std::vec::Vec::len),
197 "output parse complete"
198 );
199 entities
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_executor_creation() {
209 let executor = ScriptExecutor::try_new().expect("baseline engine");
210 assert!(
211 executor
212 .host
213 .module_cache()
214 .is_empty()
215 .expect("cache lock must not be poisoned in fresh executor")
216 );
217 }
218}