Skip to main content

hara_native/wasm_binding/
runtime.rs

1#![cfg(not(target_arch = "wasm32"))]
2
3use std::cell::RefCell;
4use std::collections::BTreeSet;
5
6use wasmtime::{
7    Config, Engine, Instance, Memory, Module, Store, StoreLimits, StoreLimitsBuilder, Val,
8};
9
10use crate::core::Value;
11
12use super::{
13    inspect_direct, HaraValueType, Lifting, MemoryBindingPlan, MemoryContract, MemoryFunctionPlan,
14    MemoryResultPlan, Ownership, WasmValueType,
15};
16
17#[cfg(test)]
18mod tests;
19
20const MAX_MEMORY_BYTES: usize = 64 * 1024 * 1024;
21const MAX_VALUE_BYTES: usize = 16 * 1024 * 1024;
22const MAX_TOTAL_INPUT_BYTES: usize = 32 * 1024 * 1024;
23const MAX_TOTAL_COPY_BYTES: usize = MAX_TOTAL_INPUT_BYTES + MAX_VALUE_BYTES;
24const INVOCATION_FUEL: u64 = 10_000_000;
25const CLEANUP_FUEL: u64 = 1_000_000;
26
27struct Session {
28    store: Store<StoreLimits>,
29    instance: Instance,
30    memory: Memory,
31}
32
33/// Generic native executor for the closed `memory.v1` binding plan.
34///
35/// Library-specific behavior remains data in the binding plan. This executor
36/// owns bounded allocation, copying, invocation, lifting, and release once for
37/// every compatible Wasm library.
38pub struct WasmtimeMemoryExecutor {
39    plan: MemoryBindingPlan,
40    session: RefCell<Session>,
41}
42
43impl WasmtimeMemoryExecutor {
44    pub fn compile(bytes: &[u8], plan: MemoryBindingPlan) -> Result<Self, String> {
45        let inspection = inspect_direct(bytes)?;
46        plan.verify(&inspection)?;
47
48        let mut config = Config::new();
49        config.consume_fuel(true);
50        let engine = Engine::new(&config)
51            .map_err(|error| format!("extension/engine-unavailable: {error}"))?;
52        let module = Module::new(&engine, bytes)
53            .map_err(|error| format!("extension/module-invalid: {error}"))?;
54        if module.imports().next().is_some() {
55            return Err("extension/module-import-denied: memory.v1 must be import-free".into());
56        }
57
58        let limits = StoreLimitsBuilder::new()
59            .memory_size(MAX_MEMORY_BYTES)
60            .instances(1)
61            .memories(1)
62            .tables(1)
63            .build();
64        let mut store = Store::new(&engine, limits);
65        store.limiter(|limits| limits);
66        let instance = Instance::new(&mut store, &module, &[])
67            .map_err(|error| format!("extension/module-invalid: {error}"))?;
68        let memory = instance
69            .get_memory(&mut store, &plan.memory.export)
70            .ok_or_else(|| {
71                format!(
72                    "extension/memory-missing: module does not export {}",
73                    plan.memory.export
74                )
75            })?;
76        validate_instance(&plan, &mut store, &instance)?;
77
78        Ok(Self {
79            plan,
80            session: RefCell::new(Session {
81                store,
82                instance,
83                memory,
84            }),
85        })
86    }
87
88    pub fn plan(&self) -> &MemoryBindingPlan {
89        &self.plan
90    }
91
92    pub fn invoke(&self, export: &str, arguments: &[Value]) -> Result<Value, String> {
93        let function = self
94            .plan
95            .functions
96            .iter()
97            .find(|function| function.name == export)
98            .cloned()
99            .ok_or_else(|| format!("extension/export-missing: {export}"))?;
100        if arguments.len() != function.arguments.len() {
101            return Err(format!(
102                "extension/arity: {export} expects {} arguments, got {}",
103                function.arguments.len(),
104                arguments.len()
105            ));
106        }
107
108        let mut session = self.session.borrow_mut();
109        session
110            .store
111            .set_fuel(INVOCATION_FUEL)
112            .map_err(|error| format!("extension/execution-limit: {error}"))?;
113
114        let mut release_always = BTreeSet::new();
115        let mut release_on_failure = BTreeSet::new();
116        let mut call_completed = false;
117        let outcome = invoke_inner(
118            &self.plan.memory,
119            &function,
120            arguments,
121            &mut session,
122            &mut release_always,
123            &mut release_on_failure,
124            &mut call_completed,
125        );
126
127        if !call_completed {
128            release_always.extend(release_on_failure);
129        }
130        let cleanup = release_pointers(&self.plan.memory, &mut session, &release_always);
131        combine_outcome(outcome, cleanup)
132    }
133}
134
135fn validate_instance(
136    plan: &MemoryBindingPlan,
137    store: &mut Store<StoreLimits>,
138    instance: &Instance,
139) -> Result<(), String> {
140    for function in &plan.functions {
141        instance
142            .get_func(&mut *store, &function.wasm_export)
143            .ok_or_else(|| {
144                format!(
145                    "extension/export-missing: {} -> {}",
146                    function.name, function.wasm_export
147                )
148            })?;
149    }
150    if let Some(name) = plan.memory.allocate.as_deref() {
151        instance
152            .get_typed_func::<i32, i32>(&mut *store, name)
153            .map_err(|error| format!("extension/allocator-invalid: {name} ({error})"))?;
154    }
155    if let Some(name) = plan.memory.release.as_deref() {
156        instance
157            .get_typed_func::<i32, ()>(&mut *store, name)
158            .map_err(|error| format!("extension/release-invalid: {name} ({error})"))?;
159    }
160    Ok(())
161}
162
163#[allow(clippy::too_many_arguments)]
164fn invoke_inner(
165    memory_contract: &MemoryContract,
166    function_plan: &MemoryFunctionPlan,
167    arguments: &[Value],
168    session: &mut Session,
169    release_always: &mut BTreeSet<i32>,
170    release_on_failure: &mut BTreeSet<i32>,
171    call_completed: &mut bool,
172) -> Result<Value, String> {
173    let mut raw_arguments = Vec::with_capacity(function_plan.raw_arguments.len());
174    let mut total_input_bytes = 0usize;
175    let mut total_copy_bytes = 0usize;
176
177    for (argument_plan, value) in function_plan.arguments.iter().zip(arguments) {
178        if argument_plan.lowering.is_none() {
179            raw_arguments.push(scalar_argument(
180                &function_plan.name,
181                &argument_plan.hara_type,
182                value,
183            )?);
184            continue;
185        }
186        let bytes = memory_argument_bytes(&argument_plan.hara_type, value, &function_plan.name)?;
187        total_input_bytes = total_input_bytes
188            .checked_add(bytes.len())
189            .ok_or_else(|| "extension/resource-limit: input byte count overflow".to_owned())?;
190        if bytes.len() > MAX_VALUE_BYTES || total_input_bytes > MAX_TOTAL_INPUT_BYTES {
191            return Err(format!(
192                "extension/resource-limit: {} input exceeds the memory.v1 byte limit",
193                function_plan.name
194            ));
195        }
196        let (pointer, length) = lower_pointer_length(
197            memory_contract,
198            session,
199            bytes,
200            &function_plan.name,
201            argument_plan.ownership,
202            release_on_failure,
203        )?;
204        total_copy_bytes = total_copy_bytes
205            .checked_add(bytes.len())
206            .ok_or_else(|| "extension/resource-limit: copy byte count overflow".to_owned())?;
207        if total_copy_bytes > MAX_TOTAL_COPY_BYTES {
208            return Err(format!(
209                "extension/resource-limit: {} exceeds the memory.v1 aggregate copy limit",
210                function_plan.name
211            ));
212        }
213        raw_arguments.push(Val::I32(pointer));
214        raw_arguments.push(Val::I32(length));
215    }
216
217    let function = session
218        .instance
219        .get_func(&mut session.store, &function_plan.wasm_export)
220        .ok_or_else(|| {
221            format!(
222                "extension/export-missing: {} -> {}",
223                function_plan.name, function_plan.wasm_export
224            )
225        })?;
226    let mut raw_results = if function_plan.raw_returns == WasmValueType::Void {
227        Vec::new()
228    } else {
229        vec![default_result(function_plan.raw_returns)]
230    };
231    function
232        .call(&mut session.store, &raw_arguments, &mut raw_results)
233        .map_err(|error| format!("extension/invoke-failed: {} ({error})", function_plan.name))?;
234    *call_completed = true;
235
236    lift_result(
237        &function_plan.name,
238        &function_plan.returns,
239        raw_results.into_iter().next(),
240        session,
241        release_always,
242        &mut total_copy_bytes,
243    )
244}
245
246fn memory_argument_bytes<'a>(
247    expected: &HaraValueType,
248    value: &'a Value,
249    export: &str,
250) -> Result<&'a [u8], String> {
251    match (expected, value) {
252        (HaraValueType::Bytes, Value::Bytes(bytes)) => Ok(bytes),
253        (HaraValueType::String, Value::String(value)) => Ok(value.as_bytes()),
254        _ => Err(format!(
255            "extension/type-error: {export} expects :{}",
256            hara_type_name(expected)
257        )),
258    }
259}
260
261fn lower_pointer_length(
262    contract: &MemoryContract,
263    session: &mut Session,
264    bytes: &[u8],
265    export: &str,
266    ownership: Option<Ownership>,
267    release_on_failure: &mut BTreeSet<i32>,
268) -> Result<(i32, i32), String> {
269    let length = i32::try_from(bytes.len())
270        .map_err(|_| format!("extension/resource-limit: {export} input is too large"))?;
271    if length == 0 {
272        return Ok((0, 0));
273    }
274    let allocator_name = contract
275        .allocate
276        .as_deref()
277        .ok_or_else(|| format!("extension/allocator-missing: {export} requires an allocator"))?;
278    let allocator = session
279        .instance
280        .get_typed_func::<i32, i32>(&mut session.store, allocator_name)
281        .map_err(|error| format!("extension/allocator-invalid: {allocator_name} ({error})"))?;
282    let pointer = allocator
283        .call(&mut session.store, length)
284        .map_err(|error| format!("extension/allocator-failed: {export} ({error})"))?;
285    if pointer != 0 && ownership == Some(Ownership::Transferred) {
286        release_on_failure.insert(pointer);
287    }
288    let start = checked_range(&session.memory, &session.store, pointer, length, export)?;
289    session
290        .memory
291        .write(&mut session.store, start, bytes)
292        .map_err(|error| format!("extension/memory-write-failed: {export} ({error})"))?;
293    Ok((pointer, length))
294}
295
296fn lift_result(
297    export: &str,
298    plan: &MemoryResultPlan,
299    raw: Option<Val>,
300    session: &mut Session,
301    release_always: &mut BTreeSet<i32>,
302    total_copy_bytes: &mut usize,
303) -> Result<Value, String> {
304    if plan.lifting.is_none() {
305        return scalar_result(export, &plan.hara_type, raw);
306    }
307    if plan.lifting != Some(Lifting::PackedI64) {
308        return Err(format!(
309            "extension/abi-type-unsupported: {export} result lifting"
310        ));
311    }
312    let Some(Val::I64(raw)) = raw else {
313        return Err(format!(
314            "extension/abi-type-unsupported: {export} expected packed i64"
315        ));
316    };
317
318    // memory.v1 packs the pointer in the low 32 bits and byte length in the
319    // high 32 bits. Both fields are interpreted as unsigned values.
320    let packed = raw as u64;
321    let pointer_u32 = packed as u32;
322    let length_u32 = (packed >> 32) as u32;
323    let pointer = i32::try_from(pointer_u32)
324        .map_err(|_| format!("extension/memory-range: {export} pointer is out of range"))?;
325    let length = i32::try_from(length_u32)
326        .map_err(|_| format!("extension/resource-limit: {export} result is too large"))?;
327    let length_usize = usize::try_from(length)
328        .map_err(|_| format!("extension/memory-range: {export} result length is negative"))?;
329    if length_usize > MAX_VALUE_BYTES {
330        return Err(format!(
331            "extension/resource-limit: {export} result exceeds the memory.v1 byte limit"
332        ));
333    }
334    *total_copy_bytes = total_copy_bytes
335        .checked_add(length_usize)
336        .ok_or_else(|| format!("extension/resource-limit: {export} copy byte count overflow"))?;
337    if *total_copy_bytes > MAX_TOTAL_COPY_BYTES {
338        return Err(format!(
339            "extension/resource-limit: {export} exceeds the memory.v1 aggregate copy limit"
340        ));
341    }
342    if plan.ownership == Some(Ownership::Caller) && pointer != 0 {
343        release_always.insert(pointer);
344    }
345    let start = checked_range(&session.memory, &session.store, pointer, length, export)?;
346    let mut bytes = vec![0u8; length_usize];
347    session
348        .memory
349        .read(&session.store, start, &mut bytes)
350        .map_err(|error| format!("extension/memory-read-failed: {export} ({error})"))?;
351    match plan.hara_type {
352        HaraValueType::Bytes => Ok(Value::Bytes(bytes)),
353        HaraValueType::String => String::from_utf8(bytes)
354            .map(Value::String)
355            .map_err(|error| format!("extension/utf8-invalid: {export} ({error})")),
356        _ => Err(format!(
357            "extension/abi-type-unsupported: {export} cannot lift :{}",
358            hara_type_name(&plan.hara_type)
359        )),
360    }
361}
362
363fn checked_range(
364    memory: &Memory,
365    store: &Store<StoreLimits>,
366    pointer: i32,
367    length: i32,
368    export: &str,
369) -> Result<usize, String> {
370    let start = usize::try_from(pointer)
371        .map_err(|_| format!("extension/memory-range: {export} pointer is negative"))?;
372    let length = usize::try_from(length)
373        .map_err(|_| format!("extension/memory-range: {export} length is negative"))?;
374    let end = start
375        .checked_add(length)
376        .ok_or_else(|| format!("extension/memory-range: {export} range overflow"))?;
377    if end > memory.data_size(store) {
378        return Err(format!(
379            "extension/memory-range: {export} range {start}..{end} exceeds linear memory"
380        ));
381    }
382    Ok(start)
383}
384
385fn release_pointers(
386    contract: &MemoryContract,
387    session: &mut Session,
388    pointers: &BTreeSet<i32>,
389) -> Result<(), String> {
390    if pointers.is_empty() {
391        return Ok(());
392    }
393    let release_name = contract
394        .release
395        .as_deref()
396        .ok_or_else(|| "extension/release-missing: cleanup requires a release export".to_owned())?;
397    let release = session
398        .instance
399        .get_typed_func::<i32, ()>(&mut session.store, release_name)
400        .map_err(|error| format!("extension/release-invalid: {release_name} ({error})"))?;
401    session
402        .store
403        .set_fuel(CLEANUP_FUEL)
404        .map_err(|error| format!("extension/release-failed: fuel ({error})"))?;
405
406    let mut failures = Vec::new();
407    for pointer in pointers {
408        if let Err(error) = release.call(&mut session.store, *pointer) {
409            failures.push(format!("{pointer}: {error}"));
410        }
411    }
412    if failures.is_empty() {
413        Ok(())
414    } else {
415        Err(format!("extension/release-failed: {}", failures.join("; ")))
416    }
417}
418
419fn combine_outcome(
420    outcome: Result<Value, String>,
421    cleanup: Result<(), String>,
422) -> Result<Value, String> {
423    match (outcome, cleanup) {
424        (Ok(value), Ok(())) => Ok(value),
425        (Err(error), Ok(())) => Err(error),
426        (Ok(_), Err(cleanup)) => Err(cleanup),
427        (Err(error), Err(cleanup)) => Err(format!("{error}; cleanup: {cleanup}")),
428    }
429}
430
431fn scalar_argument(export: &str, expected: &HaraValueType, value: &Value) -> Result<Val, String> {
432    fn finite_f32(value: f64) -> Result<f32, String> {
433        let value = value as f32;
434        if value.is_finite() {
435            Ok(value)
436        } else {
437            Err("non-finite number".into())
438        }
439    }
440    let type_error = || {
441        format!(
442            "extension/type-error: {export} expects :{}",
443            hara_type_name(expected)
444        )
445    };
446    match (expected, value) {
447        (HaraValueType::I32, Value::Number(value)) => i32::try_from(*value)
448            .map(Val::I32)
449            .map_err(|_| type_error()),
450        (HaraValueType::Boolean, Value::Bool(value)) => Ok(Val::I32(i32::from(*value))),
451        (HaraValueType::I64, Value::Number(value)) => Ok(Val::I64(*value)),
452        (HaraValueType::F32, Value::Float(value)) => {
453            Ok(Val::F32(finite_f32(*value)?.to_bits()))
454        }
455        (HaraValueType::F32, Value::Number(value)) => {
456            Ok(Val::F32(finite_f32(*value as f64)?.to_bits()))
457        }
458        (HaraValueType::F64, Value::Float(value)) => {
459            Ok(Val::F64(crate::numeric::finite_float(*value)?.to_bits()))
460        }
461        (HaraValueType::F64, Value::Number(value)) => Ok(Val::F64((*value as f64).to_bits())),
462        _ => Err(type_error()),
463    }
464}
465
466fn scalar_result(
467    export: &str,
468    expected: &HaraValueType,
469    raw: Option<Val>,
470) -> Result<Value, String> {
471    match (expected, raw) {
472        (HaraValueType::Void, None) => Ok(Value::Nil),
473        (HaraValueType::I32, Some(Val::I32(value))) => Ok(Value::Number(i64::from(value))),
474        (HaraValueType::Boolean, Some(Val::I32(value))) => Ok(Value::Bool(value != 0)),
475        (HaraValueType::I64, Some(Val::I64(value))) => Ok(Value::Number(value)),
476        (HaraValueType::F32, Some(Val::F32(value))) => {
477            Ok(Value::Float(crate::numeric::finite_float(
478                f32::from_bits(value) as f64,
479            )?))
480        }
481        (HaraValueType::F64, Some(Val::F64(value))) => {
482            Ok(Value::Float(crate::numeric::finite_float(f64::from_bits(value))?))
483        }
484        _ => Err(format!(
485            "extension/abi-type-unsupported: {export} -> :{}",
486            hara_type_name(expected)
487        )),
488    }
489}
490
491fn default_result(raw: WasmValueType) -> Val {
492    match raw {
493        WasmValueType::I32 => Val::I32(0),
494        WasmValueType::I64 => Val::I64(0),
495        WasmValueType::F32 => Val::F32(0),
496        WasmValueType::F64 => Val::F64(0),
497        WasmValueType::Void => unreachable!("void functions do not allocate a result slot"),
498    }
499}
500
501fn hara_type_name(value: &HaraValueType) -> &str {
502    match value {
503        HaraValueType::I32 => "i32",
504        HaraValueType::I64 => "i64",
505        HaraValueType::F32 => "f32",
506        HaraValueType::F64 => "f64",
507        HaraValueType::Boolean => "boolean",
508        HaraValueType::String => "string",
509        HaraValueType::Bytes => "bytes",
510        HaraValueType::Record(_) => "record",
511        HaraValueType::Variant(_) => "variant",
512        HaraValueType::Handle(_) => "handle",
513        HaraValueType::Callback(_) => "callback",
514        HaraValueType::Void => "void",
515    }
516}