From f46c32de97c18875509d15c02f217bf592a59813 Mon Sep 17 00:00:00 2001 From: Henry Date: Sun, 23 Aug 2026 22:49:57 +0200 Subject: [PATCH 1/3] feat: add host reference APIs - add Store-aware handles for references, GC objects, and exceptions - simplify host value conversion, rooting, and exception propagation - update examples, tests, and documentation Signed-off-by: Henry --- CHANGELOG.md | 14 +- CONTRIBUTING.md | 4 +- Cargo.toml | 2 +- README.md | 4 +- crates/cli/Cargo.toml | 1 + crates/cli/src/wast_runner.rs | 108 ++- crates/parser/src/conversion.rs | 7 +- crates/tinywasm/src/error.rs | 13 +- crates/tinywasm/src/func/context.rs | 30 +- crates/tinywasm/src/func/host.rs | 62 +- crates/tinywasm/src/func/mod.rs | 49 +- crates/tinywasm/src/func/resume.rs | 47 +- crates/tinywasm/src/func/values.rs | 96 +- crates/tinywasm/src/imports.rs | 11 +- crates/tinywasm/src/instance.rs | 25 +- crates/tinywasm/src/interpreter/executor.rs | 197 ++-- .../src/interpreter/stack/value_stack.rs | 137 +-- crates/tinywasm/src/interpreter/values.rs | 57 +- crates/tinywasm/src/lib.rs | 46 +- crates/tinywasm/src/reference.rs | 513 ---------- crates/tinywasm/src/reference/managed.rs | 236 +++++ crates/tinywasm/src/reference/mod.rs | 13 + crates/tinywasm/src/reference/store.rs | 914 ++++++++++++++++++ crates/tinywasm/src/reference/value.rs | 210 ++++ crates/tinywasm/src/store/const_expr.rs | 125 +-- crates/tinywasm/src/store/exception.rs | 10 - crates/tinywasm/src/store/function.rs | 8 - crates/tinywasm/src/store/gc/mod.rs | 58 +- crates/tinywasm/src/store/gc/object.rs | 148 +-- crates/tinywasm/src/store/gc/roots.rs | 58 ++ crates/tinywasm/src/store/global.rs | 12 +- crates/tinywasm/src/store/mod.rs | 283 +++++- crates/tinywasm/src/store/state.rs | 224 ++--- crates/tinywasm/src/store/table.rs | 3 + crates/tinywasm/tests/gc_refs.rs | 12 +- .../tests/host_func_signature_check.rs | 22 +- crates/tinywasm/tests/imported_table_init.rs | 7 +- crates/tinywasm/tests/internal_refs.rs | 23 +- crates/tinywasm/tests/managed_exceptions.rs | 140 +++ crates/tinywasm/tests/memory.rs | 22 +- crates/tinywasm/tests/reference_roots.rs | 96 ++ crates/tinywasm/tests/store_ownership.rs | 6 +- crates/tinywasm/tests/typed_gc_access.rs | 49 + crates/tinywasm/tests/typed_globals.rs | 6 +- crates/types/src/archive.rs | 42 +- crates/types/src/instructions.rs | 5 +- crates/types/src/lib.rs | 24 +- crates/types/src/reference.rs | 162 ---- crates/types/src/value.rs | 297 +----- examples/archive.rs | 8 +- examples/{simple.rs => basic.rs} | 6 +- examples/exceptions.rs | 28 + examples/funcref_callbacks.rs | 123 --- examples/gc.rs | 33 + examples/linking.rs | 12 +- examples/reentrance.rs | 5 +- examples/references.rs | 34 + examples/resumable.rs | 2 + examples/{wasm-rust.rs => rust.rs} | 10 +- examples/rust/README.md | 4 +- examples/simple2.rs | 21 - 61 files changed, 2870 insertions(+), 2054 deletions(-) delete mode 100644 crates/tinywasm/src/reference.rs create mode 100644 crates/tinywasm/src/reference/managed.rs create mode 100644 crates/tinywasm/src/reference/mod.rs create mode 100644 crates/tinywasm/src/reference/store.rs create mode 100644 crates/tinywasm/src/reference/value.rs delete mode 100644 crates/tinywasm/src/store/exception.rs create mode 100644 crates/tinywasm/src/store/gc/roots.rs create mode 100644 crates/tinywasm/tests/managed_exceptions.rs create mode 100644 crates/tinywasm/tests/reference_roots.rs create mode 100644 crates/tinywasm/tests/typed_gc_access.rs rename examples/{simple.rs => basic.rs} (72%) create mode 100644 examples/exceptions.rs delete mode 100644 examples/funcref_callbacks.rs create mode 100644 examples/gc.rs create mode 100644 examples/references.rs rename examples/{wasm-rust.rs => rust.rs} (95%) delete mode 100644 examples/simple2.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5473ebc..9941450 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,23 +10,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support for the WebAssembly function-references proposal -- Added basic support for the WebAssembly garbage-collection proposal +- Added support for the WebAssembly garbage-collection proposal - Added support for the WebAssembly exception-handling proposal, including tags, `try_table`, `throw`, and `throw_ref` - Added support for the WebAssembly compact-imports proposal -- Added `WasmValue::ty` and `WasmValue::matches_type` +- Added `WasmValue::ty` and `WasmValue::matches_type`. Non-null concrete reference types require Store-aware validation. - Added `ValueLane` for mapping WebAssembly value types to their physical 32-bit, 64-bit, or 128-bit storage lane. - Added a `validate` feature to `tinywasm` and `tinywasm-parser` (enabled by default) to optionally skip wasmparser validation for faster parsing of trusted modules. - Added optional parse-time operand deduplication to reduce precompiled module and `.twasm` archive size. - Added a `ResourceLimiter` trait, configurable through `engine::Config::with_resource_limiter`, to bound guest memory, table, and logical GC heap growth. +- Added Store-aware owned references, explicit GC collection, and typed host access to GC objects and exceptions. ### Changed - `HostFunction` is now a reusable definition, and module instantiation borrows `Imports` so host imports can be shared across stores. - Host function callbacks now require `Send + Sync` so the same definition can be used safely with multiple stores. -- Typed function tuples now support up to 20 parameters or results. `WasmTupleChain` is deprecated. Use untyped functions for larger signatures. +- Typed function tuples now support up to 20 parameters or results. Use untyped functions for larger signatures. - Module types now use one dense recursive type space, while function types are resolved through `Function::ty(&Store)`. - Globals are stored in separate 32-bit, 64-bit, and 128-bit value lanes, avoiding tagged value conversion during guest execution. - Linear memory now uses a single contiguous `Vec`-backed storage with const-generic fixed-width loads and stores. +- Exceptions are stored as traced managed objects, allowing unreachable exceptions and their payload graphs to be collected. - Increased the minimum supported Rust version from 1.95 to 1.98. ### Fixed @@ -50,6 +52,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed the local-memory allocation analysis (`LocalMemoryAllocation` and `ParserOptions::optimize_local_memory_allocation`). Local memories are always allocated eagerly. - Removed `Config::with_trap_on_oom`. A `ResourceLimiter` can return a trap when rejecting a memory or table allocation or growth request. - `Table::grow` now returns `Result>`, matching `Memory::grow`. Growth limits and allocation failures return `None`, while limiter-provided traps return an error. +- Public function and managed references are Store-aware. Managed references such as `StructRef`, `ArrayRef`, `ExternRef`, and `ExnRef` are owned handles that keep their referents live until the last clone is dropped. +- `ExternRef::try_new` and `I31Ref::try_new` create host references. Reference data and GC object access are provided by inherent methods on each reference type. +- Renamed the fallible `Memory`, `Table`, `Global`, and `Tag` constructors from `new` to `try_new`. +- `WasmValue` is no longer `Copy` because it can contain owned references. +- Nullable typed reference parameters and results use `Option`. Bare typed reference values are non-null. +- Removed `WasmTupleChain`. Use direct tuples up to arity 20 or untyped functions for larger signatures. ## [0.10.0] - 2026-07-24 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6cb7761..73186e7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,7 +33,7 @@ $ cargo test-wast ./wasm-testsuite/data/wasm-v1/{file}.wast $ cargo test-wasm-custom # Run a specific example (run without arguments to see available examples) -# The wasm test files required to run the `wasm-rust` examples are not +# The wasm test files required to run the `rust` example are not # included in the main repository. # To build these, you will need to install binaryen and wabt # and run `./examples/rust/build.sh`. @@ -48,7 +48,7 @@ Example usage: ```bash cargo install --locked samply -samply record -- cargo run --release --example wasm-rust -- tinywasm +samply record -- cargo run --release --example rust -- tinywasm ``` ## Commits diff --git a/Cargo.toml b/Cargo.toml index 764ae53..12fad7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ wat = "1.257" criterion = { version = "0.8", default-features = false, features = ["cargo_bench_support", "rayon"] } [[example]] -name = "wasm-rust" +name = "rust" test = false [[bench]] diff --git a/README.md b/README.md index 5355249..186e4dc 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ let result = func.call(&mut store, (1, 2))?; assert_eq!(result, 3); ``` -See the [examples](./examples) directory and [documentation](https://docs.rs/tinywasm) for more information. +See the [examples](./examples), including the [GC reference example](./examples/gc.rs), and the [documentation](https://docs.rs/tinywasm) for more information. ## Precompiled Modules @@ -96,7 +96,7 @@ TinyWasm targets non-JavaScript core proposals through [phase 3](https://github. | [**Garbage Collection**](https://github.com/WebAssembly/gc) | 🟢 | `next` | | [**Exception Handling**](https://github.com/WebAssembly/exception-handling) | 🟢 | `next` | | [**Stack Switching**](https://github.com/WebAssembly/stack-switching) | 🌑 | - | -| [**Compact Import Section**](https://github.com/WebAssembly/compact-import-section) | 🌑 | - | +| [**Compact Import Section**](https://github.com/WebAssembly/compact-import-section) | 🟢 | `next` | | [**Threads**](https://github.com/WebAssembly/threads) | 🌑 | - | **Legend**\ diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 1aae437..069a4e2 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -35,6 +35,7 @@ tinywasm = { workspace = true, features = [ "archive", "canonicalize-nans", "debug", + "guest-debug", "log", "parallel-parser", "parser", diff --git a/crates/cli/src/wast_runner.rs b/crates/cli/src/wast_runner.rs index 023c959..65fba7e 100644 --- a/crates/cli/src/wast_runner.rs +++ b/crates/cli/src/wast_runner.rs @@ -7,10 +7,7 @@ use std::time::Duration; use anyhow::{Context, Result, anyhow, bail}; use log::{debug, error}; -use tinywasm::types::{ - AbstractHeapType as TinyAbstractHeapType, AnyRef, ExternRef, FuncRef, MemoryType, RefType, RefValue, TableType, - WasmType, WasmValue, -}; +use tinywasm::types::{MemoryType, RefType, RefValue, TableType, WasmType, WasmValue}; use tinywasm::{ExecProgress, Global, HostFunction, Imports, Memory, Module, ModuleInstance, Store, Table}; use wast::{QuoteWat, core::AbstractHeapType}; @@ -171,17 +168,18 @@ impl WastRunner { fn imports(store: &mut Store, modules: &HashMap) -> Result { let mut imports = Imports::new(); - let table = Table::new(store, TableType::new(RefType::FUNCREF, 10, Some(20)), RefValue::Null.into())?; - let table64 = Table::new(store, TableType::new64(RefType::FUNCREF, 10, Some(20)), RefValue::Null.into())?; - let memory = Memory::new(store, MemoryType::default().with_page_count_initial(1).with_page_count_max(Some(2)))?; + let table = Table::try_new(store, TableType::new(RefType::FUNCREF, 10, Some(20)), RefValue::Null.into())?; + let table64 = Table::try_new(store, TableType::new64(RefType::FUNCREF, 10, Some(20)), RefValue::Null.into())?; + let memory = + Memory::try_new(store, MemoryType::default().with_page_count_initial(1).with_page_count_max(Some(2)))?; let global_i32 = - Global::new(store, tinywasm::types::GlobalType::new(WasmType::I32, false), WasmValue::I32(666))?; + Global::try_new(store, tinywasm::types::GlobalType::new(WasmType::I32, false), WasmValue::I32(666))?; let global_i64 = - Global::new(store, tinywasm::types::GlobalType::new(WasmType::I64, false), WasmValue::I64(666))?; + Global::try_new(store, tinywasm::types::GlobalType::new(WasmType::I64, false), WasmValue::I64(666))?; let global_f32 = - Global::new(store, tinywasm::types::GlobalType::new(WasmType::F32, false), WasmValue::F32(666.6))?; + Global::try_new(store, tinywasm::types::GlobalType::new(WasmType::F32, false), WasmValue::F32(666.6))?; let global_f64 = - Global::new(store, tinywasm::types::GlobalType::new(WasmType::F64, false), WasmValue::F64(666.6))?; + Global::try_new(store, tinywasm::types::GlobalType::new(WasmType::F64, false), WasmValue::F64(666.6))?; imports .define("spectest", "memory", memory) @@ -333,7 +331,7 @@ impl WastRunner { } AssertExhaustion { call, message, span } => { let module = module_registry.get_idx(call.module); - let args = convert_wastargs(call.args)?; + let args = convert_wastargs(&mut store, call.args)?; let res = catch_unwind_silent(|| exec_fn_instance(module, &mut store, call.name, &args).map(|_| ())); let Ok(Err(tinywasm::Error::Trap(trap))) = res else { @@ -368,8 +366,8 @@ impl WastRunner { wast::WastExecute::Invoke(invoke) => invoke, }; let module = module_registry.get_idx(invoke.module); - let args = - convert_wastargs(invoke.args).map_err(|err| tinywasm::Error::Other(err.to_string()))?; + let args = convert_wastargs(&mut store, invoke.args) + .map_err(|err| tinywasm::Error::Other(err.to_string()))?; exec_fn_instance(module, &mut store, invoke.name, &args).map(|_| ()) }); match res { @@ -415,8 +413,8 @@ impl WastRunner { wast::WastExecute::Invoke(invoke) => invoke, }; let module = module_registry.get_idx(invoke.module); - let args = - convert_wastargs(invoke.args).map_err(|err| tinywasm::Error::Other(err.to_string()))?; + let args = convert_wastargs(&mut store, invoke.args) + .map_err(|err| tinywasm::Error::Other(err.to_string()))?; exec_fn_instance(module, &mut store, invoke.name, &args).map(|_| ()) }); let result = match res { @@ -469,7 +467,7 @@ impl WastRunner { Invoke(invoke) => { let name = invoke.name; let res: Result, _> = catch_unwind_silent(|| { - let args = convert_wastargs(invoke.args)?; + let args = convert_wastargs(&mut store, invoke.args)?; let module = module_registry.get_idx(invoke.module); exec_fn_instance(module, &mut store, invoke.name, &args).map_err(|e| { error!("failed to execute function: {e:?}"); @@ -504,7 +502,7 @@ impl WastRunner { ); continue; }; - let module_global = match module.global_get(&store, global) { + let module_global = match module.global_get(&mut store, global) { Ok(value) => value, Err(err) => { test_group.add_result( @@ -518,7 +516,7 @@ impl WastRunner { let expected = expected_alternatives .iter() .filter_map(|alts| alts.first()) - .find(|exp| exp.matches(&module_global, &store)); + .find(|exp| exp.matches(&module_global, &store, &module)); if expected.is_none() { test_group.add_result( &format!("AssertReturn(unsupported-{i})"), @@ -552,12 +550,15 @@ impl WastRunner { let invoke_name = invoke.name; let res: Result, _> = catch_unwind_silent(|| { - let args = convert_wastargs(invoke.args)?; - let module = module_registry.get_idx(invoke.module); - let outcomes = exec_fn_instance(module, &mut store, invoke.name, &args).map_err(|e| { - error!("failed to execute function: {e:?}"); - e - })?; + let args = convert_wastargs(&mut store, invoke.args)?; + let module = module_registry + .get(invoke.module) + .ok_or_else(|| anyhow!("module instance was not found"))?; + let outcomes = + exec_fn_instance(Some(module.id()), &mut store, invoke.name, &args).map_err(|e| { + error!("failed to execute function: {e:?}"); + e + })?; if !expected_alternatives.iter().any(|expected| expected.len() == outcomes.len()) { return Err(anyhow!( "expected {} results, got {}", @@ -570,7 +571,7 @@ impl WastRunner { && outcomes .iter() .zip(expected.iter()) - .all(|(outcome, exp)| exp.matches(outcome, &store)) + .all(|(outcome, exp)| exp.matches(outcome, &store, &module)) }) { Ok(()) } else { @@ -790,8 +791,8 @@ fn parse_quote_module(module: QuoteWat) -> Result<(Option, Module)> { Ok((name, parse_module_bytes(&bytes)?)) } -fn convert_wastargs(args: Vec) -> Result> { - args.into_iter().map(wastarg2tinywasmvalue).collect() +fn convert_wastargs(store: &mut Store, args: Vec) -> Result> { + args.into_iter().map(|arg| wastarg2tinywasmvalue(store, arg)).collect() } fn convert_wastret<'a>(args: impl Iterator>) -> Result>> { @@ -802,7 +803,7 @@ fn convert_wastret<'a>(args: impl Iterator>) -> Result< for prefix in alternatives { for choice in &choices { let mut candidate = prefix.clone(); - candidate.push(*choice); + candidate.push(choice.clone()); next.push(candidate); } } @@ -811,7 +812,7 @@ fn convert_wastret<'a>(args: impl Iterator>) -> Result< Ok(alternatives) } -fn wastarg2tinywasmvalue(arg: wast::WastArg) -> Result { +fn wastarg2tinywasmvalue(store: &mut Store, arg: wast::WastArg) -> Result { let wast::WastArg::Core(arg) = arg else { bail!("unsupported arg type: Component"); }; @@ -822,7 +823,7 @@ fn wastarg2tinywasmvalue(arg: wast::WastArg) -> Result { I32(i) => WasmValue::I32(i), I64(i) => WasmValue::I64(i), V128(i) => WasmValue::V128(i.to_le_bytes()), - RefExtern(v) => ExternRef::try_new(v).ok_or_else(|| anyhow!("external reference address is too large"))?.into(), + RefExtern(v) => tinywasm::ExternRef::try_new(store, v)?.into(), RefNull(t) => match t { wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Func } => RefValue::Null.into(), wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Extern | AbstractHeapType::Any } => { @@ -832,10 +833,7 @@ fn wastarg2tinywasmvalue(arg: wast::WastArg) -> Result { bail!("unsupported arg type: refnull: {:?}", t); } }, - RefHost(value) => { - RefValue::Any(AnyRef::from_host(value).ok_or_else(|| anyhow!("host reference address is too large"))?) - .into() - } + RefHost(value) => RefValue::Any(tinywasm::ExternRef::try_new(store, value)?.to_any()).into(), }) } @@ -855,7 +853,7 @@ fn wast_v128_to_bytes(i: wast::core::V128Pattern) -> [u8; 16] { res.try_into().unwrap() } -#[derive(Clone, Copy)] +#[derive(Clone)] enum ExpectedValue { Exact(WasmValue), RefNull, @@ -866,24 +864,35 @@ enum ExpectedValue { RefI31, RefStruct, RefArray, + RefExternExact(u32), + RefFuncExact(u32), + RefHostExact(u32), } impl ExpectedValue { - fn matches(&self, value: &WasmValue, store: &Store) -> bool { + fn matches(&self, value: &WasmValue, store: &Store, module: &ModuleInstance) -> bool { match self { Self::Exact(expected) => value.eq_loose(expected), Self::RefNull => matches!(value, WasmValue::Ref(RefValue::Null)), Self::RefFunc => matches!(value, WasmValue::Ref(RefValue::Func(_))), Self::RefExtern => matches!(value, WasmValue::Ref(RefValue::Extern(_))), Self::RefAny => matches!(value, WasmValue::Ref(RefValue::Any(_))), - Self::RefEq => { - store.value_matches_type(*value, WasmType::Ref(RefType::new_abstract(false, TinyAbstractHeapType::Eq))) - } + Self::RefEq => matches!(value, WasmValue::Ref(RefValue::Any(value)) if value.as_eq().is_some()), Self::RefI31 => matches!(value, WasmValue::Ref(RefValue::Any(value)) if value.as_i31().is_some()), - Self::RefStruct => store - .value_matches_type(*value, WasmType::Ref(RefType::new_abstract(false, TinyAbstractHeapType::Struct))), - Self::RefArray => store - .value_matches_type(*value, WasmType::Ref(RefType::new_abstract(false, TinyAbstractHeapType::Array))), + Self::RefStruct => matches!(value, WasmValue::Ref(RefValue::Any(value)) if value.as_struct().is_some()), + Self::RefArray => matches!(value, WasmValue::Ref(RefValue::Any(value)) if value.as_array().is_some()), + Self::RefExternExact(expected) => match value { + WasmValue::Ref(RefValue::Extern(value)) => value.key(store) == Ok(*expected), + _ => false, + }, + Self::RefHostExact(expected) => match value { + WasmValue::Ref(RefValue::Any(value)) => value.to_extern().key(store) == Ok(*expected), + _ => false, + }, + Self::RefFuncExact(expected) => module + .func_by_index(store, *expected) + .and_then(|function| function.as_func_ref(store)) + .is_ok_and(|expected| matches!(value, WasmValue::Ref(RefValue::Func(value)) if *value == expected)), } } } @@ -912,11 +921,9 @@ fn wastretcore2tinywasmvalue(ret: wast::core::WastRetCore) -> Result ExpectedValue::Exact(WasmValue::I64(i)), V128(i) => ExpectedValue::Exact(WasmValue::V128(wast_v128_to_bytes(i))), RefNull(_) => ExpectedValue::RefNull, - RefExtern(Some(v)) => ExpectedValue::Exact( - ExternRef::try_new(v).ok_or_else(|| anyhow!("external reference address is too large"))?.into(), - ), + RefExtern(Some(v)) => ExpectedValue::RefExternExact(v), RefExtern(None) => ExpectedValue::RefExtern, - RefFunc(Some(wast::token::Index::Num(n, _))) => ExpectedValue::Exact(FuncRef::new(n).into()), + RefFunc(Some(wast::token::Index::Num(n, _))) => ExpectedValue::RefFuncExact(n), RefFunc(None) => ExpectedValue::RefFunc, RefFunc(v) => { bail!("unsupported arg type: reffunc: {:?}", v); @@ -926,10 +933,7 @@ fn wastretcore2tinywasmvalue(ret: wast::core::WastRetCore) -> Result ExpectedValue::RefI31, RefStruct => ExpectedValue::RefStruct, RefArray => ExpectedValue::RefArray, - RefHost(value) => ExpectedValue::Exact( - RefValue::Any(AnyRef::from_host(value).ok_or_else(|| anyhow!("host reference address is too large"))?) - .into(), - ), + RefHost(value) => ExpectedValue::RefHostExact(value), a => { bail!("unsupported arg type {:?}", a); } diff --git a/crates/parser/src/conversion.rs b/crates/parser/src/conversion.rs index 8f39398..6857a19 100644 --- a/crates/parser/src/conversion.rs +++ b/crates/parser/src/conversion.rs @@ -342,12 +342,9 @@ pub(crate) fn process_const_operators( } let instr = match op { - wasmparser::Operator::RefNull { hty } => { - convert_heap_type(hty, false)?; - ConstInstruction::Ref(RefValue::Null) - } + wasmparser::Operator::RefNull { hty } => ConstInstruction::RefNull(convert_heap_type(hty, true)?), wasmparser::Operator::RefFunc { function_index } => { - ConstInstruction::Ref(RefValue::Func(FuncRef::new(function_index))) + ConstInstruction::RefFunc(ModuleFuncIdx::new(function_index)) } wasmparser::Operator::RefI31 => ConstInstruction::RefI31, wasmparser::Operator::AnyConvertExtern => ConstInstruction::AnyConvertExtern, diff --git a/crates/tinywasm/src/error.rs b/crates/tinywasm/src/error.rs index 05e36aa..a9c05e6 100644 --- a/crates/tinywasm/src/error.rs +++ b/crates/tinywasm/src/error.rs @@ -2,8 +2,10 @@ use alloc::boxed::Box; use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::fmt::{Debug, Display}; +use tinywasm_types::FuncType; use tinywasm_types::archive::TwasmError; -use tinywasm_types::{ExnRef, FuncType}; + +use crate::{ExnRef, WasmValue}; #[cfg(feature = "parser")] pub use tinywasm_parser::ParseError; @@ -31,7 +33,7 @@ pub enum Error { /// The expected type expected: Box, /// The actual value - actual: Vec, + actual: Vec, }, /// An invalid label type was encountered @@ -146,9 +148,12 @@ pub enum Trap { /// Invalid Integer Conversion InvalidConversionToInt, - /// The store is not the one that the module instance was instantiated in + /// A Store-owned handle or reference was used with a different Store. InvalidStore, + /// A reference does not identify a live value of the expected kind. + InvalidReference, + /// Integer Overflow IntegerOverflow, @@ -228,6 +233,7 @@ impl Trap { Self::IndirectCallTypeMismatch { .. } => "indirect call type mismatch", Self::HostFunction(_) => "host function trap", Self::InvalidStore => "invalid store", + Self::InvalidReference => "invalid reference", Self::Other(message) => message, } } @@ -337,6 +343,7 @@ impl Display for Trap { Self::NullI31Reference => write!(f, "null i31 reference"), Self::CastFailure => write!(f, "cast failure"), Self::InvalidStore => write!(f, "invalid store"), + Self::InvalidReference => write!(f, "invalid reference"), #[cfg(feature = "debug")] Self::IndirectCallTypeMismatch { expected, actual } => { write!(f, "indirect call type mismatch: expected={expected:?}, actual={actual:?}") diff --git a/crates/tinywasm/src/func/context.rs b/crates/tinywasm/src/func/context.rs index 95a2cec..3df522f 100644 --- a/crates/tinywasm/src/func/context.rs +++ b/crates/tinywasm/src/func/context.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; -use tinywasm_types::{ModuleInstanceId, WasmValue}; +use tinywasm_types::ModuleInstanceId; -use crate::{Error, FromWasmValues, Function, FunctionTyped, IntoWasmValues, Result}; +use crate::{Error, FromWasmValues, FuncRef, Function, FunctionTyped, IntoWasmValues, Result, WasmValue}; /// The context of a host-function call #[cfg_attr(feature = "debug", derive(core::fmt::Debug))] @@ -45,7 +45,7 @@ impl FuncContext<'_> { } /// Get the value of a global export. - pub fn global_get(&self, name: &str) -> Result { + pub fn global_get(&mut self, name: &str) -> Result { self.module().global_get(self.store, name) } @@ -97,6 +97,16 @@ impl FuncContext<'_> { func.call_untyped(self.store, args, call_stack_base, value_stack_base) } + /// Calls a Store-aware function reference in the current module context. + pub fn call_ref(&mut self, func: FuncRef, args: &[WasmValue]) -> Result> { + let addr = func.addr(self.store.store_id()).ok_or(crate::Trap::InvalidStore)?; + if self.store.state.funcs.get(addr as usize).is_none() { + return Err(crate::Trap::InvalidReference.into()); + } + let function = Function { item: crate::StoreItem::new(self.store.store_id(), addr), module_id: self.module_id }; + self.call_untyped(&function, args) + } + /// Call a typed function from within the current host-function invocation. /// /// See [`Self::call_untyped`] for reentrancy and resumable-execution @@ -115,12 +125,7 @@ impl FuncContext<'_> { let params = params.into_wasm_values().collect::>(); let results = self.call_untyped(&func.func, ¶ms)?; let mut values = results.into_iter(); - let result = R::from_wasm_values(&mut values)?; - return if values.next().is_none() { - Ok(result) - } else { - Err(Error::other("typed conversion did not consume all WebAssembly values")) - }; + return R::from_wasm_values_exact(&mut values); } let call_stack_base = self.store.call_stack.len(); @@ -142,10 +147,3 @@ impl core::ops::DerefMut for FuncContext<'_> { self.store } } - -impl<'a> FuncContext<'a> { - /// Create a new host function context. - pub const fn new(store: &'a mut crate::Store, module_id: ModuleInstanceId) -> Self { - Self { store, module_id } - } -} diff --git a/crates/tinywasm/src/func/host.rs b/crates/tinywasm/src/func/host.rs index 27c5573..d1c4605 100644 --- a/crates/tinywasm/src/func/host.rs +++ b/crates/tinywasm/src/func/host.rs @@ -1,8 +1,9 @@ use alloc::{boxed::Box, sync::Arc, vec::Vec}; -use tinywasm_types::{FuncType, ModuleInstanceId, TypeAddr, WasmType, WasmValue}; +use tinywasm_types::{FuncType, ModuleInstanceId, TypeAddr, WasmType}; use super::{FromWasmValues, FuncContext, IntoWasmValues, ToWasmTypes}; -use crate::{Function, FunctionInstance, Result, Store}; +use crate::store::FuncValueTypes; +use crate::{Function, FunctionInstance, Result, Store, WasmValue}; /// A reusable host function definition. #[derive(Clone)] @@ -11,12 +12,8 @@ pub struct HostFunction(Arc); impl HostFunction { /// Instantiates the function with an already registered canonical type. pub(crate) fn instantiate_registered(&self, store: &mut Store, type_addr: TypeAddr) -> Function { - let addr = store.add_func(FunctionInstance { - type_addr, - gc: store.state.func_gc_metadata(type_addr), - kind: crate::store::FunctionKind::Host(self.clone()), - }); - Function { item: crate::StoreItem::new(store.id(), addr), module_id: 0 } + let addr = store.add_func(FunctionInstance { type_addr, kind: crate::store::FunctionKind::Host(self.clone()) }); + Function { item: crate::StoreItem::new(store.store_id(), addr), module_id: 0 } } /// Resolves the importing module's types without allocating a function instance. @@ -49,10 +46,11 @@ impl HostFunction { let result = match &self.0.callback { HostCallback::Untyped(func) => func(FuncContext { store, module_id }, args), HostCallback::Typed(func) => func.call(FuncContext { store, module_id }, args), - }?; - let expected = store.state.get_canonical_func_type(type_addr); + }; + let expected = store.state.get_canonical_func_type(type_addr).clone(); + let result = result?; if result.len() == expected.results().len() - && result.iter().zip(expected.results()).all(|(&value, &ty)| store.state.value_matches_type(value, ty)) + && result.iter().zip(expected.results()).all(|(value, &ty)| store.value_matches_type(value, ty)) { Ok(result) } else { @@ -79,8 +77,9 @@ impl HostFunction { /// /// # Errors /// - /// Returns an error if the signature uses a store-specific reference type - /// that is not registered in `store`. + /// Returns an error if the signature contains a concrete reference type. + /// Define the function through [`crate::Imports`] when concrete types must + /// be resolved against an importing module. /// /// ## Example /// @@ -104,13 +103,9 @@ impl HostFunction { .params() .iter() .chain(self.0.ty.results()) - .filter_map(|ty| match ty { - WasmType::Ref(ty) => ty.type_index(), - _ => None, - }) - .any(|type_addr| type_addr as usize >= store.state.canonical_types.len()) + .any(|ty| matches!(ty, WasmType::Ref(ty) if ty.is_concrete())) { - return Err(crate::Error::other("host function signature contains a concrete type from another store")); + return Err(crate::Error::other("standalone host functions cannot use concrete reference types")); } let type_addr = store.register_host_type(&self.0.ty); Ok(self.instantiate_registered(store, type_addr)) @@ -226,29 +221,22 @@ where R: IntoWasmValues, { fn call(&self, ctx: FuncContext<'_>, args: &[WasmValue]) -> Result> { - let mut values = args.iter().copied(); - let params = P::from_wasm_values(&mut values)?; - if values.next().is_some() { - return Err(crate::Error::other("typed conversion did not consume all WebAssembly values")); - } + let mut values = args.iter().cloned(); + let params = P::from_wasm_values_exact(&mut values)?; Ok((self.func)(ctx, params)?.into_wasm_values().collect()) } fn call_stack(&self, store: &mut Store, module_id: ModuleInstanceId, type_addr: TypeAddr) -> Result<()> { - let params = store.state.get_canonical_func_type(type_addr).params(); - let base = store.value_stack.base_before(params.iter().collect()); - let mut values = store.value_stack.wasm_values(&store.state, params, base, true); - let params = P::from_wasm_values(&mut values).and_then(|params| { - if values.next().is_some() { - Err(crate::Error::other("typed conversion did not consume all WebAssembly values")) - } else { - Ok(params) - } - }); - drop(values); + let base = { + let params = store.state.get_canonical_func_type(type_addr).params(); + store.value_stack.base_before(params.iter().collect()) + }; + let params = { + let mut values = store.stack_value_iter(type_addr, FuncValueTypes::Params, base)?; + P::from_wasm_values_exact(&mut values) + }; store.value_stack.truncate_to_base(base); - let params = params?; - let result = (self.func)(FuncContext { store, module_id }, params)?; + let result = (self.func)(FuncContext { store, module_id }, params?)?; store.push_typed_values::(type_addr, result.into_wasm_values(), base) } } diff --git a/crates/tinywasm/src/func/mod.rs b/crates/tinywasm/src/func/mod.rs index 34a1938..81a3ec8 100644 --- a/crates/tinywasm/src/func/mod.rs +++ b/crates/tinywasm/src/func/mod.rs @@ -3,7 +3,9 @@ use crate::reference::StoreItem; use crate::{Error, FunctionInstance, InterpreterRuntime, Result, Store}; use alloc::{format, vec::Vec}; use core::hint::cold_path; -use tinywasm_types::{FuncAddr, FuncType, ModuleInstanceId, WasmValue}; +use tinywasm_types::{FuncAddr, FuncType, ModuleInstanceId}; + +use crate::{FuncRef, WasmValue}; mod context; mod host; @@ -12,8 +14,6 @@ mod values; pub use context::FuncContext; pub use host::HostFunction; pub use resume::{ExecProgress, FuncExecution, FuncExecutionTyped}; -#[allow(deprecated)] -pub use values::WasmTupleChain; pub use values::{FromWasmValues, IntoWasmValues, ToWasmType, ToWasmTypes}; /// A function handle @@ -25,6 +25,12 @@ pub struct Function { } impl Function { + /// Returns this function as a Store-aware WebAssembly function reference. + pub fn as_func_ref(&self, store: &Store) -> Result { + self.item.validate_store(store)?; + Ok(FuncRef::new(store.store_id(), self.addr())) + } + #[inline] /// Returns the function's address in its store. pub(crate) const fn addr(&self) -> FuncAddr { @@ -66,7 +72,13 @@ impl Function { ))); } - if !func_ty.params().iter().zip(params).all(|(ty, param)| store.state.value_matches_type(*param, *ty)) { + for param in params { + if matches!(param, WasmValue::Ref(_)) { + param.to_runtime(store)?; + } + } + + if !func_ty.params().iter().zip(params).all(|(ty, param)| store.value_matches_type(param, *ty)) { cold_path(); #[cfg(feature = "debug")] return Err(Error::Other(format!( @@ -90,8 +102,7 @@ impl Function { ) -> Result> { let instance = store.state.get_func(self.addr()); let type_addr = instance.type_addr; - let results_may_gc = instance.gc.results; - let result = match &instance.kind { + match &instance.kind { crate::store::FunctionKind::Host(host) => { let host = host.clone(); host.call_values(store, self.module_id, type_addr, params) @@ -99,21 +110,22 @@ impl Function { crate::store::FunctionKind::Wasm(wasm) => { let wasm_params = wasm.func.params; let wasm_locals = wasm.func.locals; - let locals_base = - store.value_stack.enter_wasm_call(params, wasm_params, wasm_locals, value_stack_base)?; + store + .push_wasm_values(params.iter().cloned()) + .inspect_err(|_| store.value_stack.truncate_to_base(value_stack_base))?; + let locals_base = store + .value_stack + .enter_locals(&wasm_params, &wasm_locals) + .inspect_err(|_| store.value_stack.truncate_to_base(value_stack_base))?; let callframe = CallFrame::new(self.addr(), locals_base, wasm_locals); InterpreterRuntime::exec(store, callframe, call_stack_base).inspect_err(|_| { store.call_stack.truncate_to(call_stack_base); store.value_stack.truncate_to_base(value_stack_base); })?; - let result_types = store.state.get_canonical_func_type(type_addr).results(); - Ok(store.value_stack.pop_wasmvalues(&store.state, result_types)) + let result_type = store.state.get_canonical_func_type(type_addr).clone(); + store.pop_stack_values(result_type.results()) } - }; - if results_may_gc && let Ok(values) = &result { - store.state.pin_host_values(values); } - result } fn prepare_typed( @@ -157,7 +169,7 @@ impl Function { store.value_stack.truncate_to_base(value_stack_base); })?; } - store.take_typed_results(instance.type_addr, value_stack_base, instance.gc.results) + store.take_typed_results(instance.type_addr, value_stack_base) } } @@ -181,12 +193,7 @@ impl FunctionTyped { let params = params.into_wasm_values().collect::>(); let result = self.func.call(store, ¶ms)?; let mut values = result.into_iter(); - let result = R::from_wasm_values(&mut values)?; - return if values.next().is_none() { - Ok(result) - } else { - Err(Error::other("typed conversion did not consume all WebAssembly values")) - }; + return R::from_wasm_values_exact(&mut values); } store.enter_execution()?; diff --git a/crates/tinywasm/src/func/resume.rs b/crates/tinywasm/src/func/resume.rs index 892aa05..ee9d2e1 100644 --- a/crates/tinywasm/src/func/resume.rs +++ b/crates/tinywasm/src/func/resume.rs @@ -1,9 +1,9 @@ use alloc::vec::Vec; -use tinywasm_types::{FuncAddr, TypeAddr, WasmValue}; +use tinywasm_types::{FuncAddr, TypeAddr}; use super::{FromWasmValues, Function, FunctionTyped, IntoWasmValues}; use crate::interpreter::stack::{CallFrame, StackBase}; -use crate::{Error, InterpreterRuntime, Result, Store}; +use crate::{Error, InterpreterRuntime, Result, Store, WasmValue}; #[derive(Clone, PartialEq, Eq)] /// Progress for fuel-limited function execution. @@ -29,7 +29,7 @@ enum FuncExecutionState { #[cfg_attr(feature = "debug", derive(core::fmt::Debug))] enum CallResult { - Stack { type_addr: TypeAddr, pin_refs: bool }, + Stack { type_addr: TypeAddr }, Values(Vec), } @@ -53,7 +53,6 @@ impl Function { ) -> Result> { self.item.validate_store(store)?; self.validate_params(store, params)?; - let results_may_gc = store.state.get_func(self.addr()).gc.results; store.enter_execution()?; let result: Result = (|| { @@ -68,12 +67,8 @@ impl Function { store.call_stack.clear(); store.value_stack.clear(); let locals = wasm_func.func.locals; - let locals_base = store.value_stack.enter_wasm_call( - params, - wasm_func.func.params, - locals, - StackBase::default(), - )?; + store.push_wasm_values(params.iter().cloned())?; + let locals_base = store.value_stack.enter_locals(&wasm_func.func.params, &locals)?; let callframe = CallFrame::new(self.addr(), locals_base, locals); Ok(FuncExecutionState::Running { callframe, root_func_addr: self.addr() }) @@ -83,9 +78,6 @@ impl Function { store.exit_execution(); let state = result?; - if results_may_gc && let FuncExecutionState::Completed(Some(CallResult::Values(values))) = &state { - store.state.pin_host_values(values); - } Ok(FuncExecution { store, state }) } } @@ -123,9 +115,8 @@ impl<'store> FuncExecution<'store> { crate::interpreter::ExecState::Completed => { let func = self.store.state.get_func(root_func_addr); let result_ty = func.type_addr; - let results_may_gc = func.gc.results; self.state = FuncExecutionState::Completed(None); - Ok(ExecProgress::Completed(CallResult::Stack { type_addr: result_ty, pin_refs: results_may_gc })) + Ok(ExecProgress::Completed(CallResult::Stack { type_addr: result_ty })) } crate::interpreter::ExecState::Suspended(callframe) => { let FuncExecutionState::Running { callframe: current, .. } = &mut self.state else { @@ -142,12 +133,9 @@ impl<'store> FuncExecution<'store> { run: impl FnOnce(&mut Store, CallFrame) -> Result, ) -> Result>> { match self.resume_raw(run)? { - ExecProgress::Completed(CallResult::Stack { type_addr, pin_refs }) => { - let types = self.store.state.get_canonical_func_type(type_addr).results(); - let values = self.store.value_stack.pop_wasmvalues(&self.store.state, types); - if pin_refs { - self.store.state.pin_host_values(&values); - } + ExecProgress::Completed(CallResult::Stack { type_addr }) => { + let ty = self.store.state.get_canonical_func_type(type_addr).clone(); + let values = self.store.pop_stack_values(ty.results())?; Ok(ExecProgress::Completed(values)) } ExecProgress::Completed(CallResult::Values(values)) => Ok(ExecProgress::Completed(values)), @@ -226,10 +214,7 @@ impl FunctionTyped { store.value_stack.clear(); match self.func.prepare_typed(store, &func, params.into_wasm_values(), StackBase::default())? { Some(callframe) => Ok(FuncExecutionState::Running { callframe, root_func_addr: self.func.addr() }), - None => Ok(FuncExecutionState::Completed(Some(CallResult::Stack { - type_addr: func.type_addr, - pin_refs: func.gc.results, - }))), + None => Ok(FuncExecutionState::Completed(Some(CallResult::Stack { type_addr: func.type_addr }))), } })(); store.exit_execution(); @@ -244,16 +229,12 @@ impl<'store, R: FromWasmValues> FuncExecutionTyped<'store, R> { run: impl FnOnce(&mut Store, CallFrame) -> Result, ) -> Result> { match self.execution.resume_raw(run)? { - ExecProgress::Completed(CallResult::Stack { type_addr, pin_refs }) => Ok(ExecProgress::Completed( - self.execution.store.take_typed_results(type_addr, StackBase::default(), pin_refs)?, - )), + ExecProgress::Completed(CallResult::Stack { type_addr }) => { + Ok(ExecProgress::Completed(self.execution.store.take_typed_results(type_addr, StackBase::default())?)) + } ExecProgress::Completed(CallResult::Values(values)) => { let mut values = values.into_iter(); - let result = R::from_wasm_values(&mut values)?; - if values.next().is_some() { - return Err(Error::other("typed conversion did not consume all WebAssembly values")); - } - Ok(ExecProgress::Completed(result)) + Ok(ExecProgress::Completed(R::from_wasm_values_exact(&mut values)?)) } ExecProgress::Suspended => Ok(ExecProgress::Suspended), } diff --git a/crates/tinywasm/src/func/values.rs b/crates/tinywasm/src/func/values.rs index 319b493..9e30b7f 100644 --- a/crates/tinywasm/src/func/values.rs +++ b/crates/tinywasm/src/func/values.rs @@ -1,6 +1,8 @@ use crate::{Error, Result}; -use alloc::{borrow::Cow, vec::Vec}; -use tinywasm_types::{ExternRef, FuncRef, WasmType, WasmValue}; +use alloc::borrow::Cow; +use tinywasm_types::WasmType; + +use crate::{AnyRef, ArrayRef, EqRef, ExnRef, ExternRef, FuncRef, I31Ref, StructRef, WasmValue}; /// Convert a Rust value or tuple into WebAssembly values. pub trait IntoWasmValues { @@ -12,6 +14,15 @@ pub trait IntoWasmValues { pub trait FromWasmValues: Sized { /// Read this value from a flattened WebAssembly value iterator. fn from_wasm_values(values: &mut impl Iterator) -> Result; + + /// Read one value and reject unconsumed iterator items. + fn from_wasm_values_exact(values: &mut impl Iterator) -> Result { + let result = Self::from_wasm_values(values)?; + if values.next().is_some() { + return Err(Error::other("typed conversion did not consume all WebAssembly values")); + } + Ok(result) + } } /// Describes the WebAssembly value types produced by a Rust value or tuple shape. @@ -117,70 +128,27 @@ impl_scalar_wasm_traits!( i64 => WasmType::I64, f32 => WasmType::F32, f64 => WasmType::F64, - FuncRef => WasmType::Ref(tinywasm_types::RefType::FUNCREF), - ExternRef => WasmType::Ref(tinywasm_types::RefType::EXTERNREF), + FuncRef => WasmType::Ref(tinywasm_types::RefType::FUNCREF.with_nullability(false)), + AnyRef => WasmType::Ref(tinywasm_types::RefType::new_abstract(false, tinywasm_types::AbstractHeapType::Any)), + EqRef => WasmType::Ref(tinywasm_types::RefType::new_abstract(false, tinywasm_types::AbstractHeapType::Eq)), + I31Ref => WasmType::Ref(tinywasm_types::RefType::new_abstract(false, tinywasm_types::AbstractHeapType::I31)), + StructRef => WasmType::Ref(tinywasm_types::RefType::new_abstract(false, tinywasm_types::AbstractHeapType::Struct)), + ArrayRef => WasmType::Ref(tinywasm_types::RefType::new_abstract(false, tinywasm_types::AbstractHeapType::Array)), + ExternRef => WasmType::Ref(tinywasm_types::RefType::EXTERNREF.with_nullability(false)), + ExnRef => WasmType::Ref(tinywasm_types::RefType::EXNREF.with_nullability(false)), ); -impl_tuple_traits!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20); - -/// Concatenates two typed parameter or result groups. -/// -/// Direct tuple conversions are supported up to arity 20. Use untyped functions -/// for larger signatures. -#[deprecated(note = "direct tuples are supported up to arity 20, use untyped functions for larger signatures")] -#[derive(Default)] -pub struct WasmTupleChain(T1, T2); - -#[allow(deprecated)] -impl WasmTupleChain { - /// Create a new concatenated tuple wrapper. - pub const fn new(left: T1, right: T2) -> Self { - Self(left, right) - } - - /// Split the wrapper back into its two component values. - pub fn into_inner(self) -> (T1, T2) { - (self.0, self.1) - } -} - -#[allow(deprecated)] -impl From<(T1, T2)> for WasmTupleChain { - fn from((left, right): (T1, T2)) -> Self { - Self::new(left, right) - } -} - -#[allow(deprecated)] -impl ToWasmTypes for WasmTupleChain { - const WASM_TYPES: Option<&'static [WasmType]> = None; - - #[inline] - fn wasm_types() -> Cow<'static, [WasmType]> { - let mut types = Vec::new(); - types.extend_from_slice(&T1::wasm_types()); - types.extend_from_slice(&T2::wasm_types()); - Cow::Owned(types) - } -} -#[allow(deprecated)] -impl IntoWasmValues for WasmTupleChain { - #[inline] - fn into_wasm_values(self) -> impl Iterator { - let (left, right) = self.into_inner(); - left.into_wasm_values().chain(right.into_wasm_values()) - } -} - -#[allow(deprecated)] -impl FromWasmValues for WasmTupleChain { - #[inline] - fn from_wasm_values(values: &mut impl Iterator) -> Result { - let left = T1::from_wasm_values(values)?; - let right = T2::from_wasm_values(values)?; - Ok(Self::new(left, right)) - } -} +impl_scalar_wasm_traits!( + Option => WasmType::Ref(tinywasm_types::RefType::FUNCREF), + Option => WasmType::Ref(tinywasm_types::RefType::new_abstract(true, tinywasm_types::AbstractHeapType::Any)), + Option => WasmType::Ref(tinywasm_types::RefType::new_abstract(true, tinywasm_types::AbstractHeapType::Eq)), + Option => WasmType::Ref(tinywasm_types::RefType::new_abstract(true, tinywasm_types::AbstractHeapType::I31)), + Option => WasmType::Ref(tinywasm_types::RefType::new_abstract(true, tinywasm_types::AbstractHeapType::Struct)), + Option => WasmType::Ref(tinywasm_types::RefType::new_abstract(true, tinywasm_types::AbstractHeapType::Array)), + Option => WasmType::Ref(tinywasm_types::RefType::EXTERNREF), + Option => WasmType::Ref(tinywasm_types::RefType::EXNREF), +); +impl_tuple_traits!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20); impl ToWasmTypes for () { const WASM_TYPES: Option<&'static [WasmType]> = Some(&[]); diff --git a/crates/tinywasm/src/imports.rs b/crates/tinywasm/src/imports.rs index eb2351b..6859bbf 100644 --- a/crates/tinywasm/src/imports.rs +++ b/crates/tinywasm/src/imports.rs @@ -66,17 +66,20 @@ impl_conv! { /// Ok(()) /// }); /// -/// let table = Table::new( +/// let table = Table::try_new( /// &mut store, /// TableType::new(tinywasm::types::RefType::FUNCREF, 10, Some(20)), /// tinywasm::types::RefValue::Null.into(), /// )?; -/// let memory = Memory::new( +/// let memory = Memory::try_new( /// &mut store, /// MemoryType::default().with_page_count_initial(1).with_page_count_max(Some(2)), /// )?; -/// let global_i32 = -/// Global::new(&mut store, GlobalType::default().with_ty(WasmType::I32), WasmValue::I32(666))?; +/// let global_i32 = Global::try_new( +/// &mut store, +/// GlobalType::default().with_ty(WasmType::I32), +/// WasmValue::I32(666), +/// )?; /// /// imports /// .define("my_module", "print_i32", print_i32) diff --git a/crates/tinywasm/src/instance.rs b/crates/tinywasm/src/instance.rs index 6700d53..7ad3cf5 100644 --- a/crates/tinywasm/src/instance.rs +++ b/crates/tinywasm/src/instance.rs @@ -3,8 +3,11 @@ use core::hint::cold_path; use tinywasm_types::*; use crate::func::{FromWasmValues, IntoWasmValues, ToWasmTypes}; +use crate::reference::StoreId; use crate::store::MemoryInstance; -use crate::{Error, Function, FunctionTyped, Global, Imports, Memory, Result, Store, StoreItem, Table, Tag, Trap}; +use crate::{ + Error, Function, FunctionTyped, Global, Imports, Memory, Result, Store, StoreItem, Table, Tag, Trap, WasmValue, +}; /// A typed view over an exported extern value. pub enum ExternItem { @@ -47,7 +50,7 @@ pub struct ModuleInstance(Rc); #[cfg_attr(feature = "debug", derive(Debug))] struct ModuleInstanceInner { - store_id: u32, + store_id: StoreId, id: ModuleInstanceId, type_addrs: Box<[TypeAddr]>, func_addrs: Box<[FuncAddr]>, @@ -121,7 +124,7 @@ impl ModuleInstance { #[inline] pub(crate) fn validate_store(&self, store: &Store) -> Result<()> { - if self.0.store_id != store.id() { + if self.0.store_id != store.store_id() { return cold!(Err(Trap::InvalidStore.into())); } Ok(()) @@ -160,9 +163,9 @@ impl ModuleInstance { /// let mut store = Store::default(); /// let instance = ModuleInstance::instantiate_no_start(&mut store, &module, None)?; /// - /// assert_eq!(instance.global_get(&store, "g")?, 0.into()); + /// assert_eq!(instance.global_get(&mut store, "g")?, 0.into()); /// instance.start(&mut store)?; - /// assert_eq!(instance.global_get(&store, "g")?, 42.into()); + /// assert_eq!(instance.global_get(&mut store, "g")?, 42.into()); /// # Ok(()) /// # } /// ``` @@ -189,7 +192,7 @@ impl ModuleInstance { store.init_data(&addrs.memories, &addrs.globals, &addrs.funcs, &module.data, &type_addrs)?; let instance = ModuleInstanceInner { - store_id: store.id(), + store_id: store.store_id(), id, type_addrs, func_addrs: addrs.funcs.into_boxed_slice(), @@ -213,7 +216,7 @@ impl ModuleInstance { } /// Get a export by name - pub fn export_addr(&self, name: &str) -> Option { + pub(crate) fn export_addr(&self, name: &str) -> Option { let export = self.0.exports.iter().find(|e| *e.name == *name)?; let addr = match export.kind { ExternalKind::Func => self.0.func_addrs.get(export.index as usize)?, @@ -312,7 +315,7 @@ impl ModuleInstance { /// let ExternItem::Global(global) = instance.extern_item("answer")? else { /// panic!("expected global export"); /// }; - /// assert_eq!(global.get(&store)?, 42.into()); + /// assert_eq!(global.get(&mut store)?, 42.into()); /// # Ok(()) /// # } /// ``` @@ -516,7 +519,7 @@ impl ModuleInstance { } /// Get the value of a global export by name. - pub fn global_get(&self, store: &Store, name: &str) -> Result { + pub fn global_get(&self, store: &mut Store, name: &str) -> Result { self.global(name)?.get(store) } @@ -601,9 +604,9 @@ impl ModuleInstance { /// let mut store = Store::default(); /// let instance = ModuleInstance::instantiate_no_start(&mut store, &module, None)?; /// - /// assert_eq!(instance.global_get(&store, "g")?, 0.into()); + /// assert_eq!(instance.global_get(&mut store, "g")?, 0.into()); /// assert_eq!(instance.start(&mut store)?, Some(())); - /// assert_eq!(instance.global_get(&store, "g")?, 7.into()); + /// assert_eq!(instance.global_get(&mut store, "g")?, 7.into()); /// # Ok(()) /// # } /// ``` diff --git a/crates/tinywasm/src/interpreter/executor.rs b/crates/tinywasm/src/interpreter/executor.rs index a50c1fa..c8aa188 100644 --- a/crates/tinywasm/src/interpreter/executor.rs +++ b/crates/tinywasm/src/interpreter/executor.rs @@ -913,58 +913,46 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { self.cf.instr_ptr = target_ip as usize; } - fn create_exception(&mut self, tag_index: TagAddr) -> Result { + fn create_exception(&mut self, tag_index: TagAddr) -> Result { let tag_addr = self.module.resolve_tag_addr(tag_index); let type_addr = self.store.state.get_tag(tag_addr).type_addr; - let addr = cold_err!(u32::try_from(self.store.state.exceptions.len())).map_err(|_| Trap::OutOfMemory)?; - cold_err!(self.store.state.exceptions.try_reserve(1)).map_err(|_| Trap::OutOfMemory)?; - let params = self.store.state.get_canonical_func_type(type_addr).params(); + let payload_len = self.store.state.get_canonical_func_type(type_addr).params().len(); + self.store.state.gc.check_allocation(payload_len, true)?; let mut payload = Vec::new(); - cold_err!(payload.try_reserve_exact(params.len())).map_err(|_| Trap::OutOfMemory)?; + cold_err!(payload.try_reserve_exact(payload_len)).map_err(|_| Trap::OutOfMemory)?; let value_stack = &mut self.store.value_stack; - for &ty in params.iter().rev() { + for index in (0..payload_len).rev() { + let ty = self.store.state.get_canonical_func_type(type_addr).params()[index]; payload.push(match ty { - WasmType::I32 | WasmType::F32 => TinyWasmValue::Value32(Value32::stack_pop(value_stack)), - WasmType::I64 | WasmType::F64 => TinyWasmValue::Value64(Value64::stack_pop(value_stack)), - WasmType::V128 => TinyWasmValue::Value128(Value128::stack_pop(value_stack)), - WasmType::Ref(_) => TinyWasmValue::ValueRef(ValueRef::stack_pop(value_stack)), + WasmType::I32 | WasmType::F32 => RuntimeValue::Value32(Value32::stack_pop(value_stack)), + WasmType::I64 | WasmType::F64 => RuntimeValue::Value64(Value64::stack_pop(value_stack)), + WasmType::V128 => RuntimeValue::Value128(Value128::stack_pop(value_stack)), + WasmType::Ref(_) => RuntimeValue::ValueRef(ValueRef::stack_pop(value_stack)), }); } payload.reverse(); - let exception = crate::store::ExceptionInstance { tag_addr, payload: payload.into_boxed_slice() }; - self.store.state.exceptions.push(exception); - Ok(addr) + let roots = (&self.store.value_stack.stack_32).into_iter().copied().map(ValueRef::from_raw); + self.store.state.alloc_exception(tag_addr, payload, roots) } fn exec_throw(&mut self, tag_index: TagAddr) -> Result<()> { let exception = self.create_exception(tag_index)?; - let outcome = match self.dispatch_exception(exception) { - Ok(outcome) => outcome, - Err(trap) => { - _ = self.store.state.exceptions.pop(); - return Err(trap.into()); - } - }; - match outcome { - Some(catch) if !catch.with_ref() => { - debug_assert_eq!(self.store.state.exceptions.len() - 1, exception as usize); - _ = self.store.state.exceptions.pop(); - Ok(()) - } - Some(_) => Ok(()), - None => Err(Error::Exception(ExnRef::new(exception))), - } + self.throw_exception(exception) } fn exec_throw_ref(&mut self) -> Result<()> { let exception = ValueRef::stack_pop(&mut self.store.value_stack); - let exception = exception - .addr() - .filter(|addr| self.store.state.exceptions.get(*addr as usize).is_some()) - .ok_or(Trap::NullReference)?; - match self.dispatch_exception(exception)? { - Some(_) => Ok(()), - None => Err(Error::Exception(ExnRef::new(exception))), + if exception.is_null() { + return Err(Trap::NullReference.into()); + } + self.throw_exception(exception) + } + + fn throw_exception(&mut self, exception: ValueRef) -> Result<()> { + match self.dispatch_exception(exception) { + Ok(Some(_)) => Ok(()), + Ok(None) => self.store.root_exception(exception).and_then(|exception| Err(Error::Exception(exception))), + Err(trap) => Err(trap.into()), } } @@ -995,8 +983,11 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { } } - fn dispatch_exception(&mut self, exception_addr: ExnAddr) -> Result, Trap> { - let tag_addr = self.store.state.exceptions[exception_addr as usize].tag_addr; + fn dispatch_exception(&mut self, exception: ValueRef) -> Result, Trap> { + let object = self.store.state.gc.get(exception).ok_or(Trap::InvalidReference)?; + let crate::store::GcObjectKind::Exception(tag_addr) = object.kind else { + return Err(Trap::InvalidReference); + }; let mut protected_ip = self.cf.instr_ptr; loop { if let Some(catch) = self.matching_catch(protected_ip, tag_addr) { @@ -1013,12 +1004,13 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { self.store.value_stack.truncate_to_base(target); if include_payload { let Store { state, value_stack, .. } = self.store; - for value in state.exceptions[exception_addr as usize].payload.iter().copied() { + let object = state.gc.get(exception).ok_or(Trap::InvalidReference)?; + for value in object.values.iter().copied() { value_stack.push_dyn(value)?; } } if with_ref { - self.store.value_stack.push(ValueRef::from_category_addr(exception_addr))?; + self.store.value_stack.push(exception)?; } self.cf.instr_ptr = landing_pad as usize; return Ok(Some(catch)); @@ -1078,12 +1070,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { Ok(()) } - fn exec_call_host( - &mut self, - host_func: HostFunction, - type_addr: TypeAddr, - params_may_gc: bool, - ) -> Result { + fn exec_call_host(&mut self, host_func: HostFunction, type_addr: TypeAddr) -> Result { if let Some(host_func) = host_func.typed_callback() { cold_err!(host_func.call_stack(self.store, self.module.id(), type_addr)) .map_err(|error| Trap::HostFunction(Box::new(error)))?; @@ -1094,23 +1081,32 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { return Ok(false); } - let param_types = self.store.state.get_canonical_func_type(type_addr).params(); + let (param_count, base) = { + let param_types = self.store.state.get_canonical_func_type(type_addr).params(); + (param_types.len(), self.store.value_stack.base_before(param_types.iter().collect())) + }; let mut params = core::mem::take(&mut self.store.host_params); debug_assert!(params.is_empty()); - cold_err!(params.try_reserve_exact(param_types.len())).map_err(|_| Trap::OutOfMemory)?; - for &ty in param_types.iter().rev() { - params.push(self.store.value_stack.pop_wasmvalue(&self.store.state, ty)); - } - params.reverse(); - if params_may_gc { - self.store.state.pin_host_values(¶ms); + if cold_err!(params.try_reserve_exact(param_count)).is_err() { + self.store.host_params = params; + return Err(Trap::OutOfMemory); } + let host_values = self.store.stack_value_iter(type_addr, crate::store::FuncValueTypes::Params, base)?; + params.extend(host_values); + self.store.value_stack.truncate_to_base(base); let result = host_func.call_values(self.store, self.module.id(), type_addr, ¶ms); params.clear(); self.store.host_params = params; - let res = cold_err!(result).map_err(|error| Trap::HostFunction(Box::new(error)))?; + let res = match result { + Ok(values) => values, + Err(error) => return Err(Trap::HostFunction(Box::new(error))), + }; - self.store.value_stack.extend_wasmvalues(res.iter().copied())?; + let push_result = self.store.push_wasm_values(res); + push_result.map_err(|error| match error { + Error::Trap(trap) => trap, + other => Trap::HostFunction(Box::new(other)), + })?; if TAIL { Ok(self.exec_return()) } else { @@ -1128,7 +1124,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { self.exec_call(wasm_func.func.clone(), wasm_func.owner, addr) } crate::store::FunctionKind::Host(host_func) => { - self.exec_call_host::(host_func.clone(), func.type_addr, func.gc.params)?; + self.exec_call_host::(host_func.clone(), func.type_addr)?; Ok(()) } } @@ -1144,7 +1140,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { Ok(false) } crate::store::FunctionKind::Host(host_func) => { - self.exec_call_host::(host_func.clone(), func.type_addr, func.gc.params) + self.exec_call_host::(host_func.clone(), func.type_addr) } } } @@ -1174,7 +1170,6 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { fn exec_call_indirect(&mut self, index: OperandIdx) -> Result { let TwoU32 { first: type_addr, second: table_addr } = index.get(&self.func.data); self.charge_call_fuel(FUEL_COST_CALL_TOTAL); - // verify that the table is of the right type, this should be validated by the parser already let table_addr = self.module.resolve_table_addr(table_addr); let table_idx = self.pop_table_operand(self.store.state.get_table(table_addr).kind.arch())?; @@ -1209,7 +1204,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { false => self.exec_call(wasm_func.func.clone(), wasm_func.owner, func_addr), }, crate::store::FunctionKind::Host(host_func) => { - return self.exec_call_host::(host_func.clone(), func.type_addr, func.gc.params); + return self.exec_call_host::(host_func.clone(), func.type_addr); } }?; Ok(false) @@ -1438,22 +1433,34 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { self.store.value_stack.push(value) } - fn push_gc_object(&mut self, type_addr: TypeAddr, values: Vec) -> Result<(), Trap> { - let roots = (&self.store.value_stack.stack_32).into_iter().copied(); + fn push_gc_object(&mut self, type_addr: TypeAddr, values: Vec) -> Result<(), Trap> { + let roots = (&self.store.value_stack.stack_32).into_iter().copied().map(ValueRef::from_raw); let reference = self.store.state.alloc_gc_object(type_addr, values, roots)?; self.store.value_stack.push(reference) } fn exec_struct_new(&mut self, type_index: TypeAddr, default: bool) -> Result<(), Trap> { let type_addr = self.module.resolve_type_addr(type_index); - let fields = &self.store.state.get_type(type_addr).as_struct().expect("validated struct.new type").fields; + let field_count = + self.store.state.get_type(type_addr).as_struct().expect("validated struct.new type").fields.len(); + self.store.state.check_gc_allocation(type_addr, field_count)?; let mut values = Vec::new(); - cold_err!(values.try_reserve_exact(fields.len())).map_err(|_| Trap::OutOfMemory)?; + cold_err!(values.try_reserve_exact(field_count)).map_err(|_| Trap::OutOfMemory)?; if default { - values.extend(fields.iter().map(|field| default_value(field.storage))); + values.extend( + self.store + .state + .get_type(type_addr) + .as_struct() + .expect("validated struct.new type") + .fields + .iter() + .map(|field| default_value(field.storage)), + ); } else { - for field in fields.iter().rev() { - values.push(pop_value(&mut self.store.value_stack, field.storage)); + for index in (0..field_count).rev() { + let storage = self.store.state.get_type(type_addr).as_struct().unwrap().fields[index].storage; + values.push(pop_value(&mut self.store.value_stack, storage)); } values.reverse(); } @@ -1468,6 +1475,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { [field_index as usize] .storage; let object = self.store.state.gc_object(reference, type_addr)?; + let object = self.store.state.gc.get_handle(object).ok_or(Trap::Other("invalid GC reference"))?; let value = *object.values.get(field_index as usize).expect("validated struct field index"); push_value(&mut self.store.value_stack, value, storage, signed) } @@ -1480,8 +1488,8 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { .storage; let value = pop_value(&mut self.store.value_stack, storage); let reference = ValueRef::stack_pop(&mut self.store.value_stack); - self.store.state.gc_object(reference, type_addr)?; - self.store.state.gc.set(reference, field_index as usize, value).expect("live struct field"); + let object = self.store.state.gc_object(reference, type_addr)?; + self.store.state.gc.set(object, field_index as usize, value).expect("live struct field"); Ok(()) } @@ -1489,6 +1497,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let type_addr = self.module.resolve_type_addr(type_index); let storage = self.store.state.get_type(type_addr).as_array().expect("validated array.new type").field.storage; let len = u32::stack_pop(&mut self.store.value_stack) as usize; + self.store.state.check_gc_allocation(type_addr, len)?; let value = if default { default_value(storage) } else { pop_value(&mut self.store.value_stack, storage) }; let mut values = Vec::new(); cold_err!(values.try_reserve_exact(len)).map_err(|_| Trap::OutOfMemory)?; @@ -1502,6 +1511,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let storage = self.store.state.get_type(type_addr).as_array().expect("validated array.new_fixed type").field.storage; let len = len as usize; + self.store.state.check_gc_allocation(type_addr, len)?; let mut values = Vec::new(); cold_err!(values.try_reserve_exact(len)).map_err(|_| Trap::OutOfMemory)?; for _ in 0..len { @@ -1517,6 +1527,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let type_addr = self.module.resolve_type_addr(type_index); let storage = self.store.state.get_type(type_addr).as_array().expect("validated array.get type").field.storage; let object = self.store.state.gc_object(reference, type_addr)?; + let object = self.store.state.gc.get_handle(object).ok_or(Trap::Other("invalid GC reference"))?; let value = *object.values.get(index).ok_or(Trap::ArrayOutOfBounds)?; push_value(&mut self.store.value_stack, value, storage, signed) } @@ -1527,8 +1538,8 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let value = pop_value(&mut self.store.value_stack, storage); let index = u32::stack_pop(&mut self.store.value_stack) as usize; let reference = ValueRef::stack_pop(&mut self.store.value_stack); - self.store.state.gc_object(reference, type_addr)?; - self.store.state.gc.set(reference, index, value).ok_or(Trap::ArrayOutOfBounds) + let object = self.store.state.gc_object(reference, type_addr)?; + self.store.state.gc.set(object, index, value).ok_or(Trap::ArrayOutOfBounds) } fn exec_array_len(&mut self) -> Result<(), Trap> { @@ -1537,7 +1548,10 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { return Err(Trap::NullArrayReference); } let object = self.store.state.gc.get(reference).ok_or(Trap::Other("invalid GC reference"))?; - if self.store.state.get_type(object.type_addr).as_array().is_none() { + let crate::store::GcObjectKind::Composite(type_addr) = object.kind else { + return Err(Trap::Other("GC reference is not an array")); + }; + if self.store.state.get_type(type_addr).as_array().is_none() { return Err(Trap::Other("GC reference is not an array")); } self.store.value_stack.push(object.values.len() as i32) @@ -1551,8 +1565,9 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let index = u32::stack_pop(&mut self.store.value_stack) as usize; let reference = ValueRef::stack_pop(&mut self.store.value_stack); let object = self.store.state.gc_object(reference, type_addr)?; - let end = index.checked_add(len).filter(|end| *end <= object.values.len()).ok_or(Trap::ArrayOutOfBounds)?; - self.store.state.gc.fill(reference, index..end, value).expect("live array range"); + let object_ref = self.store.state.gc.get_handle(object).ok_or(Trap::Other("invalid GC reference"))?; + let end = index.checked_add(len).filter(|end| *end <= object_ref.values.len()).ok_or(Trap::ArrayOutOfBounds)?; + self.store.state.gc.fill(object, index..end, value).expect("live array range"); Ok(()) } @@ -1565,16 +1580,22 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let dst = ValueRef::stack_pop(&mut self.store.value_stack); let dst_type = self.module.resolve_type_addr(dst_type); let src_type = self.module.resolve_type_addr(src_type); - let dst_len = self.store.state.gc_object(dst, dst_type)?.values.len(); - let src_object = self.store.state.gc_object(src, src_type)?; + let dst_handle = self.store.state.gc_object(dst, dst_type)?; + let dst_len = self.store.state.gc.get_handle(dst_handle).expect("validated array").values.len(); + let src_handle = self.store.state.gc_object(src, src_type)?; + let src_object = self.store.state.gc.get_handle(src_handle).expect("validated array"); let src_end = src_index.checked_add(len).filter(|end| *end <= src_object.values.len()).ok_or(Trap::ArrayOutOfBounds)?; dst_index.checked_add(len).filter(|end| *end <= dst_len).ok_or(Trap::ArrayOutOfBounds)?; - if src == dst { - self.store.state.gc.copy_within(dst, src_index..src_end, dst_index).expect("live array range"); + if src_handle == dst_handle { + self.store.state.gc.copy_within(dst_handle, src_index..src_end, dst_index).expect("live array range"); return Ok(()); } - self.store.state.gc.copy_between(src, src_index..src_end, dst, dst_index).expect("live array ranges"); + self.store + .state + .gc + .copy_between(src_handle, src_index..src_end, dst_handle, dst_index) + .expect("live array ranges"); Ok(()) } @@ -1584,6 +1605,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let src = u32::stack_pop(&mut self.store.value_stack) as usize; let type_addr = self.module.resolve_type_addr(type_index); let storage = self.store.state.get_type(type_addr).as_array().expect("validated array type").field.storage; + self.store.state.check_gc_allocation(type_addr, len)?; let data_addr = self.module.resolve_data_addr(data_index); let data = self.store.state.data[data_addr as usize].data.as_deref().unwrap_or(&[]); let values = decode_data(storage, data, src, len)?; @@ -1594,12 +1616,13 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let TwoU32 { first: type_index, second: elem_index } = index.get(&self.func.data); let len = u32::stack_pop(&mut self.store.value_stack) as usize; let src = u32::stack_pop(&mut self.store.value_stack) as usize; + let type_addr = self.module.resolve_type_addr(type_index); + self.store.state.check_gc_allocation(type_addr, len)?; let elem_addr = self.module.resolve_elem_addr(elem_index); let items = self.store.state.elements[elem_addr as usize].items_range(src, len)?; let mut values = Vec::new(); cold_err!(values.try_reserve_exact(len)).map_err(|_| Trap::OutOfMemory)?; - values.extend(items.iter().copied().map(TinyWasmValue::ValueRef)); - let type_addr = self.module.resolve_type_addr(type_index); + values.extend(items.iter().copied().map(RuntimeValue::ValueRef)); self.push_gc_object(type_addr, values) } @@ -1611,12 +1634,13 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let reference = ValueRef::stack_pop(&mut self.store.value_stack); let type_addr = self.module.resolve_type_addr(type_index); let storage = self.store.state.get_type(type_addr).as_array().expect("validated array type").field.storage; - let object_len = self.store.state.gc_object(reference, type_addr)?.values.len(); + let object = self.store.state.gc_object(reference, type_addr)?; + let object_len = self.store.state.gc.get_handle(object).expect("validated array").values.len(); dst.checked_add(len).filter(|end| *end <= object_len).ok_or(Trap::ArrayOutOfBounds)?; let data = self.store.state.data[self.module.resolve_data_addr(data_index) as usize].data.as_deref().unwrap_or(&[]); let values = decode_data(storage, data, src, len)?; - self.store.state.gc.set_slice(reference, dst, &values).expect("live array range"); + self.store.state.gc.set_slice(object, dst, &values).expect("live array range"); Ok(()) } @@ -1627,14 +1651,15 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let dst = u32::stack_pop(&mut self.store.value_stack) as usize; let reference = ValueRef::stack_pop(&mut self.store.value_stack); let type_addr = self.module.resolve_type_addr(type_index); - let object_len = self.store.state.gc_object(reference, type_addr)?.values.len(); + let object = self.store.state.gc_object(reference, type_addr)?; + let object_len = self.store.state.gc.get_handle(object).expect("validated array").values.len(); dst.checked_add(len).filter(|end| *end <= object_len).ok_or(Trap::ArrayOutOfBounds)?; let items = self.store.state.elements[self.module.resolve_elem_addr(elem_index) as usize].items_range(src, len)?; let mut values = Vec::new(); cold_err!(values.try_reserve_exact(len)).map_err(|_| Trap::OutOfMemory)?; - values.extend(items.iter().copied().map(TinyWasmValue::ValueRef)); - self.store.state.gc.set_slice(reference, dst, &values).expect("live array range"); + values.extend(items.iter().copied().map(RuntimeValue::ValueRef)); + self.store.state.gc.set_slice(object, dst, &values).expect("live array range"); Ok(()) } diff --git a/crates/tinywasm/src/interpreter/stack/value_stack.rs b/crates/tinywasm/src/interpreter/stack/value_stack.rs index 62a9480..e123a41 100644 --- a/crates/tinywasm/src/interpreter/stack/value_stack.rs +++ b/crates/tinywasm/src/interpreter/stack/value_stack.rs @@ -1,6 +1,6 @@ -use alloc::{vec, vec::Vec}; +use alloc::vec::Vec; use core::hint::cold_path; -use tinywasm_types::{MemoryArch, ValueCounts, WasmType, WasmValue}; +use tinywasm_types::{MemoryArch, ValueCounts}; use super::StackBase; use crate::engine::{Config, StackConfig}; @@ -18,66 +18,6 @@ pub(crate) struct ValueStack { pub(crate) stack_128: Stack, } -struct WasmValues<'a> { - stack: &'a ValueStack, - state: &'a crate::store::State, - types: core::slice::Iter<'a, WasmType>, - index: StackBase, - pin_refs: bool, -} - -impl Iterator for WasmValues<'_> { - type Item = WasmValue; - - fn next(&mut self) -> Option { - let value = match *self.types.next()? { - WasmType::I32 => { - let value = *self.stack.stack_32.get(self.index.s32 as usize) as i32; - self.index.s32 += 1; - WasmValue::I32(value) - } - WasmType::I64 => { - let value = *self.stack.stack_64.get(self.index.s64 as usize) as i64; - self.index.s64 += 1; - WasmValue::I64(value) - } - WasmType::F32 => { - let value = f32::from_bits(*self.stack.stack_32.get(self.index.s32 as usize)); - self.index.s32 += 1; - WasmValue::F32(value) - } - WasmType::F64 => { - let value = f64::from_bits(*self.stack.stack_64.get(self.index.s64 as usize)); - self.index.s64 += 1; - WasmValue::F64(value) - } - WasmType::Ref(ty) => { - let value = - self.state.to_ref_value(ValueRef::from_raw(*self.stack.stack_32.get(self.index.s32 as usize)), ty); - self.index.s32 += 1; - WasmValue::Ref(value) - } - WasmType::V128 => { - let value = self.stack.stack_128.get(self.index.s128 as usize).0; - self.index.s128 += 1; - WasmValue::V128(value) - } - }; - if self.pin_refs - && let WasmValue::Ref(value) = value - { - self.state.pin_host_ref(value); - } - Some(value) - } - - fn size_hint(&self) -> (usize, Option) { - self.types.size_hint() - } -} - -impl ExactSizeIterator for WasmValues<'_> {} - #[cfg_attr(feature = "debug", derive(Debug))] pub(crate) struct Stack { data: Vec, @@ -281,11 +221,6 @@ impl ValueStack { } } - #[inline(always)] - pub(crate) fn len(&self) -> usize { - self.stack_32.len() + self.stack_64.len() + self.stack_128.len() - } - #[inline(always)] pub(crate) fn push(&mut self, value: T) -> Result<(), Trap> { T::stack_push(self, value) @@ -325,19 +260,6 @@ impl ValueStack { Ok(StackBase { s32: locals_base32, s64: locals_base64, s128: locals_base128 }) } - #[inline] - /// Pushes call arguments and allocates the function's local lanes. - pub(crate) fn enter_wasm_call( - &mut self, - values: &[WasmValue], - params: ValueCounts, - locals: ValueCounts, - base: StackBase, - ) -> Result { - self.extend_wasmvalues(values.iter().copied()).inspect_err(|_| self.truncate_to_base(base))?; - self.enter_locals(¶ms, &locals).inspect_err(|_| self.truncate_to_base(base)) - } - #[inline(always)] pub(crate) fn truncate_keep_counts(&mut self, base: StackBase, keep: ValueCounts) { self.stack_32.truncate_keep(base.s32 as usize, keep.c32 as usize); @@ -352,57 +274,12 @@ impl ValueStack { self.stack_128.truncate_to(base.s128 as usize); } - pub(crate) fn push_dyn(&mut self, value: TinyWasmValue) -> Result<(), Trap> { + pub(crate) fn push_dyn(&mut self, value: RuntimeValue) -> Result<(), Trap> { match value { - TinyWasmValue::Value32(v) => self.stack_32.push(v), - TinyWasmValue::Value64(v) => self.stack_64.push(v), - TinyWasmValue::Value128(v) => self.stack_128.push(v), - TinyWasmValue::ValueRef(v) => self.stack_32.push(v.raw()), - } - } - - pub(crate) fn pop_wasmvalue(&mut self, state: &crate::store::State, val_type: WasmType) -> WasmValue { - match val_type { - WasmType::I32 => WasmValue::I32(self.stack_32.pop() as i32), - WasmType::I64 => WasmValue::I64(self.stack_64.pop() as i64), - WasmType::F32 => WasmValue::F32(f32::from_bits(self.stack_32.pop())), - WasmType::F64 => WasmValue::F64(f64::from_bits(self.stack_64.pop())), - WasmType::Ref(ty) => WasmValue::Ref(state.to_ref_value(ValueRef::from_raw(self.stack_32.pop()), ty)), - WasmType::V128 => WasmValue::V128(self.stack_128.pop().0), - } - } - - /// Pops values in their logical WebAssembly order. - pub(crate) fn pop_wasmvalues(&mut self, state: &crate::store::State, types: &[WasmType]) -> Vec { - debug_assert!(self.len() >= types.len()); - let mut values = vec![WasmValue::I32(0); types.len()]; - for (index, &ty) in types.iter().enumerate().rev() { - values[index] = self.pop_wasmvalue(state, ty); + RuntimeValue::Value32(v) => self.stack_32.push(v), + RuntimeValue::Value64(v) => self.stack_64.push(v), + RuntimeValue::Value128(v) => self.stack_128.push(v), + RuntimeValue::ValueRef(v) => self.stack_32.push(v.raw()), } - values - } - - pub(crate) fn wasm_values<'a>( - &'a self, - state: &'a crate::store::State, - types: &'a [WasmType], - index: StackBase, - pin_refs: bool, - ) -> impl ExactSizeIterator + 'a { - WasmValues { stack: self, state, types: types.iter(), index, pin_refs } - } - - pub(crate) fn extend_wasmvalues(&mut self, values: impl Iterator) -> Result<(), Trap> { - for value in values { - match value { - WasmValue::I32(v) => self.stack_32.push(v as u32)?, - WasmValue::I64(v) => self.stack_64.push(v as u64)?, - WasmValue::F32(v) => self.stack_32.push(v.to_bits())?, - WasmValue::F64(v) => self.stack_64.push(v.to_bits())?, - WasmValue::Ref(v) => self.stack_32.push(ValueRef::from(v).raw())?, - WasmValue::V128(v) => self.stack_128.push(Value128(v))?, - } - } - Ok(()) } } diff --git a/crates/tinywasm/src/interpreter/values.rs b/crates/tinywasm/src/interpreter/values.rs index 2e0793b..22f20b7 100644 --- a/crates/tinywasm/src/interpreter/values.rs +++ b/crates/tinywasm/src/interpreter/values.rs @@ -1,7 +1,7 @@ use super::stack::{CallFrame, ValueStack}; use crate::store::Globals; -use crate::{Result, interpreter::simd::Value128}; -use tinywasm_types::{GlobalAddr, LocalAddr, RefValue, WasmValue}; +use crate::{Error, Result, Store, WasmValue, interpreter::simd::Value128}; +use tinywasm_types::{GlobalAddr, LocalAddr, WasmType}; pub(crate) type Value32 = u32; pub(crate) type Value64 = u64; @@ -34,6 +34,14 @@ impl ValueRef { Self(((value as u32) << 1) | 1) } + #[inline] + pub(crate) const fn try_from_host_any(addr: u32) -> Option { + if addr >= Self::HOST_ANY_TAG - 1 { + return None; + } + Some(Self::from_category_addr(addr | Self::HOST_ANY_TAG)) + } + pub(crate) const fn is_host_any(self) -> bool { matches!(self.addr(), Some(addr) if addr & Self::HOST_ANY_TAG != 0) } @@ -69,21 +77,9 @@ impl ValueRef { } } -impl From for ValueRef { - fn from(value: RefValue) -> Self { - match value { - RefValue::Null => Self::NULL, - RefValue::Func(value) => Self::from_category_addr(value.addr()), - RefValue::Extern(value) => Self::from_raw(value.raw()), - RefValue::Exn(value) => Self::from_category_addr(value.addr()), - RefValue::Any(value) => Self::from_raw(value.raw()), - } - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// An untyped internal WebAssembly value. -pub(crate) enum TinyWasmValue { +pub(crate) enum RuntimeValue { /// A 32-bit value. Value32(Value32), /// A 64-bit value. @@ -94,16 +90,17 @@ pub(crate) enum TinyWasmValue { ValueRef(ValueRef), } -impl From for TinyWasmValue { - fn from(value: WasmValue) -> Self { - match value { - WasmValue::I32(v) => Self::Value32(v as u32), - WasmValue::I64(v) => Self::Value64(v as u64), - WasmValue::F32(v) => Self::Value32(v.to_bits()), - WasmValue::F64(v) => Self::Value64(v.to_bits()), - WasmValue::Ref(value) => Self::ValueRef(value.into()), - WasmValue::V128(v) => Self::Value128(Value128(v)), - } +impl RuntimeValue { + pub(crate) fn into_wasm(self, store: &mut Store, ty: WasmType) -> Result { + Ok(match (self, ty) { + (Self::Value32(value), WasmType::I32) => WasmValue::I32(value as i32), + (Self::Value32(value), WasmType::F32) => WasmValue::F32(f32::from_bits(value)), + (Self::Value64(value), WasmType::I64) => WasmValue::I64(value as i64), + (Self::Value64(value), WasmType::F64) => WasmValue::F64(f64::from_bits(value)), + (Self::Value128(value), WasmType::V128) => WasmValue::V128(value.0), + (Self::ValueRef(value), WasmType::Ref(ty)) => WasmValue::Ref(store.decode_ref(value, ty)?), + _ => return Err(Error::other("internal value does not match its WebAssembly type")), + }) } } @@ -234,3 +231,13 @@ impl_internalvalue! { stack_32, s32, get_32, set_32, ValueRef, |v| v.raw(), |v| ValueRef(v) stack_128, s128, get_128, set_128, Value128, |v| v, |v| v } + +#[cfg(test)] +mod tests { + use super::ValueRef; + + #[test] + fn value_ref_remains_four_bytes() { + assert_eq!(core::mem::size_of::(), 4); + } +} diff --git a/crates/tinywasm/src/lib.rs b/crates/tinywasm/src/lib.rs index bd80ef1..8509be9 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -18,47 +18,48 @@ //! //! ## Getting started //! -//! The easiest way to get started is to use the `parse_bytes` function to load a -//! WebAssembly module from bytes. This will parse the module and validate it, returning -//! a [`Module`] that can be used to instantiate the module. +//! Use [`parse_bytes`] to parse and validate a WebAssembly module, then instantiate it +//! in a [`Store`]. A module can be reused, while each store owns its runtime state. //! //! ```rust //! # #[cfg(feature = "parser")] //! # fn main() -> tinywasm::Result<()> { //! use tinywasm::{ModuleInstance, Store}; //! -//! // Load a module from bytes //! let wasm = include_bytes!("../../../examples/wasm/add.wasm"); //! let module = tinywasm::parse_bytes(wasm)?; //! -//! // # Create a new store -//! // Stores are used to allocate objects like functions and globals //! let mut store = Store::default(); -//! -//! // # Instantiate the module -//! // This will allocate the module and its globals into the store -//! // and execute the module's start function. //! let instance = ModuleInstance::instantiate(&mut store, &module, None)?; //! -//! // # Get a typed handle to the exported "add" function -//! // Alternatively, you can use `instance.func_untyped` to get an untyped handle -//! // that takes and returns [`types::WasmValue`]s -//! let func = instance.func::<(i32, i32), i32>(&store, "add")?; -//! let res = func.call(&mut store, (1, 2))?; +//! let add = instance.func::<(i32, i32), i32>(&store, "add")?; +//! let result = add.call(&mut store, (1, 2))?; //! -//! assert_eq!(res, 3); +//! assert_eq!(result, 3); //! # Ok(()) //! # } //! # #[cfg(not(feature = "parser"))] //! # fn main() {} //! ``` //! -//! For non-default runtime behavior, construct a [`Store`] with a custom [`Engine`] -//! and [`engine::Config`] to control stack sizing and fuel accounting. A [`ResourceLimiter`] can be -//! attached to the engine configuration to bound memory and table allocation and growth for stores -//! created from that engine and to trap rejected requests. +//! Typed functions convert Rust values directly. Use [`ModuleInstance::func_untyped`] +//! and [`WasmValue`] when types are selected at runtime. References such as [`StructRef`] +//! and [`ExternRef`] are owned handles tied to their store. Cloning a managed reference +//! keeps its referent live. +//! +//! Construct a store with a custom [`Engine`] and [`engine::Config`] to configure +//! stack limits, fuel, and GC collection. A [`ResourceLimiter`] can bound guest memory, +//! table, and GC heap growth. +//! +//! ## References and GC +//! +//! Runtime references belong to a [`Store`]. Passing a reference to another store +//! returns [`Trap::InvalidStore`]. Managed references are cloneable owned handles, and +//! their referents become collectible after the last handle is dropped. Nullable typed +//! parameters and results use `Option`. Use [`RefValue`] for dynamic reference values +//! and [`Store::gc`] to request collection explicitly. //! -//! For more examples, see the [`examples`](https://github.com/explodingcamera/tinywasm/tree/main/examples) directory. +//! For more examples, see the [`examples`](https://github.com/explodingcamera/tinywasm/tree/next/examples) directory. //! //! ## Cargo features //! @@ -108,8 +109,6 @@ pub(crate) mod log { mod error; pub use error::*; -#[allow(deprecated)] -pub use func::WasmTupleChain; pub use func::{ ExecProgress, FromWasmValues, FuncContext, FuncExecution, FuncExecutionTyped, Function, FunctionTyped, HostFunction, IntoWasmValues, ToWasmType, ToWasmTypes, @@ -145,6 +144,7 @@ pub use parser::{parse_file, parse_stream}; /// Re-export of [`tinywasm_types`]. pub mod types { + pub use crate::{AnyRef, ArrayRef, EqRef, ExnRef, ExternRef, FuncRef, I31Ref, RefValue, StructRef, WasmValue}; pub use tinywasm_types::*; } diff --git a/crates/tinywasm/src/reference.rs b/crates/tinywasm/src/reference.rs deleted file mode 100644 index 271e71c..0000000 --- a/crates/tinywasm/src/reference.rs +++ /dev/null @@ -1,513 +0,0 @@ -use core::hint::cold_path; - -use alloc::ffi::CString; -use alloc::format; -use alloc::string::{String, ToString}; -use alloc::vec::Vec; - -use crate::interpreter::ValueRef; -use crate::store::TableInstance; -use crate::{Error, MemoryInstance, Result, Store, Trap}; -use tinywasm_types::{ - Addr, FuncType, GlobalType, MemAddr, MemoryType, TableAddr, TableType, TagAddr, WasmType, WasmValue, -}; - -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub(crate) struct StoreItem { - pub(crate) store_id: u32, - pub(crate) addr: Addr, -} - -impl StoreItem { - #[inline] - /// Creates a handle for an address owned by a store. - pub(crate) const fn new(store_id: u32, addr: Addr) -> Self { - Self { store_id, addr } - } - - #[inline] - pub(crate) fn validate_store(&self, store: &Store) -> Result<(), Trap> { - if self.store_id != store.id() { - return Err(Trap::InvalidStore); - } - Ok(()) - } -} - -/// A memory instance in a store. -/// -/// ## Example -/// ```rust -/// # fn main() -> tinywasm::Result<()> { -/// use tinywasm::types::MemoryType; -/// use tinywasm::{Memory, Store}; -/// -/// let mut store = Store::default(); -/// let memory = Memory::new(&mut store, MemoryType::default().with_page_count_initial(1))?; -/// -/// memory.copy_from_slice(&mut store, 0, b"hi")?; -/// assert_eq!(memory.read_vec(&store, 0, 2)?, b"hi"); -/// assert_eq!(memory.page_count(&store)?, 1); -/// memory.grow(&mut store, 1)?; -/// assert_eq!(memory.page_count(&store)?, 2); -/// # Ok(()) -/// # } -/// ``` -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct Memory(pub(crate) StoreItem); - -/// A table instance in a store. -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct Table(pub(crate) StoreItem); - -/// A global instance in a store. -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct Global(pub(crate) StoreItem); - -/// A tag instance in a store. -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub struct Tag(pub(crate) StoreItem); - -/// A cursor over a [`Memory`] instance. -/// -/// Available with the `std` feature enabled. -#[cfg(feature = "std")] -pub struct MemoryCursor<'a> { - memory: &'a mut MemoryInstance, - position: u64, -} - -#[cfg(feature = "std")] -impl MemoryCursor<'_> { - fn offset(&self) -> crate::std::io::Result { - usize::try_from(self.position).map_err(|_| { - crate::std::io::Error::new(crate::std::io::ErrorKind::InvalidInput, "cursor position exceeds usize") - }) - } - - fn advance(&mut self, amount: usize) -> crate::std::io::Result<()> { - self.position = self.position.checked_add(amount as u64).ok_or_else(|| { - crate::std::io::Error::new(crate::std::io::ErrorKind::InvalidInput, "cursor position overflow") - })?; - Ok(()) - } - - /// Returns the current cursor position. - pub const fn position(&self) -> u64 { - self.position - } - - /// Sets the current cursor position. - pub fn set_position(&mut self, position: u64) { - self.position = position; - } -} - -#[cfg(feature = "std")] -impl crate::std::io::Read for MemoryCursor<'_> { - fn read(&mut self, buf: &mut [u8]) -> crate::std::io::Result { - let offset = self.offset()?; - let read = self.memory.inner.read(offset, buf); - self.advance(read)?; - Ok(read) - } -} - -#[cfg(feature = "std")] -impl crate::std::io::Write for MemoryCursor<'_> { - fn write(&mut self, buf: &[u8]) -> crate::std::io::Result { - let offset = self.offset()?; - let written = self.memory.inner.write(offset, buf); - self.advance(written)?; - Ok(written) - } - - fn flush(&mut self) -> crate::std::io::Result<()> { - Ok(()) - } -} - -#[cfg(feature = "std")] -impl crate::std::io::Seek for MemoryCursor<'_> { - fn seek(&mut self, pos: crate::std::io::SeekFrom) -> crate::std::io::Result { - let len = self.memory.inner.len() as i128; - let current = i128::from(self.position); - - let next = match pos { - crate::std::io::SeekFrom::Start(offset) => i128::from(offset), - crate::std::io::SeekFrom::End(offset) => len + i128::from(offset), - crate::std::io::SeekFrom::Current(offset) => current + i128::from(offset), - }; - - if next < 0 { - return Err(crate::std::io::Error::new( - crate::std::io::ErrorKind::InvalidInput, - "invalid seek before start", - )); - } - - let next = u64::try_from(next).map_err(|_| { - crate::std::io::Error::new(crate::std::io::ErrorKind::InvalidInput, "invalid seek position") - })?; - self.position = next; - Ok(next) - } -} - -impl Memory { - /// Create a new memory in the given store. - pub fn new(store: &mut Store, ty: MemoryType) -> Result { - let addr = store.state.memories.len() as MemAddr; - let limiter = store.engine.config().resource_limiter.clone(); - store.state.memories.push(MemoryInstance::new(ty, limiter.as_deref())?); - Ok(Self(StoreItem::new(store.id(), addr))) - } - - /// Creates a cursor positioned at the start of this memory. - /// - /// Available with the `std` feature enabled. - /// - /// ## Example - /// - /// ```rust - /// # fn main() -> Result<(), Box> { - /// use std::io::{Read, Seek, SeekFrom, Write}; - /// use tinywasm::types::MemoryType; - /// use tinywasm::{Memory, Store}; - /// - /// let mut store = Store::default(); - /// let memory = Memory::new(&mut store, MemoryType::default().with_page_count_initial(1))?; - /// let mut cursor = memory.cursor(&mut store)?; - /// - /// cursor.seek(SeekFrom::Start(2))?; - /// cursor.write_all(b"abc")?; - /// cursor.seek(SeekFrom::Start(0))?; - /// - /// let mut bytes = [0; 5]; - /// cursor.read_exact(&mut bytes)?; - /// assert_eq!(bytes, [0, 0, b'a', b'b', b'c']); - /// # Ok(()) - /// # } - /// ``` - #[cfg(feature = "std")] - pub fn cursor<'a>(&self, store: &'a mut Store) -> Result> { - self.cursor_at(store, 0) - } - - /// Creates a cursor positioned at `position` bytes from the start of this memory. - /// - /// Available with the `std` feature enabled. - #[cfg(feature = "std")] - pub fn cursor_at<'a>(&self, store: &'a mut Store, position: u64) -> Result> { - Ok(MemoryCursor { memory: self.instance_mut(store)?, position }) - } - - #[inline] - fn instance<'a>(&self, store: &'a Store) -> Result<&'a MemoryInstance> { - self.0.validate_store(store)?; - Ok(store.state.get_mem(self.0.addr)) - } - - #[inline] - fn instance_mut<'a>(&self, store: &'a mut Store) -> Result<&'a mut MemoryInstance> { - self.0.validate_store(store)?; - Ok(store.state.get_mem_mut(self.0.addr)) - } - - /// Returns the raw memory byte length. - pub fn len(&self, store: &Store) -> Result { - Ok(self.instance(store)?.inner.len()) - } - - /// Returns the memory type, including page size and limits. - pub fn ty(&self, store: &Store) -> Result { - Ok(self.instance(store)?.kind) - } - - /// Reads up to `dst.len()` bytes from memory and returns the number of bytes read. - /// - /// This returns fewer bytes than requested when the range extends past the end of memory. Use - /// [`Self::read_exact`] or [`Self::read_vec`] when you need a full range. - pub fn read(&self, store: &Store, offset: usize, dst: &mut [u8]) -> Result { - Ok(self.instance(store)?.inner.read(offset, dst)) - } - - /// Writes up to `src.len()` bytes into memory and returns the number of bytes written. - /// - /// This returns fewer bytes than requested when the range extends past the end of memory. Use - /// [`Self::copy_from_slice`] when you need the full slice written. - pub fn write(&self, store: &mut Store, offset: usize, src: &[u8]) -> Result { - Ok(self.instance_mut(store)?.inner.write(offset, src)) - } - - /// Reads exactly `dst.len()` bytes from memory. - pub fn read_exact(&self, store: &Store, offset: usize, dst: &mut [u8]) -> Result<()> { - self.instance(store)?.inner.read_exact(offset, dst).ok_or_else(|| { - Error::Trap(crate::Trap::MemoryOutOfBounds { - offset, - len: dst.len(), - max: self.instance(store).unwrap().inner.len(), - }) - }) - } - - /// Reads `len` bytes from memory into a newly allocated buffer. - pub fn read_vec(&self, store: &Store, offset: usize, len: usize) -> Result> { - self.instance(store)?.inner.read_vec(offset, len).ok_or_else(|| { - Error::Trap(crate::Trap::MemoryOutOfBounds { offset, len, max: self.instance(store).unwrap().inner.len() }) - }) - } - - /// Grows the memory by the given number of pages. - /// - /// Returns the previous size, or `None` if growth fails or is rejected by the resource limiter. - /// A limiter-provided trap is returned as an error. - pub fn grow(&self, store: &mut Store, delta_pages: i64) -> Result> { - let limiter = store.engine.config().resource_limiter.clone(); - let mem = self.instance_mut(store)?; - mem.grow(delta_pages, limiter.as_deref()).map_err(Into::into) - } - - /// Get the current size of the memory in pages. - pub fn page_count(&self, store: &Store) -> Result { - Ok(self.instance(store)?.page_count) - } - - /// Copy a slice of memory to another place in memory. - pub fn copy_within(&self, store: &mut Store, src: usize, dst: usize, len: usize) -> Result<()> { - self.instance_mut(store)?.copy_within(dst, src, len)?; - Ok(()) - } - - /// Fill a slice of memory with a value. - pub fn fill(&self, store: &mut Store, offset: usize, len: usize, val: u8) -> Result<()> { - self.instance_mut(store)?.inner.fill(offset, len, val).ok_or_else(|| { - Error::Trap(crate::Trap::MemoryOutOfBounds { offset, len, max: self.instance(store).unwrap().inner.len() }) - }) - } - - /// Copies a full slice into memory. - pub fn copy_from_slice(&self, store: &mut Store, offset: usize, data: &[u8]) -> Result<()> { - self.instance_mut(store)?.inner.write_all(offset, data).ok_or_else(|| { - Error::Trap(crate::Trap::MemoryOutOfBounds { - offset, - len: data.len(), - max: self.instance(store).unwrap().inner.len(), - }) - }) - } - - /// Copies a nul-terminated C string into memory. - pub fn write_cstring(&self, store: &mut Store, offset: usize, string: &CString) -> Result<()> { - self.copy_from_slice(store, offset, string.as_bytes_with_nul()) - } - - /// Copies a UTF-8 string into memory and appends a trailing nul byte. - pub fn write_cstring_bytes(&self, store: &mut Store, offset: usize, string: &str) -> Result<()> { - let mut bytes = Vec::with_capacity(string.len() + 1); - bytes.extend_from_slice(string.as_bytes()); - bytes.push(0); - self.copy_from_slice(store, offset, &bytes) - } - - /// Reads a C-style string from memory. - pub fn read_cstring(&self, store: &Store, offset: usize, len: usize) -> Result { - CString::from_vec_with_nul(self.read_vec(store, offset, len)?) - .map_err(|e| crate::Error::Other(format!("Invalid C-style string: {e}"))) - } - - /// Reads a C-style string from memory, stopping at the first null byte. - pub fn read_cstring_until_null(&self, store: &Store, offset: usize, max_len: usize) -> Result { - let bytes = self.read_vec(store, offset, max_len)?; - let Some(null) = bytes.iter().position(|byte| *byte == 0) else { - return Err(crate::Error::Other("Invalid C-style string: missing null terminator".to_string())); - }; - - CString::from_vec_with_nul(bytes[..=null].to_vec()) - .map_err(|e| crate::Error::Other(format!("Invalid C-style string: {e}"))) - } - - /// Reads a UTF-8 string from memory. - pub fn read_string(&self, store: &Store, offset: usize, len: usize) -> Result { - String::from_utf8(self.read_vec(store, offset, len)?) - .map_err(|e| crate::Error::Other(format!("Invalid UTF-8 string: {e}"))) - } - - /// Reads a JavaScript-style utf-16 string from memory. - pub fn read_js_string(&self, store: &Store, offset: usize, len: usize) -> Result { - let bytes = self.read_vec(store, offset, len)?; - let mut string = String::new(); - for i in 0..(len / 2) { - let c = u16::from_le_bytes([bytes[i * 2], bytes[i * 2 + 1]]); - string.push( - char::from_u32(u32::from(c)).ok_or_else(|| crate::Error::Other("Invalid UTF-16 string".to_string()))?, - ); - } - Ok(string) - } -} - -fn table_value_to_element( - state: &crate::store::State, - element_type: tinywasm_types::RefType, - value: WasmValue, -) -> Result { - let WasmValue::Ref(value) = value else { - return Err(Trap::Other("invalid table value type")); - }; - if !state.value_matches_type(WasmValue::Ref(value), WasmType::Ref(element_type)) { - return Err(Trap::Other("invalid table value type")); - } - Ok(value.into()) -} - -impl Table { - /// Create a new table in the given store. - pub fn new(store: &mut Store, ty: TableType, init: WasmValue) -> Result { - if ty.element_type.is_concrete() { - return Err(Error::other("host tables cannot use module-relative concrete reference types")); - } - let init = table_value_to_element(&store.state, ty.element_type, init).map_err(Error::from)?; - let limiter = store.engine.config().resource_limiter.clone(); - let addr = store.state.tables.len() as TableAddr; - store.state.tables.push(TableInstance::new(ty, init, limiter.as_deref())?); - Ok(Self(StoreItem::new(store.id(), addr))) - } - - #[inline] - fn instance<'a>(&self, store: &'a Store) -> Result<&'a TableInstance> { - self.0.validate_store(store)?; - Ok(store.state.get_table(self.0.addr)) - } - - /// Get the type of the table. - pub fn ty(&self, store: &Store) -> Result { - Ok(self.instance(store)?.kind) - } - - /// Get the current number of elements in the table. - pub fn size(&self, store: &Store) -> Result { - Ok(self.instance(store)?.size()) - } - - /// Get a table element as a wasm reference value. - pub fn get(&self, store: &Store, index: TableAddr) -> Result { - let table = self.instance(store)?; - let value = store.state.to_ref_value(*table.get(index as usize)?, table.kind.element_type); - store.state.pin_host_ref(value); - Ok(WasmValue::Ref(value)) - } - - /// Load a range of table elements and iterate over wasm reference values. - pub fn load<'a>( - &self, - store: &'a Store, - offset: usize, - len: usize, - ) -> Result + 'a> { - let table = self.instance(store)?; - let element_type = table.kind.element_type; - let elements = table.load(offset, len)?; - let may_contain_gc = store.state.type_may_contain_gc(&WasmType::Ref(element_type)); - Ok(elements.iter().copied().map(move |value| { - if may_contain_gc { - store.state.gc.pin(value); - } - WasmValue::Ref(store.state.to_ref_value(value, element_type)) - })) - } - - /// Set a table element. - pub fn set(&self, store: &mut Store, index: TableAddr, value: WasmValue) -> Result<(), Trap> { - self.0.validate_store(store)?; - let element_type = store.state.get_table(self.0.addr).kind.element_type; - let value = table_value_to_element(&store.state, element_type, value)?; - store.state.get_table_mut(self.0.addr).set(index as usize, value) - } - - /// Copy elements within the same table. - pub fn copy_within(&self, store: &mut Store, src: usize, dst: usize, len: usize) -> Result<(), Trap> { - self.0.validate_store(store)?; - store.state.get_table_mut(self.0.addr).copy_within(dst, src, len) - } - - /// Grows the table and returns the previous size. - /// - /// Returns `None` if growth fails or is rejected by the resource limiter. A limiter-provided - /// trap is returned as an error. - pub fn grow(&self, store: &mut Store, delta: i32, init: WasmValue) -> Result> { - self.0.validate_store(store)?; - let table = store.state.get_table(self.0.addr); - let old_size = table.size(); - let init = table_value_to_element(&store.state, table.kind.element_type, init)?; - let Ok(delta) = usize::try_from(delta) else { - return Ok(None); - }; - let limiter = store.engine.config().resource_limiter.clone(); - match store.state.get_table_mut(self.0.addr).grow(delta, init, limiter.as_deref())? { - true => Ok(Some(old_size)), - false => Ok(None), - } - } -} - -impl Global { - /// Create a new global in the given store. - pub fn new(store: &mut Store, ty: GlobalType, value: WasmValue) -> Result { - if matches!(ty.ty, WasmType::Ref(ty) if ty.is_concrete()) { - return Err(Error::other("host globals cannot use module-relative concrete reference types")); - } - if !store.state.value_matches_type(value, ty.ty) { - cold_path(); - return Err(Error::Other("invalid global value type".to_string())); - } - let addr = store.state.globals.push(ty, value.into()); - Ok(Self(StoreItem::new(store.id(), addr))) - } - - /// Get the type of the global. - pub fn ty(&self, store: &Store) -> Result { - self.0.validate_store(store)?; - Ok(store.state.globals.ty(self.0.addr)) - } - - /// Get the current value of the global. - pub fn get(&self, store: &Store) -> Result { - self.0.validate_store(store)?; - let value = store.state.get_global_wasmvalue(self.0.addr); - if let WasmValue::Ref(value) = value { - store.state.pin_host_ref(value); - } - Ok(value) - } - - /// Set the current value of the global. - pub fn set(&self, store: &mut Store, value: WasmValue) -> Result<()> { - self.0.validate_store(store)?; - store.state.set_global_wasmvalue(self.0.addr, value) - } -} - -impl Tag { - /// Create a new exception tag in the given store. - pub fn new(store: &mut Store, ty: FuncType) -> Result { - if !ty.results().is_empty() { - return Err(Error::other("tag types must not have results")); - } - let type_addr = store.register_host_type(&ty); - let addr = store.state.tags.len() as TagAddr; - store.state.tags.push(crate::store::TagInstance { type_addr }); - Ok(Self(StoreItem::new(store.id(), addr))) - } - - /// Get the payload type of the tag. - pub fn ty<'a>(&self, store: &'a Store) -> Result<&'a FuncType> { - self.0.validate_store(store)?; - Ok(store.state.get_canonical_func_type(store.state.get_tag(self.0.addr).type_addr)) - } -} diff --git a/crates/tinywasm/src/reference/managed.rs b/crates/tinywasm/src/reference/managed.rs new file mode 100644 index 0000000..1e5f38a --- /dev/null +++ b/crates/tinywasm/src/reference/managed.rs @@ -0,0 +1,236 @@ +use alloc::sync::Arc; +use core::sync::atomic::{AtomicU32, Ordering}; + +use crate::interpreter::ValueRef; +use crate::{Result, Store, Trap}; + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct StoreId(u32); + +impl StoreId { + pub(crate) fn fresh() -> Self { + static NEXT: AtomicU32 = AtomicU32::new(0); + next_store_id(&NEXT) + } + + pub(crate) const fn get(self) -> u32 { + self.0 + } +} + +fn next_store_id(counter: &AtomicU32) -> StoreId { + let id = counter + .try_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1)) + .expect("Store identity space exhausted"); + StoreId(id) +} + +#[cfg(feature = "debug")] +impl core::fmt::Debug for StoreId { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("StoreId(..)") + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub(crate) enum ReferentKind { + I31, + HostExtern, + Struct, + Array, + Exception, +} + +#[derive(Clone)] +pub(crate) struct RootedItem { + pub(crate) store: StoreId, + pub(crate) value: ValueRef, + pub(crate) kind: ReferentKind, + pub(crate) _token: Option>, +} + +impl PartialEq for RootedItem { + fn eq(&self, other: &Self) -> bool { + self.kind == other.kind + && self.value == other.value + && (self.kind == ReferentKind::I31 || self.store == other.store) + } +} + +impl Eq for RootedItem {} + +#[cfg(feature = "debug")] +impl core::fmt::Debug for RootedItem { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Reference").field("kind", &self.kind).finish_non_exhaustive() + } +} + +pub(crate) trait StoredRef: Clone + Sized { + fn from_rooted_item(item: RootedItem) -> Self; + fn rooted_item(&self) -> &RootedItem; +} + +macro_rules! reference_types { + ($($(#[$meta:meta])* $name:ident),* $(,)?) => {$($( + #[$meta] + )* + #[derive(Clone, PartialEq, Eq)] + #[cfg_attr(feature = "debug", derive(Debug))] + pub struct $name(RootedItem); + + impl StoredRef for $name { + fn from_rooted_item(item: RootedItem) -> Self { Self(item) } + fn rooted_item(&self) -> &RootedItem { &self.0 } + } + )*}; +} + +reference_types! { + /// An owned WebAssembly `anyref`. + AnyRef, + /// An owned WebAssembly `eqref`. + EqRef, + /// An owned WebAssembly `i31ref`. + I31Ref, + /// An owned WebAssembly `structref`. + StructRef, + /// An owned WebAssembly `arrayref`. + ArrayRef, + /// An owned WebAssembly `externref`. + ExternRef, + /// An owned WebAssembly `exnref`. + ExnRef, +} + +impl AnyRef { + /// Returns this reference as an `eqref` when its referent is comparable. + pub fn as_eq(&self) -> Option { + matches!(self.0.kind, ReferentKind::I31 | ReferentKind::Struct | ReferentKind::Array) + .then(|| EqRef(self.0.clone())) + } + + /// Returns this reference as an `i31ref` when it contains an i31. + pub fn as_i31(&self) -> Option { + (self.0.kind == ReferentKind::I31).then(|| I31Ref(self.0.clone())) + } + + /// Returns this reference as a `structref` when it refers to a struct. + pub fn as_struct(&self) -> Option { + (self.0.kind == ReferentKind::Struct).then(|| StructRef(self.0.clone())) + } + + /// Returns this reference as an `arrayref` when it refers to an array. + pub fn as_array(&self) -> Option { + (self.0.kind == ReferentKind::Array).then(|| ArrayRef(self.0.clone())) + } + + /// Returns this reference with the `externref` view. + pub fn to_extern(&self) -> ExternRef { + ExternRef(self.0.clone()) + } +} + +macro_rules! impl_to_any { + ($($ty:ty),* $(,)?) => {$( + impl $ty { + /// Returns this reference with the `anyref` view. + pub fn to_any(&self) -> AnyRef { AnyRef(self.0.clone()) } + } + )*}; +} + +impl_to_any!(EqRef, I31Ref, StructRef, ArrayRef, ExternRef); + +/// A Store-aware WebAssembly function reference. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct FuncRef { + store: StoreId, + addr: u32, +} + +impl FuncRef { + pub(crate) const fn new(store: StoreId, addr: u32) -> Self { + Self { store, addr } + } + + pub(crate) fn addr(self, store: StoreId) -> Option { + (self.store == store).then_some(self.addr) + } +} + +#[cfg(feature = "debug")] +impl core::fmt::Debug for FuncRef { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("FuncRef(..)") + } +} + +/// A host-facing WebAssembly reference value. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub enum RefValue { + /// A null reference. + Null, + /// A function reference. + Func(FuncRef), + /// An external reference. + Extern(ExternRef), + /// A reference in the `any` hierarchy. + Any(AnyRef), + /// An exception reference. + Exn(ExnRef), +} + +impl ExternRef { + /// Creates an external reference containing a host-defined key. + /// + /// Keys must be in `0..=(1 << 30) - 2`. + pub fn try_new(store: &mut Store, key: u32) -> Result { + let value = ValueRef::try_from_host_any(key).ok_or(Trap::InvalidReference)?; + store.root_reference(value, ReferentKind::HostExtern) + } + + /// Returns the host-defined key stored in this external reference. + /// + /// Returns an error for a reference produced by `extern.convert_any` from a + /// guest GC object or when `store` does not own the reference. + pub fn key(&self, store: &Store) -> Result { + let value = store.resolve_ref(self)?; + let addr = value.addr().filter(|_| value.is_host_any()).ok_or(Trap::InvalidReference)?; + Ok(addr & !(1 << 30)) + } +} + +impl I31Ref { + /// Creates a signed WebAssembly i31 reference. + /// + /// Values must be in `-2^30..=2^30 - 1`. + pub fn try_new(store: &mut Store, value: i32) -> Result { + if !(-(1 << 30)..(1 << 30)).contains(&value) { + return Err(Trap::InvalidReference.into()); + } + store.root_reference(ValueRef::from_i31(value), ReferentKind::I31) + } +} + +#[cfg(test)] +mod tests { + extern crate std; + use super::*; + + #[test] + fn exhausted_store_identity_counter_never_wraps() { + let counter = AtomicU32::new(u32::MAX - 1); + assert_eq!(next_store_id(&counter).get(), u32::MAX - 1); + assert!(std::panic::catch_unwind(|| next_store_id(&counter)).is_err()); + assert!(std::panic::catch_unwind(|| next_store_id(&counter)).is_err()); + } + + #[test] + fn host_reference_key_fits_internal_encoding() { + assert!(ValueRef::try_from_host_any((1 << 30) - 2).is_some()); + assert!(ValueRef::try_from_host_any((1 << 30) - 1).is_none()); + } +} diff --git a/crates/tinywasm/src/reference/mod.rs b/crates/tinywasm/src/reference/mod.rs new file mode 100644 index 0000000..7bc61e5 --- /dev/null +++ b/crates/tinywasm/src/reference/mod.rs @@ -0,0 +1,13 @@ +mod managed; +mod store; +mod value; + +pub use managed::{AnyRef, ArrayRef, EqRef, ExnRef, ExternRef, FuncRef, I31Ref, RefValue, StructRef}; +pub(crate) use managed::{ReferentKind, RootedItem, StoreId, StoredRef}; +#[cfg(feature = "std")] +pub use store::MemoryCursor; +pub(crate) use store::StoreItem; +pub use store::{ + GcFieldType, GcHeapType, GcRefType, GcStorageType, GcType, GcTypeKind, GcValueType, Global, Memory, Table, Tag, +}; +pub use value::WasmValue; diff --git a/crates/tinywasm/src/reference/store.rs b/crates/tinywasm/src/reference/store.rs new file mode 100644 index 0000000..f2ee6bc --- /dev/null +++ b/crates/tinywasm/src/reference/store.rs @@ -0,0 +1,914 @@ +use core::hint::cold_path; + +use alloc::ffi::CString; +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; + +use super::managed::{ReferentKind, StoreId, StoredRef}; +use crate::interpreter::ValueRef; +use crate::store::TableInstance; +use crate::{ArrayRef, Error, ExnRef, I31Ref, MemoryInstance, Result, Store, StructRef, Trap, WasmValue}; +use tinywasm_types::{ + AbstractHeapType, Addr, CompositeType, FieldType, FuncType, GlobalType, MemAddr, MemoryType, StorageType, + TableAddr, TableType, TagAddr, TypeAddr, WasmType, +}; + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub(crate) struct StoreItem { + pub(crate) store_id: StoreId, + pub(crate) addr: Addr, +} + +impl StoreItem { + #[inline] + /// Creates a handle for an address owned by a store. + pub(crate) const fn new(store_id: StoreId, addr: Addr) -> Self { + Self { store_id, addr } + } + + #[inline] + pub(crate) fn validate_store(&self, store: &Store) -> Result<(), Trap> { + if self.store_id != store.store_id() { + return Err(Trap::InvalidStore); + } + Ok(()) + } +} + +/// A memory instance in a store. +/// +/// ## Example +/// ```rust +/// # fn main() -> tinywasm::Result<()> { +/// use tinywasm::types::MemoryType; +/// use tinywasm::{Memory, Store}; +/// +/// let mut store = Store::default(); +/// let memory = Memory::try_new(&mut store, MemoryType::default().with_page_count_initial(1))?; +/// +/// memory.copy_from_slice(&mut store, 0, b"hi")?; +/// assert_eq!(memory.read_vec(&store, 0, 2)?, b"hi"); +/// assert_eq!(memory.page_count(&store)?, 1); +/// memory.grow(&mut store, 1)?; +/// assert_eq!(memory.page_count(&store)?, 2); +/// # Ok(()) +/// # } +/// ``` +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Memory(pub(crate) StoreItem); + +/// A table instance in a store. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Table(pub(crate) StoreItem); + +/// A global instance in a store. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Global(pub(crate) StoreItem); + +/// A tag instance in a store. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Tag(pub(crate) StoreItem); + +/// An opaque canonical function, struct, or array type owned by a Store. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct GcType(StoreItem); + +/// The composite kind of a [`GcType`]. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub enum GcTypeKind { + /// A function type. + Function, + /// A struct type. + Struct, + /// An array type. + Array, +} + +/// Metadata for a struct field or array element. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct GcFieldType { + storage: GcStorageType, + mutable: bool, +} + +impl GcFieldType { + /// Returns the field's storage type. + pub const fn storage(self) -> GcStorageType { + self.storage + } + + /// Returns whether the field can be changed. + pub const fn is_mutable(self) -> bool { + self.mutable + } +} + +/// Host-visible storage metadata for a GC field or array element. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub enum GcStorageType { + /// A packed 8-bit integer. + I8, + /// A packed 16-bit integer. + I16, + /// An unpacked WebAssembly value. + Value(GcValueType), +} + +/// Host-visible value metadata that does not expose concrete reference encodings. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub enum GcValueType { + /// A 32-bit integer. + I32, + /// A 64-bit integer. + I64, + /// A 32-bit float. + F32, + /// A 64-bit float. + F64, + /// A 128-bit vector. + V128, + /// A reference type. + Ref(GcRefType), +} + +/// Host-visible reference metadata with opaque concrete types. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct GcRefType { + nullable: bool, + heap_type: GcHeapType, +} + +impl GcRefType { + /// Returns whether the reference type accepts null. + pub const fn is_nullable(self) -> bool { + self.nullable + } + + /// Returns the abstract or opaque concrete heap type. + pub const fn heap_type(self) -> GcHeapType { + self.heap_type + } +} + +/// Host-visible heap type metadata. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub enum GcHeapType { + /// An abstract WebAssembly heap type. + Abstract(AbstractHeapType), + /// An opaque canonical type owned by the Store. + Concrete(GcType), +} + +impl GcType { + fn subtype(self, store: &Store) -> Result<&tinywasm_types::SubType> { + self.0.validate_store(store)?; + store.state.canonical_types.get(self.0.addr as usize).ok_or_else(|| Trap::InvalidReference.into()) + } + + /// Returns this type's composite kind. + pub fn kind(self, store: &Store) -> Result { + Ok(match &self.subtype(store)?.composite { + CompositeType::Func(_) => GcTypeKind::Function, + CompositeType::Struct(_) => GcTypeKind::Struct, + CompositeType::Array(_) => GcTypeKind::Array, + }) + } + + /// Returns whether this type is a nominal subtype of `other`. + pub fn is_subtype_of(self, store: &Store, other: Self) -> Result { + self.subtype(store)?; + other.subtype(store)?; + Ok(store.state.type_addr_is_subtype(self.0.addr, other.0.addr)) + } + + /// Returns the number of struct fields. + pub fn field_count(self, store: &Store) -> Result { + self.subtype(store)? + .as_struct() + .map(|ty| ty.fields.len()) + .ok_or_else(|| Error::other("GC type is not a struct")) + } + + /// Returns metadata for one struct field. + pub fn field(self, store: &Store, index: usize) -> Result { + let field = self + .subtype(store)? + .as_struct() + .ok_or_else(|| Error::other("GC type is not a struct"))? + .fields + .get(index) + .copied() + .ok_or_else(|| Error::other("struct field out of bounds"))?; + Ok(gc_field_type(store, field)) + } + + /// Returns metadata for an array element. + pub fn array_element(self, store: &Store) -> Result { + let field = self.subtype(store)?.as_array().ok_or_else(|| Error::other("GC type is not an array"))?.field; + Ok(gc_field_type(store, field)) + } +} + +fn gc_field_type(store: &Store, field: FieldType) -> GcFieldType { + let storage = match field.storage { + StorageType::I8 => GcStorageType::I8, + StorageType::I16 => GcStorageType::I16, + StorageType::Value(ty) => GcStorageType::Value(match ty { + WasmType::I32 => GcValueType::I32, + WasmType::I64 => GcValueType::I64, + WasmType::F32 => GcValueType::F32, + WasmType::F64 => GcValueType::F64, + WasmType::V128 => GcValueType::V128, + WasmType::Ref(ty) => GcValueType::Ref(GcRefType { + nullable: ty.is_nullable(), + heap_type: match ty.type_index() { + Some(addr) => GcHeapType::Concrete(GcType(StoreItem::new(store.store_id(), addr))), + None => GcHeapType::Abstract(ty.abstract_heap_type().expect("abstract reference type")), + }, + }), + }), + }; + GcFieldType { storage, mutable: field.mutable } +} + +fn rooted_object(store: &Store, root: &T, kind: ReferentKind) -> Result<(ValueRef, TypeAddr)> { + let value = store.resolve_ref(root)?; + if root.rooted_item().kind != kind { + return Err(Trap::InvalidReference.into()); + } + let object = store.state.gc.get(value).ok_or(Trap::InvalidReference)?; + let crate::store::GcObjectKind::Composite(type_addr) = object.kind else { + return Err(Trap::InvalidReference.into()); + }; + Ok((value, type_addr)) +} + +/// A cursor over a [`Memory`] instance. +/// +/// Available with the `std` feature enabled. +#[cfg(feature = "std")] +pub struct MemoryCursor<'a> { + memory: &'a mut MemoryInstance, + position: u64, +} + +#[cfg(feature = "std")] +impl MemoryCursor<'_> { + fn offset(&self) -> crate::std::io::Result { + usize::try_from(self.position).map_err(|_| { + crate::std::io::Error::new(crate::std::io::ErrorKind::InvalidInput, "cursor position exceeds usize") + }) + } + + fn advance(&mut self, amount: usize) -> crate::std::io::Result<()> { + self.position = self.position.checked_add(amount as u64).ok_or_else(|| { + crate::std::io::Error::new(crate::std::io::ErrorKind::InvalidInput, "cursor position overflow") + })?; + Ok(()) + } + + /// Returns the current cursor position. + pub const fn position(&self) -> u64 { + self.position + } + + /// Sets the current cursor position. + pub fn set_position(&mut self, position: u64) { + self.position = position; + } +} + +#[cfg(feature = "std")] +impl crate::std::io::Read for MemoryCursor<'_> { + fn read(&mut self, buf: &mut [u8]) -> crate::std::io::Result { + let offset = self.offset()?; + let read = self.memory.inner.read(offset, buf); + self.advance(read)?; + Ok(read) + } +} + +#[cfg(feature = "std")] +impl crate::std::io::Write for MemoryCursor<'_> { + fn write(&mut self, buf: &[u8]) -> crate::std::io::Result { + let offset = self.offset()?; + let written = self.memory.inner.write(offset, buf); + self.advance(written)?; + Ok(written) + } + + fn flush(&mut self) -> crate::std::io::Result<()> { + Ok(()) + } +} + +#[cfg(feature = "std")] +impl crate::std::io::Seek for MemoryCursor<'_> { + fn seek(&mut self, pos: crate::std::io::SeekFrom) -> crate::std::io::Result { + let len = self.memory.inner.len() as i128; + let current = i128::from(self.position); + + let next = match pos { + crate::std::io::SeekFrom::Start(offset) => i128::from(offset), + crate::std::io::SeekFrom::End(offset) => len + i128::from(offset), + crate::std::io::SeekFrom::Current(offset) => current + i128::from(offset), + }; + + if next < 0 { + return Err(crate::std::io::Error::new( + crate::std::io::ErrorKind::InvalidInput, + "invalid seek before start", + )); + } + + let next = u64::try_from(next).map_err(|_| { + crate::std::io::Error::new(crate::std::io::ErrorKind::InvalidInput, "invalid seek position") + })?; + self.position = next; + Ok(next) + } +} + +impl Memory { + /// Create a new memory in the given store. + pub fn try_new(store: &mut Store, ty: MemoryType) -> Result { + let addr = store.state.memories.len() as MemAddr; + let limiter = store.engine.config().resource_limiter.clone(); + store.state.memories.push(MemoryInstance::new(ty, limiter.as_deref())?); + Ok(Self(StoreItem::new(store.store_id(), addr))) + } + + /// Creates a cursor positioned at the start of this memory. + /// + /// Available with the `std` feature enabled. + /// + /// ## Example + /// + /// ```rust + /// # fn main() -> Result<(), Box> { + /// use std::io::{Read, Seek, SeekFrom, Write}; + /// use tinywasm::types::MemoryType; + /// use tinywasm::{Memory, Store}; + /// + /// let mut store = Store::default(); + /// let memory = Memory::try_new(&mut store, MemoryType::default().with_page_count_initial(1))?; + /// let mut cursor = memory.cursor(&mut store)?; + /// + /// cursor.seek(SeekFrom::Start(2))?; + /// cursor.write_all(b"abc")?; + /// cursor.seek(SeekFrom::Start(0))?; + /// + /// let mut bytes = [0; 5]; + /// cursor.read_exact(&mut bytes)?; + /// assert_eq!(bytes, [0, 0, b'a', b'b', b'c']); + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "std")] + pub fn cursor<'a>(&self, store: &'a mut Store) -> Result> { + self.cursor_at(store, 0) + } + + /// Creates a cursor positioned at `position` bytes from the start of this memory. + /// + /// Available with the `std` feature enabled. + #[cfg(feature = "std")] + pub fn cursor_at<'a>(&self, store: &'a mut Store, position: u64) -> Result> { + Ok(MemoryCursor { memory: self.instance_mut(store)?, position }) + } + + #[inline] + fn instance<'a>(&self, store: &'a Store) -> Result<&'a MemoryInstance> { + self.0.validate_store(store)?; + Ok(store.state.get_mem(self.0.addr)) + } + + #[inline] + fn instance_mut<'a>(&self, store: &'a mut Store) -> Result<&'a mut MemoryInstance> { + self.0.validate_store(store)?; + Ok(store.state.get_mem_mut(self.0.addr)) + } + + /// Returns the raw memory byte length. + pub fn len(&self, store: &Store) -> Result { + Ok(self.instance(store)?.inner.len()) + } + + /// Returns the memory type, including page size and limits. + pub fn ty(&self, store: &Store) -> Result { + Ok(self.instance(store)?.kind) + } + + /// Reads up to `dst.len()` bytes from memory and returns the number of bytes read. + /// + /// This returns fewer bytes than requested when the range extends past the end of memory. Use + /// [`Self::read_exact`] or [`Self::read_vec`] when you need a full range. + pub fn read(&self, store: &Store, offset: usize, dst: &mut [u8]) -> Result { + Ok(self.instance(store)?.inner.read(offset, dst)) + } + + /// Writes up to `src.len()` bytes into memory and returns the number of bytes written. + /// + /// This returns fewer bytes than requested when the range extends past the end of memory. Use + /// [`Self::copy_from_slice`] when you need the full slice written. + pub fn write(&self, store: &mut Store, offset: usize, src: &[u8]) -> Result { + Ok(self.instance_mut(store)?.inner.write(offset, src)) + } + + /// Reads exactly `dst.len()` bytes from memory. + pub fn read_exact(&self, store: &Store, offset: usize, dst: &mut [u8]) -> Result<()> { + self.instance(store)?.inner.read_exact(offset, dst).ok_or_else(|| { + Error::Trap(crate::Trap::MemoryOutOfBounds { + offset, + len: dst.len(), + max: self.instance(store).unwrap().inner.len(), + }) + }) + } + + /// Reads `len` bytes from memory into a newly allocated buffer. + pub fn read_vec(&self, store: &Store, offset: usize, len: usize) -> Result> { + self.instance(store)?.inner.read_vec(offset, len).ok_or_else(|| { + Error::Trap(crate::Trap::MemoryOutOfBounds { offset, len, max: self.instance(store).unwrap().inner.len() }) + }) + } + + /// Grows the memory by the given number of pages. + /// + /// Returns the previous size, or `None` if growth fails or is rejected by the resource limiter. + /// A limiter-provided trap is returned as an error. + pub fn grow(&self, store: &mut Store, delta_pages: i64) -> Result> { + let limiter = store.engine.config().resource_limiter.clone(); + let mem = self.instance_mut(store)?; + mem.grow(delta_pages, limiter.as_deref()).map_err(Into::into) + } + + /// Get the current size of the memory in pages. + pub fn page_count(&self, store: &Store) -> Result { + Ok(self.instance(store)?.page_count) + } + + /// Copy a slice of memory to another place in memory. + pub fn copy_within(&self, store: &mut Store, src: usize, dst: usize, len: usize) -> Result<()> { + self.instance_mut(store)?.copy_within(dst, src, len)?; + Ok(()) + } + + /// Fill a slice of memory with a value. + pub fn fill(&self, store: &mut Store, offset: usize, len: usize, val: u8) -> Result<()> { + self.instance_mut(store)?.inner.fill(offset, len, val).ok_or_else(|| { + Error::Trap(crate::Trap::MemoryOutOfBounds { offset, len, max: self.instance(store).unwrap().inner.len() }) + }) + } + + /// Copies a full slice into memory. + pub fn copy_from_slice(&self, store: &mut Store, offset: usize, data: &[u8]) -> Result<()> { + self.instance_mut(store)?.inner.write_all(offset, data).ok_or_else(|| { + Error::Trap(crate::Trap::MemoryOutOfBounds { + offset, + len: data.len(), + max: self.instance(store).unwrap().inner.len(), + }) + }) + } + + /// Copies a nul-terminated C string into memory. + pub fn write_cstring(&self, store: &mut Store, offset: usize, string: &CString) -> Result<()> { + self.copy_from_slice(store, offset, string.as_bytes_with_nul()) + } + + /// Copies a UTF-8 string into memory and appends a trailing nul byte. + pub fn write_cstring_bytes(&self, store: &mut Store, offset: usize, string: &str) -> Result<()> { + let mut bytes = Vec::with_capacity(string.len() + 1); + bytes.extend_from_slice(string.as_bytes()); + bytes.push(0); + self.copy_from_slice(store, offset, &bytes) + } + + /// Reads a C-style string from memory. + pub fn read_cstring(&self, store: &Store, offset: usize, len: usize) -> Result { + CString::from_vec_with_nul(self.read_vec(store, offset, len)?) + .map_err(|e| crate::Error::Other(format!("Invalid C-style string: {e}"))) + } + + /// Reads a C-style string from memory, stopping at the first null byte. + pub fn read_cstring_until_null(&self, store: &Store, offset: usize, max_len: usize) -> Result { + let bytes = self.read_vec(store, offset, max_len)?; + let Some(null) = bytes.iter().position(|byte| *byte == 0) else { + return Err(crate::Error::Other("Invalid C-style string: missing null terminator".to_string())); + }; + + CString::from_vec_with_nul(bytes[..=null].to_vec()) + .map_err(|e| crate::Error::Other(format!("Invalid C-style string: {e}"))) + } + + /// Reads a UTF-8 string from memory. + pub fn read_string(&self, store: &Store, offset: usize, len: usize) -> Result { + String::from_utf8(self.read_vec(store, offset, len)?) + .map_err(|e| crate::Error::Other(format!("Invalid UTF-8 string: {e}"))) + } + + /// Reads a JavaScript-style utf-16 string from memory. + pub fn read_js_string(&self, store: &Store, offset: usize, len: usize) -> Result { + let bytes = self.read_vec(store, offset, len)?; + let mut string = String::new(); + for i in 0..(len / 2) { + let c = u16::from_le_bytes([bytes[i * 2], bytes[i * 2 + 1]]); + string.push( + char::from_u32(u32::from(c)).ok_or_else(|| crate::Error::Other("Invalid UTF-16 string".to_string()))?, + ); + } + Ok(string) + } +} + +fn table_value_to_element( + store: &Store, + element_type: tinywasm_types::RefType, + value: WasmValue, +) -> Result { + let WasmValue::Ref(reference) = &value else { + return Err(Trap::Other("invalid table value type")); + }; + if !store.value_matches_type(&value, WasmType::Ref(element_type)) { + return Err(Trap::Other("invalid table value type")); + } + store.encode_ref(reference).map_err(|error| match error { + Error::Trap(trap) => trap, + _ => Trap::Other("invalid table value type"), + }) +} + +fn storage_type(storage: StorageType) -> WasmType { + match storage { + StorageType::I8 | StorageType::I16 => WasmType::I32, + StorageType::Value(ty) => ty, + } +} + +impl I31Ref { + /// Returns the signed i31 value. + pub fn value(&self, store: &Store) -> Result { + if self.rooted_item().kind != ReferentKind::I31 { + return Err(Trap::InvalidReference.into()); + } + store.resolve_ref(self)?.i31_s().ok_or_else(|| Trap::InvalidReference.into()) + } +} + +impl StructRef { + /// Returns this struct's canonical type. + pub fn ty(&self, store: &Store) -> Result { + let (_, type_addr) = rooted_object(store, self, ReferentKind::Struct)?; + Ok(GcType(StoreItem::new(store.store_id(), type_addr))) + } + + /// Reads one field. + /// + /// Packed integer fields are returned as zero-extended `i32` values. Mutable + /// Store access is required to register owned reference results. + pub fn field(&self, store: &mut Store, index: usize) -> Result { + let (reference, type_addr) = rooted_object(store, self, ReferentKind::Struct)?; + let storage = store + .state + .get_type(type_addr) + .as_struct() + .unwrap() + .fields + .get(index) + .ok_or_else(|| Error::other("struct field out of bounds"))? + .storage; + let value = *store.state.gc.get(reference).unwrap().values.get(index).unwrap(); + value.into_wasm(store, storage_type(storage)) + } + + /// Reads all fields in declaration order. + /// + /// Mutable Store access is required because reference results are registered with the Store. + pub fn fields(&self, store: &mut Store) -> Result> { + let (reference, type_addr) = rooted_object(store, self, ReferentKind::Struct)?; + let field_count = store.state.get_type(type_addr).as_struct().unwrap().fields.len(); + let mut values = Vec::new(); + values.try_reserve_exact(field_count).map_err(|_| Trap::OutOfMemory)?; + for index in 0..field_count { + let storage = store.state.get_type(type_addr).as_struct().unwrap().fields[index].storage; + let value = store.state.gc.get(reference).unwrap().values[index]; + values.push(value.into_wasm(store, storage_type(storage))?); + } + Ok(values) + } + + /// Writes one mutable field after validating its canonical value type. + /// + /// Writes to packed integer fields truncate the input to the field width. + pub fn set_field(&self, store: &mut Store, index: usize, value: WasmValue) -> Result<()> { + let (reference, type_addr) = rooted_object(store, self, ReferentKind::Struct)?; + let field = *store + .state + .get_type(type_addr) + .as_struct() + .unwrap() + .fields + .get(index) + .ok_or_else(|| Error::other("struct field out of bounds"))?; + set_gc_value(store, reference, index, field, value, "struct field") + } +} + +impl ArrayRef { + /// Returns this array's canonical type. + pub fn ty(&self, store: &Store) -> Result { + let (_, type_addr) = rooted_object(store, self, ReferentKind::Array)?; + Ok(GcType(StoreItem::new(store.store_id(), type_addr))) + } + + /// Returns the number of array elements. + pub fn len(&self, store: &Store) -> Result { + let (reference, _) = rooted_object(store, self, ReferentKind::Array)?; + Ok(store.state.gc.get(reference).unwrap().values.len()) + } + + /// Returns whether the array contains no elements. + pub fn is_empty(&self, store: &Store) -> Result { + Ok(self.len(store)? == 0) + } + + /// Reads one array element. + /// + /// Packed integer elements are returned as zero-extended `i32` values. Mutable + /// Store access is required to register owned reference results. + pub fn get(&self, store: &mut Store, index: usize) -> Result { + let (reference, type_addr) = rooted_object(store, self, ReferentKind::Array)?; + let storage = store.state.get_type(type_addr).as_array().unwrap().field.storage; + let value = *store.state.gc.get(reference).unwrap().values.get(index).ok_or(Trap::ArrayOutOfBounds)?; + value.into_wasm(store, storage_type(storage)) + } + + /// Writes one mutable array element after validating its canonical value type. + /// + /// Writes to packed integer elements truncate the input to the element width. + pub fn set(&self, store: &mut Store, index: usize, value: WasmValue) -> Result<()> { + let (reference, type_addr) = rooted_object(store, self, ReferentKind::Array)?; + let field = store.state.get_type(type_addr).as_array().unwrap().field; + if index >= store.state.gc.get(reference).unwrap().values.len() { + return Err(Trap::ArrayOutOfBounds.into()); + } + set_gc_value(store, reference, index, field, value, "array element") + } +} + +fn set_gc_value( + store: &mut Store, + reference: ValueRef, + index: usize, + field: FieldType, + value: WasmValue, + field_name: &'static str, +) -> Result<()> { + if !field.mutable { + return Err(Error::other(format!("{field_name} is immutable"))); + } + let expected = storage_type(field.storage); + if !store.value_matches_type(&value, expected) { + return Err(Error::other(format!("invalid {field_name} value type"))); + } + let value = match (field.storage, value.to_runtime(store)?) { + (StorageType::I8, crate::interpreter::RuntimeValue::Value32(value)) => { + crate::interpreter::RuntimeValue::Value32(value as u8 as u32) + } + (StorageType::I16, crate::interpreter::RuntimeValue::Value32(value)) => { + crate::interpreter::RuntimeValue::Value32(value as u16 as u32) + } + (_, value) => value, + }; + let handle = store.state.gc.handle(reference).ok_or(Trap::InvalidReference)?; + store.state.gc.set(handle, index, value).ok_or_else(|| Trap::InvalidReference.into()) +} + +impl ExnRef { + /// Returns the exception's tag. + pub fn tag(&self, store: &Store) -> Result { + let (_, tag_addr) = exception_object(store, self)?; + Ok(Tag(StoreItem::new(store.store_id(), tag_addr))) + } + + /// Reads one exception payload field. + /// + /// Mutable Store access is required because reference results are registered with the Store. + pub fn field(&self, store: &mut Store, index: usize) -> Result { + let (reference, tag_addr) = exception_object(store, self)?; + let type_addr = store.state.get_tag(tag_addr).type_addr; + let ty = *store + .state + .get_canonical_func_type(type_addr) + .params() + .get(index) + .ok_or_else(|| Error::other("exception payload field out of bounds"))?; + let value = *store + .state + .gc + .get(reference) + .unwrap() + .values + .get(index) + .ok_or_else(|| Error::other("exception payload field out of bounds"))?; + value.into_wasm(store, ty) + } + + /// Reads all exception payload fields. + /// + /// Mutable Store access is required because reference results are registered with the Store. + pub fn fields(&self, store: &mut Store) -> Result> { + let (reference, tag_addr) = exception_object(store, self)?; + let type_addr = store.state.get_tag(tag_addr).type_addr; + let field_count = store.state.gc.get(reference).unwrap().values.len(); + let mut fields = Vec::new(); + fields.try_reserve_exact(field_count).map_err(|_| Trap::OutOfMemory)?; + for index in 0..field_count { + let ty = store.state.get_canonical_func_type(type_addr).params()[index]; + let value = store.state.gc.get(reference).unwrap().values[index]; + fields.push(value.into_wasm(store, ty)?); + } + Ok(fields) + } +} + +fn exception_object(store: &Store, root: &ExnRef) -> Result<(ValueRef, TagAddr)> { + let reference = store.resolve_ref(root)?; + if root.rooted_item().kind != ReferentKind::Exception { + return Err(Trap::InvalidReference.into()); + } + let object = store.state.gc.get(reference).ok_or(Trap::InvalidReference)?; + let crate::store::GcObjectKind::Exception(tag_addr) = object.kind else { + return Err(Trap::InvalidReference.into()); + }; + Ok((reference, tag_addr)) +} + +impl Table { + /// Create a new table in the given store. + pub fn try_new(store: &mut Store, ty: TableType, init: WasmValue) -> Result { + if ty.element_type.is_concrete() { + return Err(Error::other("host tables cannot use module-relative concrete reference types")); + } + let init = table_value_to_element(store, ty.element_type, init).map_err(Error::from)?; + let limiter = store.engine.config().resource_limiter.clone(); + let addr = store.state.tables.len() as TableAddr; + store.state.tables.push(TableInstance::new(ty, init, limiter.as_deref())?); + Ok(Self(StoreItem::new(store.store_id(), addr))) + } + + #[inline] + fn instance<'a>(&self, store: &'a Store) -> Result<&'a TableInstance> { + self.0.validate_store(store)?; + Ok(store.state.get_table(self.0.addr)) + } + + /// Get the type of the table. + pub fn ty(&self, store: &Store) -> Result { + Ok(self.instance(store)?.kind) + } + + /// Get the current number of elements in the table. + pub fn size(&self, store: &Store) -> Result { + Ok(self.instance(store)?.size()) + } + + /// Get a table element as a wasm reference value. + /// + /// Mutable Store access is required because managed results are registered with the Store. + pub fn get(&self, store: &mut Store, index: TableAddr) -> Result { + self.0.validate_store(store)?; + let table = store.state.get_table(self.0.addr); + let value = *table.get(index as usize)?; + let element_type = table.kind.element_type; + Ok(WasmValue::Ref(store.decode_ref(value, element_type)?)) + } + + /// Load a range of table elements and iterate over wasm reference values. + /// + /// Mutable Store access is required because managed results are registered with the Store. + pub fn load(&self, store: &mut Store, offset: usize, len: usize) -> Result> { + self.0.validate_store(store)?; + let table = store.state.get_table(self.0.addr); + let element_type = table.kind.element_type; + let elements = table.load(offset, len)?.to_vec(); + let mut values = Vec::new(); + values.try_reserve_exact(elements.len()).map_err(|_| Trap::OutOfMemory)?; + for value in elements { + values.push(WasmValue::Ref(store.decode_ref(value, element_type)?)); + } + Ok(values.into_iter()) + } + + /// Set a table element. + pub fn set(&self, store: &mut Store, index: TableAddr, value: WasmValue) -> Result<(), Trap> { + self.0.validate_store(store)?; + let element_type = store.state.get_table(self.0.addr).kind.element_type; + let value = table_value_to_element(store, element_type, value)?; + store.state.get_table_mut(self.0.addr).set(index as usize, value) + } + + /// Copy elements within the same table. + pub fn copy_within(&self, store: &mut Store, src: usize, dst: usize, len: usize) -> Result<(), Trap> { + self.0.validate_store(store)?; + store.state.get_table_mut(self.0.addr).copy_within(dst, src, len) + } + + /// Grows the table and returns the previous size. + /// + /// Returns `None` if growth fails or is rejected by the resource limiter. A limiter-provided + /// trap is returned as an error. + pub fn grow(&self, store: &mut Store, delta: i32, init: WasmValue) -> Result> { + self.0.validate_store(store)?; + let table = store.state.get_table(self.0.addr); + let old_size = table.size(); + let init = table_value_to_element(store, table.kind.element_type, init)?; + let Ok(delta) = usize::try_from(delta) else { + return Ok(None); + }; + let limiter = store.engine.config().resource_limiter.clone(); + match store.state.get_table_mut(self.0.addr).grow(delta, init, limiter.as_deref())? { + true => Ok(Some(old_size)), + false => Ok(None), + } + } +} + +impl Global { + /// Create a new global in the given store. + pub fn try_new(store: &mut Store, ty: GlobalType, value: WasmValue) -> Result { + if matches!(ty.ty, WasmType::Ref(ty) if ty.is_concrete()) { + return Err(Error::other("host globals cannot use module-relative concrete reference types")); + } + if !store.value_matches_type(&value, ty.ty) { + cold_path(); + return Err(Error::Other("invalid global value type".to_string())); + } + let value = value.to_runtime(store)?; + let addr = store.state.globals.push(ty, value); + Ok(Self(StoreItem::new(store.store_id(), addr))) + } + + /// Get the type of the global. + pub fn ty(&self, store: &Store) -> Result { + self.0.validate_store(store)?; + Ok(store.state.globals.ty(self.0.addr)) + } + + /// Get the current value of the global. + /// + /// Mutable Store access is required because an owned reference result is registered with the Store. + pub fn get(&self, store: &mut Store) -> Result { + self.0.validate_store(store)?; + let ty = store.state.globals.ty(self.0.addr).ty; + let value = store.state.get_global_internal(self.0.addr); + value.into_wasm(store, ty) + } + + /// Set the current value of the global. + pub fn set(&self, store: &mut Store, value: WasmValue) -> Result<()> { + self.0.validate_store(store)?; + let ty = store.state.globals.ty(self.0.addr).ty; + if !store.value_matches_type(&value, ty) { + return Err(Error::other("invalid global value type")); + } + let value = value.to_runtime(store)?; + store.state.set_global_wasmvalue(self.0.addr, value) + } +} + +impl Tag { + /// Create a new exception tag in the given store. + pub fn try_new(store: &mut Store, ty: FuncType) -> Result { + if !ty.results().is_empty() { + return Err(Error::other("tag types must not have results")); + } + if ty.params().iter().any(|ty| matches!(ty, WasmType::Ref(ty) if ty.is_concrete())) { + return Err(Error::other("host tags cannot use concrete reference types")); + } + let type_addr = store.register_host_type(&ty); + let addr = store.state.tags.len() as TagAddr; + store.state.tags.push(crate::store::TagInstance { type_addr }); + Ok(Self(StoreItem::new(store.store_id(), addr))) + } + + /// Get the payload type of the tag. + pub fn ty<'a>(&self, store: &'a Store) -> Result<&'a FuncType> { + self.0.validate_store(store)?; + Ok(store.state.get_canonical_func_type(store.state.get_tag(self.0.addr).type_addr)) + } +} diff --git a/crates/tinywasm/src/reference/value.rs b/crates/tinywasm/src/reference/value.rs new file mode 100644 index 0000000..bdf7921 --- /dev/null +++ b/crates/tinywasm/src/reference/value.rs @@ -0,0 +1,210 @@ +use core::fmt::Debug; + +use tinywasm_types::{AbstractHeapType, RefType, WasmType}; + +use crate::interpreter::{RuntimeValue, Value128}; +use crate::{AnyRef, ArrayRef, EqRef, ExnRef, ExternRef, FuncRef, I31Ref, RefValue, Result, Store, StructRef}; + +/// A host-facing WebAssembly value. +#[derive(Clone, PartialEq)] +pub enum WasmValue { + /// A 32-bit integer. + I32(i32), + /// A 64-bit integer. + I64(i64), + /// A 32-bit float. + F32(f32), + /// A 64-bit float. + F64(f64), + /// A 128-bit vector. + V128([u8; 16]), + /// A reference. + Ref(RefValue), +} + +impl WasmValue { + pub(crate) fn to_runtime(&self, store: &Store) -> Result { + Ok(match self { + Self::I32(value) => RuntimeValue::Value32(*value as u32), + Self::I64(value) => RuntimeValue::Value64(*value as u64), + Self::F32(value) => RuntimeValue::Value32(value.to_bits()), + Self::F64(value) => RuntimeValue::Value64(value.to_bits()), + Self::V128(value) => RuntimeValue::Value128(Value128(*value)), + Self::Ref(value) => RuntimeValue::ValueRef(store.encode_ref(value)?), + }) + } + + /// Returns this value's broad WebAssembly type, or `None` for null. + pub fn ty(&self) -> Option { + match self { + Self::I32(_) => Some(WasmType::I32), + Self::I64(_) => Some(WasmType::I64), + Self::F32(_) => Some(WasmType::F32), + Self::F64(_) => Some(WasmType::F64), + Self::V128(_) => Some(WasmType::V128), + Self::Ref(RefValue::Null) => None, + Self::Ref(RefValue::Func(_)) => Some(WasmType::Ref(RefType::FUNCREF)), + Self::Ref(RefValue::Extern(_)) => Some(WasmType::Ref(RefType::EXTERNREF)), + Self::Ref(RefValue::Exn(_)) => Some(WasmType::Ref(RefType::EXNREF)), + Self::Ref(RefValue::Any(_)) => Some(WasmType::Ref(RefType::new_abstract(true, AbstractHeapType::Any))), + } + } + + /// Returns whether this value matches a broad or abstract type. + /// + /// A non-null value does not match a concrete reference type because + /// concrete types require Store-specific subtype information. + pub fn matches_type(&self, ty: WasmType) -> bool { + match (self, ty) { + (Self::I32(_), WasmType::I32) + | (Self::I64(_), WasmType::I64) + | (Self::F32(_), WasmType::F32) + | (Self::F64(_), WasmType::F64) + | (Self::V128(_), WasmType::V128) => true, + (Self::Ref(RefValue::Null), WasmType::Ref(ty)) => ty.is_nullable(), + (Self::Ref(RefValue::Func(_)), WasmType::Ref(ty)) => { + ty.abstract_heap_type() == Some(AbstractHeapType::Func) + } + (Self::Ref(RefValue::Extern(_)), WasmType::Ref(ty)) => { + ty.abstract_heap_type() == Some(AbstractHeapType::Extern) + } + (Self::Ref(RefValue::Exn(_)), WasmType::Ref(ty)) => ty.abstract_heap_type() == Some(AbstractHeapType::Exn), + (Self::Ref(RefValue::Any(value)), WasmType::Ref(ty)) => match ty.abstract_heap_type() { + Some(AbstractHeapType::Any) => true, + Some(AbstractHeapType::Eq) => value.as_eq().is_some(), + Some(AbstractHeapType::I31) => value.as_i31().is_some(), + Some(AbstractHeapType::Struct) => value.as_struct().is_some(), + Some(AbstractHeapType::Array) => value.as_array().is_some(), + _ => false, + }, + _ => false, + } + } + + /// Returns the default value for `ty`, or `None` for a non-null reference type. + pub fn default_for(ty: WasmType) -> Option { + match ty { + WasmType::I32 => Some(Self::I32(0)), + WasmType::I64 => Some(Self::I64(0)), + WasmType::F32 => Some(Self::F32(0.0)), + WasmType::F64 => Some(Self::F64(0.0)), + WasmType::V128 => Some(Self::V128([0; 16])), + WasmType::Ref(ty) if ty.is_nullable() => Some(Self::Ref(RefValue::Null)), + WasmType::Ref(_) => None, + } + } + + /// Compares values while treating NaN values with different payloads as equal. + pub fn eq_loose(&self, other: &Self) -> bool { + match (self, other) { + (Self::F32(a), Self::F32(b)) => a.is_nan() && b.is_nan() || a.to_bits() == b.to_bits(), + (Self::F64(a), Self::F64(b)) => a.is_nan() && b.is_nan() || a.to_bits() == b.to_bits(), + (Self::V128(a), Self::V128(b)) => a == b || vector_nan_eq(*a, *b), + _ => self == other, + } + } +} + +fn vector_nan_eq(a: [u8; 16], b: [u8; 16]) -> bool { + let f32_equal = a.as_chunks::<4>().0.iter().zip(b.as_chunks::<4>().0).all(|(a, b)| { + let a = f32::from_le_bytes(*a); + let b = f32::from_le_bytes(*b); + a.is_nan() && b.is_nan() || a.to_bits() == b.to_bits() + }); + if f32_equal && a.as_chunks::<4>().0.iter().any(|v| f32::from_le_bytes(*v).is_nan()) { + return true; + } + a.as_chunks::<8>().0.iter().zip(b.as_chunks::<8>().0).all(|(a, b)| { + let a = f64::from_le_bytes(*a); + let b = f64::from_le_bytes(*b); + a.is_nan() && b.is_nan() || a.to_bits() == b.to_bits() + }) && a.as_chunks::<8>().0.iter().any(|v| f64::from_le_bytes(*v).is_nan()) +} + +impl Debug for WasmValue { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::I32(value) => write!(f, "i32({value})"), + Self::I64(value) => write!(f, "i64({value})"), + Self::F32(value) => write!(f, "f32({value})"), + Self::F64(value) => write!(f, "f64({value})"), + Self::V128(value) => write!(f, "v128({value:?})"), + #[cfg(feature = "debug")] + Self::Ref(value) => write!(f, "ref({value:?})"), + #[cfg(not(feature = "debug"))] + Self::Ref(_) => f.write_str("ref(..)"), + } + } +} + +macro_rules! value_conversions { + ($($ty:ty => $variant:ident, $method:ident);* $(;)?) => {$( + impl WasmValue { + #[doc = concat!("Returns the contained `", stringify!($ty), "` value.")] + pub fn $method(&self) -> Option<$ty> { + if let Self::$variant(value) = self { Some(value.clone()) } else { None } + } + } + impl From<$ty> for WasmValue { fn from(value: $ty) -> Self { Self::$variant(value) } } + impl TryFrom for $ty { + type Error = (); + fn try_from(value: WasmValue) -> Result { + if let WasmValue::$variant(value) = value { Ok(value) } else { Err(()) } + } + } + )*}; +} + +value_conversions! { + i32 => I32, as_i32; + i64 => I64, as_i64; + f32 => F32, as_f32; + f64 => F64, as_f64; + [u8; 16] => V128, as_v128; + RefValue => Ref, as_ref; +} + +macro_rules! ref_conversions { + ($($ty:ty => $variant:ident),* $(,)?) => {$( + impl From<$ty> for WasmValue { fn from(value: $ty) -> Self { Self::Ref(RefValue::$variant(value)) } } + impl TryFrom for $ty { + type Error = (); + fn try_from(value: WasmValue) -> Result { + if let WasmValue::Ref(RefValue::$variant(value)) = value { Ok(value) } else { Err(()) } + } + } + )*}; +} + +ref_conversions!(FuncRef => Func, ExternRef => Extern, AnyRef => Any, ExnRef => Exn); + +macro_rules! any_ref_conversions { + ($($ty:ty => $cast:ident),* $(,)?) => {$( + impl From<$ty> for WasmValue { fn from(value: $ty) -> Self { Self::Ref(RefValue::Any(value.to_any())) } } + impl TryFrom for $ty { + type Error = (); + fn try_from(value: WasmValue) -> Result { + let WasmValue::Ref(RefValue::Any(value)) = value else { return Err(()) }; + value.$cast().ok_or(()) + } + } + )*}; +} + +any_ref_conversions!(EqRef => as_eq, I31Ref => as_i31, StructRef => as_struct, ArrayRef => as_array); + +macro_rules! nullable_conversions { + ($($ty:ty),* $(,)?) => {$( + impl From> for WasmValue { + fn from(value: Option<$ty>) -> Self { value.map(Into::into).unwrap_or(Self::Ref(RefValue::Null)) } + } + impl TryFrom for Option<$ty> { + type Error = (); + fn try_from(value: WasmValue) -> Result { + if matches!(value, WasmValue::Ref(RefValue::Null)) { Ok(None) } else { <$ty>::try_from(value).map(Some) } + } + } + )*}; +} + +nullable_conversions!(FuncRef, AnyRef, EqRef, I31Ref, StructRef, ArrayRef, ExternRef, ExnRef); diff --git a/crates/tinywasm/src/store/const_expr.rs b/crates/tinywasm/src/store/const_expr.rs index 356304b..cd01648 100644 --- a/crates/tinywasm/src/store/const_expr.rs +++ b/crates/tinywasm/src/store/const_expr.rs @@ -2,42 +2,42 @@ use alloc::{format, vec::Vec}; use tinywasm_types::*; use super::{State, default_value}; -use crate::interpreter::{TinyWasmValue, Value128, ValueRef}; +use crate::interpreter::{RuntimeValue, Value128, ValueRef}; use crate::{Error, Result, Trap}; fn resolve(items: &[T], index: u32, kind: &str) -> Result { items.get(index as usize).copied().ok_or_else(|| Error::Other(format!("{kind} {index} not found"))) } -fn pop_value(stack: &mut Vec, storage: StorageType) -> Result { +fn pop_value(stack: &mut Vec, storage: StorageType) -> Result { let value = stack.pop().ok_or_else(|| Error::other("const stack underflow"))?; match (storage, value) { - (StorageType::I8, TinyWasmValue::Value32(value)) => Ok(TinyWasmValue::Value32(value as u8 as u32)), - (StorageType::I16, TinyWasmValue::Value32(value)) => Ok(TinyWasmValue::Value32(value as u16 as u32)), - (StorageType::Value(WasmType::I32 | WasmType::F32), value @ TinyWasmValue::Value32(_)) - | (StorageType::Value(WasmType::I64 | WasmType::F64), value @ TinyWasmValue::Value64(_)) - | (StorageType::Value(WasmType::V128), value @ TinyWasmValue::Value128(_)) - | (StorageType::Value(WasmType::Ref(_)), value @ TinyWasmValue::ValueRef(_)) => Ok(value), + (StorageType::I8, RuntimeValue::Value32(value)) => Ok(RuntimeValue::Value32(value as u8 as u32)), + (StorageType::I16, RuntimeValue::Value32(value)) => Ok(RuntimeValue::Value32(value as u16 as u32)), + (StorageType::Value(WasmType::I32 | WasmType::F32), value @ RuntimeValue::Value32(_)) + | (StorageType::Value(WasmType::I64 | WasmType::F64), value @ RuntimeValue::Value64(_)) + | (StorageType::Value(WasmType::V128), value @ RuntimeValue::Value128(_)) + | (StorageType::Value(WasmType::Ref(_)), value @ RuntimeValue::ValueRef(_)) => Ok(value), _ => Err(Error::other("type mismatch in GC constant")), } } -fn value_ref(value: &TinyWasmValue) -> Option { +fn value_ref(value: &RuntimeValue) -> Option { match value { - TinyWasmValue::ValueRef(value) => Some(*value), + RuntimeValue::ValueRef(value) => Some(*value), _ => None, } } fn alloc_object( state: &mut State, - stack: &mut Vec, + stack: &mut Vec, type_addr: TypeAddr, - values: Vec, + values: Vec, ) -> Result<()> { - let roots = stack.iter().filter_map(value_ref).map(ValueRef::raw); + let roots = stack.iter().filter_map(value_ref); let reference = state.alloc_gc_object(type_addr, values, roots)?; - stack.push(TinyWasmValue::ValueRef(reference)); + stack.push(RuntimeValue::ValueRef(reference)); Ok(()) } @@ -48,33 +48,33 @@ pub(super) fn eval_const( global_addrs: &[GlobalAddr], func_addrs: &[FuncAddr], type_addrs: &[TypeAddr], -) -> Result { +) -> Result { use ConstInstruction::*; if let [instruction] = instructions { match instruction { - I32Const(value) => return Ok(TinyWasmValue::Value32(*value as u32)), - I64Const(value) => return Ok(TinyWasmValue::Value64(*value as u64)), - F32Const(value) => return Ok(TinyWasmValue::Value32(value.to_bits())), - F64Const(value) => return Ok(TinyWasmValue::Value64(value.to_bits())), - V128Const(value) => return Ok(TinyWasmValue::Value128(Value128(*value))), + I32Const(value) => return Ok(RuntimeValue::Value32(*value as u32)), + I64Const(value) => return Ok(RuntimeValue::Value64(*value as u64)), + F32Const(value) => return Ok(RuntimeValue::Value32(value.to_bits())), + F64Const(value) => return Ok(RuntimeValue::Value64(value.to_bits())), + V128Const(value) => return Ok(RuntimeValue::Value128(Value128(*value))), GlobalGet32(index) => { - return Ok(TinyWasmValue::Value32(state.globals.get_32(resolve(global_addrs, *index, "global")?))); + return Ok(RuntimeValue::Value32(state.globals.get_32(resolve(global_addrs, *index, "global")?))); } GlobalGet64(index) => { - return Ok(TinyWasmValue::Value64(state.globals.get_64(resolve(global_addrs, *index, "global")?))); + return Ok(RuntimeValue::Value64(state.globals.get_64(resolve(global_addrs, *index, "global")?))); } GlobalGet128(index) => { - return Ok(TinyWasmValue::Value128(state.globals.get_128(resolve(global_addrs, *index, "global")?))); + return Ok(RuntimeValue::Value128(state.globals.get_128(resolve(global_addrs, *index, "global")?))); } GlobalGetRef(index) => { let value = state.globals.get_32(resolve(global_addrs, *index, "global")?); - return Ok(TinyWasmValue::ValueRef(ValueRef::from_raw(value))); + return Ok(RuntimeValue::ValueRef(ValueRef::from_raw(value))); } - Ref(RefValue::Null) => return Ok(TinyWasmValue::ValueRef(ValueRef::NULL)), - Ref(RefValue::Func(func)) => { - let addr = resolve(func_addrs, func.addr(), "function")?; - return Ok(TinyWasmValue::ValueRef(ValueRef::from_category_addr(addr))); + RefNull(_) => return Ok(RuntimeValue::ValueRef(ValueRef::NULL)), + RefFunc(func) => { + let addr = resolve(func_addrs, func.index(), "function")?; + return Ok(RuntimeValue::ValueRef(ValueRef::from_category_addr(addr))); } _ => {} } @@ -83,62 +83,69 @@ pub(super) fn eval_const( let mut stack = Vec::new(); for instruction in instructions { match instruction { - I32Const(value) => stack.push(TinyWasmValue::Value32(*value as u32)), - I64Const(value) => stack.push(TinyWasmValue::Value64(*value as u64)), - F32Const(value) => stack.push(TinyWasmValue::Value32(value.to_bits())), - F64Const(value) => stack.push(TinyWasmValue::Value64(value.to_bits())), - V128Const(value) => stack.push(TinyWasmValue::Value128(Value128(*value))), + I32Const(value) => stack.push(RuntimeValue::Value32(*value as u32)), + I64Const(value) => stack.push(RuntimeValue::Value64(*value as u64)), + F32Const(value) => stack.push(RuntimeValue::Value32(value.to_bits())), + F64Const(value) => stack.push(RuntimeValue::Value64(value.to_bits())), + V128Const(value) => stack.push(RuntimeValue::Value128(Value128(*value))), GlobalGet32(index) => { - stack.push(TinyWasmValue::Value32(state.globals.get_32(resolve(global_addrs, *index, "global")?))); + stack.push(RuntimeValue::Value32(state.globals.get_32(resolve(global_addrs, *index, "global")?))); } GlobalGet64(index) => { - stack.push(TinyWasmValue::Value64(state.globals.get_64(resolve(global_addrs, *index, "global")?))); + stack.push(RuntimeValue::Value64(state.globals.get_64(resolve(global_addrs, *index, "global")?))); } GlobalGet128(index) => { - stack.push(TinyWasmValue::Value128(state.globals.get_128(resolve(global_addrs, *index, "global")?))); + stack.push(RuntimeValue::Value128(state.globals.get_128(resolve(global_addrs, *index, "global")?))); } GlobalGetRef(index) => { let value = state.globals.get_32(resolve(global_addrs, *index, "global")?); - stack.push(TinyWasmValue::ValueRef(ValueRef::from_raw(value))); + stack.push(RuntimeValue::ValueRef(ValueRef::from_raw(value))); } - Ref(RefValue::Null) => stack.push(TinyWasmValue::ValueRef(ValueRef::NULL)), - Ref(RefValue::Func(func)) => { - let addr = resolve(func_addrs, func.addr(), "function")?; - stack.push(TinyWasmValue::ValueRef(ValueRef::from_category_addr(addr))); - } - Ref(_) => { - return cold!(Err(Error::other("unsupported reference constant"))); + RefNull(_) => stack.push(RuntimeValue::ValueRef(ValueRef::NULL)), + RefFunc(func) => { + let addr = resolve(func_addrs, func.index(), "function")?; + stack.push(RuntimeValue::ValueRef(ValueRef::from_category_addr(addr))); } RefI31 => { let value = stack.pop().ok_or_else(|| Error::other("const stack underflow"))?; - let TinyWasmValue::Value32(value) = value else { + let RuntimeValue::Value32(value) = value else { return Err(Error::other("type mismatch in const ref.i31")); }; - stack.push(TinyWasmValue::ValueRef(ValueRef::from_i31(value as i32))); + stack.push(RuntimeValue::ValueRef(ValueRef::from_i31(value as i32))); } AnyConvertExtern | ExternConvertAny => { let value = stack.pop().ok_or_else(|| Error::other("const stack underflow"))?; - if !matches!(value, TinyWasmValue::ValueRef(_)) { + if !matches!(value, RuntimeValue::ValueRef(_)) { return Err(Error::other("type mismatch in const reference conversion")); } stack.push(value); } StructNew(type_index) | StructNewDefault(type_index) => { let type_addr = resolve(type_addrs, *type_index, "type")?; - let fields = state + let field_count = state .get_type(type_addr) .as_struct() .ok_or_else(|| Error::other("GC constant type is not a struct"))? .fields - .as_ref(); + .len(); let default = matches!(instruction, StructNewDefault(_)); + state.check_gc_allocation(type_addr, field_count)?; let mut values = Vec::new(); - cold_err!(values.try_reserve_exact(fields.len())).map_err(|_| Trap::OutOfMemory)?; + cold_err!(values.try_reserve_exact(field_count)).map_err(|_| Trap::OutOfMemory)?; if default { - values.extend(fields.iter().map(|field| default_value(field.storage))); + values.extend( + state + .get_type(type_addr) + .as_struct() + .unwrap() + .fields + .iter() + .map(|field| default_value(field.storage)), + ); } else { - for field in fields.iter().rev() { - values.push(pop_value(&mut stack, field.storage)?); + for index in (0..field_count).rev() { + let storage = state.get_type(type_addr).as_struct().unwrap().fields[index].storage; + values.push(pop_value(&mut stack, storage)?); } values.reverse(); } @@ -152,9 +159,10 @@ pub(super) fn eval_const( .ok_or_else(|| Error::other("GC constant type is not an array"))? .field .storage; - let Some(TinyWasmValue::Value32(len)) = stack.pop() else { + let Some(RuntimeValue::Value32(len)) = stack.pop() else { return Err(Error::other("type mismatch in const array length")); }; + state.check_gc_allocation(type_addr, len as usize)?; let value = if matches!(instruction, ArrayNewDefault(_)) { default_value(storage) } else { @@ -173,6 +181,7 @@ pub(super) fn eval_const( .ok_or_else(|| Error::other("GC constant type is not an array"))? .field .storage; + state.check_gc_allocation(type_addr, *len as usize)?; let mut values = Vec::new(); cold_err!(values.try_reserve_exact(*len as usize)).map_err(|_| Trap::OutOfMemory)?; for _ in 0..*len { @@ -184,7 +193,7 @@ pub(super) fn eval_const( I32Add | I32Sub | I32Mul => { let rhs = stack.pop().ok_or_else(|| Error::other("const stack underflow"))?; let lhs = stack.pop().ok_or_else(|| Error::other("const stack underflow"))?; - let (TinyWasmValue::Value32(lhs), TinyWasmValue::Value32(rhs)) = (lhs, rhs) else { + let (RuntimeValue::Value32(lhs), RuntimeValue::Value32(rhs)) = (lhs, rhs) else { return cold!(Err(Error::other("type mismatch in const i32 op"))); }; let out = match instruction { @@ -193,12 +202,12 @@ pub(super) fn eval_const( I32Mul => (lhs as i32).wrapping_mul(rhs as i32), _ => unreachable!(), }; - stack.push(TinyWasmValue::Value32(out as u32)); + stack.push(RuntimeValue::Value32(out as u32)); } I64Add | I64Sub | I64Mul => { let rhs = stack.pop(); let lhs = stack.pop(); - let (Some(TinyWasmValue::Value64(lhs)), Some(TinyWasmValue::Value64(rhs))) = (lhs, rhs) else { + let (Some(RuntimeValue::Value64(lhs)), Some(RuntimeValue::Value64(rhs))) = (lhs, rhs) else { return cold!(Err(Error::other("type mismatch in const i64 op"))); }; let out = match instruction { @@ -207,7 +216,7 @@ pub(super) fn eval_const( I64Mul => (lhs as i64).wrapping_mul(rhs as i64), _ => unreachable!(), }; - stack.push(TinyWasmValue::Value64(out as u64)); + stack.push(RuntimeValue::Value64(out as u64)); } } } diff --git a/crates/tinywasm/src/store/exception.rs b/crates/tinywasm/src/store/exception.rs deleted file mode 100644 index 0725cda..0000000 --- a/crates/tinywasm/src/store/exception.rs +++ /dev/null @@ -1,10 +0,0 @@ -use alloc::boxed::Box; -use tinywasm_types::TagAddr; - -use crate::interpreter::TinyWasmValue; - -#[cfg_attr(feature = "debug", derive(Debug))] -pub(crate) struct ExceptionInstance { - pub(crate) tag_addr: TagAddr, - pub(crate) payload: Box<[TinyWasmValue]>, -} diff --git a/crates/tinywasm/src/store/function.rs b/crates/tinywasm/src/store/function.rs index 3580e9d..3c716a7 100644 --- a/crates/tinywasm/src/store/function.rs +++ b/crates/tinywasm/src/store/function.rs @@ -10,17 +10,9 @@ use crate::func::HostFunction; #[cfg_attr(feature = "debug", derive(Debug))] pub(crate) struct FunctionInstance { pub(crate) type_addr: TypeAddr, - pub(crate) gc: FunctionGcMetadata, pub(crate) kind: FunctionKind, } -#[derive(Clone, Copy)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub(crate) struct FunctionGcMetadata { - pub(crate) params: bool, - pub(crate) results: bool, -} - #[derive(Clone)] #[cfg_attr(feature = "debug", derive(Debug))] pub(crate) enum FunctionKind { diff --git a/crates/tinywasm/src/store/gc/mod.rs b/crates/tinywasm/src/store/gc/mod.rs index 6379357..adccd45 100644 --- a/crates/tinywasm/src/store/gc/mod.rs +++ b/crates/tinywasm/src/store/gc/mod.rs @@ -1,58 +1,56 @@ mod arena; mod object; +mod roots; use alloc::vec::Vec; use tinywasm_types::{StorageType, WasmType}; use crate::Trap; use crate::interpreter::stack::ValueStack; -use crate::interpreter::{InternalValue, TinyWasmValue, Value128, ValueRef}; +use crate::interpreter::{InternalValue, RuntimeValue, Value128, ValueRef}; pub(crate) use arena::{AllocError, Arena, Handle, Trace}; -pub(crate) use object::{GcHeap, GcObject}; +pub(crate) use object::{GcHeap, GcObjectKind}; +pub(crate) use roots::Roots; /// Returns the zero value for a GC field or array element. -pub(crate) fn default_value(storage: StorageType) -> TinyWasmValue { +pub(crate) fn default_value(storage: StorageType) -> RuntimeValue { match storage { StorageType::I8 | StorageType::I16 | StorageType::Value(WasmType::I32 | WasmType::F32) => { - TinyWasmValue::Value32(0) + RuntimeValue::Value32(0) } - StorageType::Value(WasmType::I64 | WasmType::F64) => TinyWasmValue::Value64(0), - StorageType::Value(WasmType::V128) => TinyWasmValue::Value128(Value128([0; 16])), - StorageType::Value(WasmType::Ref(_)) => TinyWasmValue::ValueRef(ValueRef::NULL), + StorageType::Value(WasmType::I64 | WasmType::F64) => RuntimeValue::Value64(0), + StorageType::Value(WasmType::V128) => RuntimeValue::Value128(Value128([0; 16])), + StorageType::Value(WasmType::Ref(_)) => RuntimeValue::ValueRef(ValueRef::NULL), } } /// Pops and packs a GC field or array element from the operand stack. -pub(crate) fn pop_value(stack: &mut ValueStack, storage: StorageType) -> TinyWasmValue { +pub(crate) fn pop_value(stack: &mut ValueStack, storage: StorageType) -> RuntimeValue { match storage { - StorageType::I8 => TinyWasmValue::Value32(i32::stack_pop(stack) as u8 as u32), - StorageType::I16 => TinyWasmValue::Value32(i32::stack_pop(stack) as u16 as u32), - StorageType::Value(WasmType::I32 | WasmType::F32) => TinyWasmValue::Value32(u32::stack_pop(stack)), - StorageType::Value(WasmType::I64 | WasmType::F64) => TinyWasmValue::Value64(u64::stack_pop(stack)), - StorageType::Value(WasmType::V128) => TinyWasmValue::Value128(Value128::stack_pop(stack)), - StorageType::Value(WasmType::Ref(_)) => TinyWasmValue::ValueRef(ValueRef::stack_pop(stack)), + StorageType::I8 => RuntimeValue::Value32(i32::stack_pop(stack) as u8 as u32), + StorageType::I16 => RuntimeValue::Value32(i32::stack_pop(stack) as u16 as u32), + StorageType::Value(WasmType::I32 | WasmType::F32) => RuntimeValue::Value32(u32::stack_pop(stack)), + StorageType::Value(WasmType::I64 | WasmType::F64) => RuntimeValue::Value64(u64::stack_pop(stack)), + StorageType::Value(WasmType::V128) => RuntimeValue::Value128(Value128::stack_pop(stack)), + StorageType::Value(WasmType::Ref(_)) => RuntimeValue::ValueRef(ValueRef::stack_pop(stack)), } } /// Extends a packed value and pushes it onto the operand stack. pub(crate) fn push_value( stack: &mut ValueStack, - value: TinyWasmValue, + value: RuntimeValue, storage: StorageType, signed: Option, ) -> Result<(), Trap> { let value = match (value, storage, signed) { - (TinyWasmValue::Value32(value), StorageType::I8, Some(true)) => { - TinyWasmValue::Value32(value as i8 as i32 as u32) - } - (TinyWasmValue::Value32(value), StorageType::I16, Some(true)) => { - TinyWasmValue::Value32(value as i16 as i32 as u32) - } - (TinyWasmValue::Value32(value), StorageType::I8, Some(false)) => TinyWasmValue::Value32(value & u8::MAX as u32), - (TinyWasmValue::Value32(value), StorageType::I16, Some(false)) => { - TinyWasmValue::Value32(value & u16::MAX as u32) + (RuntimeValue::Value32(value), StorageType::I8, Some(true)) => RuntimeValue::Value32(value as i8 as i32 as u32), + (RuntimeValue::Value32(value), StorageType::I16, Some(true)) => { + RuntimeValue::Value32(value as i16 as i32 as u32) } + (RuntimeValue::Value32(value), StorageType::I8, Some(false)) => RuntimeValue::Value32(value & u8::MAX as u32), + (RuntimeValue::Value32(value), StorageType::I16, Some(false)) => RuntimeValue::Value32(value & u16::MAX as u32), (value, _, None) => value, _ => unreachable!("validated packed field access"), }; @@ -65,7 +63,7 @@ pub(crate) fn decode_data( data: &[u8], src: usize, len: usize, -) -> Result, Trap> { +) -> Result, Trap> { let width = match storage { StorageType::I8 => 1, StorageType::I16 => 2, @@ -81,11 +79,11 @@ pub(crate) fn decode_data( let mut values = Vec::new(); cold_err!(values.try_reserve_exact(len)).map_err(|_| Trap::OutOfMemory)?; values.extend(data[src..end].chunks_exact(width).map(|bytes| match width { - 1 => TinyWasmValue::Value32(u32::from(bytes[0])), - 2 => TinyWasmValue::Value32(u32::from(u16::from_le_bytes(bytes.try_into().unwrap()))), - 4 => TinyWasmValue::Value32(u32::from_le_bytes(bytes.try_into().unwrap())), - 8 => TinyWasmValue::Value64(u64::from_le_bytes(bytes.try_into().unwrap())), - 16 => TinyWasmValue::Value128(Value128(<[u8; 16]>::try_from(bytes).unwrap())), + 1 => RuntimeValue::Value32(u32::from(bytes[0])), + 2 => RuntimeValue::Value32(u32::from(u16::from_le_bytes(bytes.try_into().unwrap()))), + 4 => RuntimeValue::Value32(u32::from_le_bytes(bytes.try_into().unwrap())), + 8 => RuntimeValue::Value64(u64::from_le_bytes(bytes.try_into().unwrap())), + 16 => RuntimeValue::Value128(Value128(<[u8; 16]>::try_from(bytes).unwrap())), _ => unreachable!(), })); Ok(values) diff --git a/crates/tinywasm/src/store/gc/object.rs b/crates/tinywasm/src/store/gc/object.rs index ca22338..cf27cd5 100644 --- a/crates/tinywasm/src/store/gc/object.rs +++ b/crates/tinywasm/src/store/gc/object.rs @@ -1,12 +1,11 @@ use alloc::{boxed::Box, sync::Arc, vec::Vec}; -use core::cell::RefCell; use core::mem::size_of; use core::sync::atomic::{AtomicU32, Ordering}; -use tinywasm_types::TypeAddr; +use tinywasm_types::{TagAddr, TypeAddr}; use crate::engine::Config; -use crate::interpreter::{TinyWasmValue, ValueRef}; +use crate::interpreter::{RuntimeValue, ValueRef}; use crate::{ResourceLimiter, Trap}; use super::{AllocError, Arena, Handle, Trace}; @@ -14,11 +13,17 @@ use super::{AllocError, Arena, Handle, Trace}; static NEXT_GC_REF: AtomicU32 = AtomicU32::new(0); pub(crate) struct GcObject { - pub(crate) type_addr: TypeAddr, - pub(crate) values: Box<[TinyWasmValue]>, + pub(crate) kind: GcObjectKind, + pub(crate) values: Box<[RuntimeValue]>, references: Option]>>, } +#[derive(Clone, Copy)] +pub(crate) enum GcObjectKind { + Composite(TypeAddr), + Exception(TagAddr), +} + impl Trace for GcObject { fn trace(&self, mark: &mut impl FnMut(Handle)) { if let Some(references) = &self.references { @@ -30,7 +35,6 @@ impl Trace for GcObject { pub(crate) struct GcHeap { objects: Arena, directory: Vec<(u32, Handle)>, - pinned: RefCell>, resource_limiter: Option>, } @@ -46,7 +50,6 @@ impl GcHeap { Self { objects: Arena::new(config.gc_collection_threshold), directory: Vec::new(), - pinned: RefCell::new(Vec::new()), resource_limiter: config.resource_limiter.clone(), } } @@ -65,19 +68,13 @@ impl GcHeap { } #[inline] - pub(crate) fn get_mut(&mut self, value: ValueRef) -> Option<&mut GcObject> { - self.objects.get_mut(self.handle(value)?) + pub(crate) fn get_handle(&self, handle: Handle) -> Option<&GcObject> { + self.objects.get(handle) } - /// Allocates an object and returns its compact runtime reference. - pub(crate) fn alloc( - &mut self, - type_addr: TypeAddr, - values: Vec, - trace_references: bool, - ) -> Result { - let element_size = size_of::() + if trace_references { size_of::>() } else { 0 }; - let out_of_line_bytes = values.len().checked_mul(element_size).ok_or(Trap::OutOfMemory)?; + pub(crate) fn check_allocation(&self, value_count: usize, trace_references: bool) -> Result<(), Trap> { + let element_size = size_of::() + if trace_references { size_of::>() } else { 0 }; + let out_of_line_bytes = value_count.checked_mul(element_size).ok_or(Trap::OutOfMemory)?; let allocation_size = Arena::::allocation_size(out_of_line_bytes).ok_or(Trap::OutOfMemory)?; let desired = self.objects.allocated_bytes.checked_add(allocation_size).ok_or(Trap::OutOfMemory)?; if let Some(limiter) = &self.resource_limiter @@ -85,7 +82,38 @@ impl GcHeap { { return Err(Trap::OutOfMemory); } + Ok(()) + } + + /// Allocates an object and returns its compact runtime reference. + pub(crate) fn alloc( + &mut self, + type_addr: TypeAddr, + values: Vec, + trace_references: bool, + ) -> Result { + self.alloc_kind(GcObjectKind::Composite(type_addr), values, trace_references, None) + } + + /// Allocates an exception and traces references in its payload. + pub(crate) fn alloc_exception( + &mut self, + tag_addr: TagAddr, + payload: Vec, + trace_fields: &[bool], + ) -> Result { + self.alloc_kind(GcObjectKind::Exception(tag_addr), payload, true, Some(trace_fields)) + } + fn alloc_kind( + &mut self, + kind: GcObjectKind, + values: Vec, + trace_references: bool, + trace_fields: Option<&[bool]>, + ) -> Result { + let element_size = size_of::() + if trace_references { size_of::>() } else { 0 }; + let out_of_line_bytes = values.len().checked_mul(element_size).ok_or(Trap::OutOfMemory)?; let key = NEXT_GC_REF.try_update(Ordering::Relaxed, Ordering::Relaxed, |key| (key < (1 << 30)).then_some(key + 1)); let Ok(key) = key else { @@ -94,27 +122,27 @@ impl GcHeap { let references = if trace_references { let mut references = Vec::new(); references.try_reserve_exact(values.len()).map_err(|_| Trap::OutOfMemory)?; - references.extend(values.iter().map(|value| match value { - TinyWasmValue::ValueRef(value) => self.handle(*value), + references.extend(values.iter().enumerate().map(|(index, value)| match value { + RuntimeValue::ValueRef(value) if trace_fields.is_none_or(|fields| fields[index]) => self.handle(*value), _ => None, })); Some(references.into_boxed_slice()) } else { None }; - let object = GcObject { type_addr, values: values.into_boxed_slice(), references }; + let object = GcObject { kind, values: values.into_boxed_slice(), references }; self.directory.try_reserve(1).map_err(|_| Trap::OutOfMemory)?; let handle = self.objects.alloc(object, out_of_line_bytes).map_err(|_| Trap::OutOfMemory)?; self.directory.push((key, handle)); Ok(ValueRef::from_category_addr(key)) } - pub(crate) fn set(&mut self, object: ValueRef, index: usize, value: TinyWasmValue) -> Option<()> { + pub(crate) fn set(&mut self, object: Handle, index: usize, value: RuntimeValue) -> Option<()> { let reference = match value { - TinyWasmValue::ValueRef(value) => self.handle(value), + RuntimeValue::ValueRef(value) => self.handle(value), _ => None, }; - let object = self.get_mut(object)?; + let object = self.objects.get_mut(object)?; *object.values.get_mut(index)? = value; if let Some(references) = &mut object.references { references[index] = reference; @@ -123,20 +151,17 @@ impl GcHeap { } /// Replaces a contiguous range and updates its traced references. - pub(crate) fn set_slice(&mut self, object: ValueRef, index: usize, values: &[TinyWasmValue]) -> Option<()> { - let object_handle = self.handle(object)?; + pub(crate) fn set_slice(&mut self, object: Handle, index: usize, values: &[RuntimeValue]) -> Option<()> { let end = index.checked_add(values.len())?; let directory = &self.directory; - let object = self.objects.get_mut(object_handle)?; + let object = self.objects.get_mut(object)?; object.values.get_mut(index..end)?.copy_from_slice(values); if let Some(references) = &mut object.references { for (reference, value) in references[index..end].iter_mut().zip(values) { *reference = match value { - TinyWasmValue::ValueRef(value) => { - let key = value.addr(); - key.and_then(|key| directory.binary_search_by_key(&key, |entry| entry.0).ok()) - .map(|index| directory[index].1) - } + RuntimeValue::ValueRef(value) => value.addr().and_then(|key| { + directory.binary_search_by_key(&key, |entry| entry.0).ok().map(|index| directory[index].1) + }), _ => None, }; } @@ -145,17 +170,12 @@ impl GcHeap { } /// Fills a contiguous range and updates its traced references. - pub(crate) fn fill( - &mut self, - object: ValueRef, - range: core::ops::Range, - value: TinyWasmValue, - ) -> Option<()> { + pub(crate) fn fill(&mut self, object: Handle, range: core::ops::Range, value: RuntimeValue) -> Option<()> { let reference = match value { - TinyWasmValue::ValueRef(value) => self.handle(value), + RuntimeValue::ValueRef(value) => self.handle(value), _ => None, }; - let object = self.get_mut(object)?; + let object = self.objects.get_mut(object)?; object.values.get_mut(range.clone())?.fill(value); if let Some(references) = &mut object.references { references[range].fill(reference); @@ -164,36 +184,24 @@ impl GcHeap { } pub(crate) fn should_collect(&self, value_count: usize, trace_references: bool) -> bool { - let element_size = size_of::() + if trace_references { size_of::>() } else { 0 }; + let element_size = size_of::() + if trace_references { size_of::>() } else { 0 }; self.objects.should_collect(value_count.saturating_mul(element_size)) } - /// Reclaims objects unreachable from runtime and permanent host roots. + /// Reclaims objects unreachable from runtime roots. pub(crate) fn collect(&mut self, roots: impl IntoIterator) -> Result<(), AllocError> { - let pinned = self.pinned.borrow(); let directory = &self.directory; let root_handles = roots.into_iter().filter_map(|value| { let key = value.addr()?; Some(directory.get(directory.binary_search_by_key(&key, |entry| entry.0).ok()?)?.1) }); - self.objects.collect(pinned.iter().copied().chain(root_handles))?; - drop(pinned); + self.objects.collect(root_handles)?; self.directory.retain(|(_, handle)| self.objects.get(*handle).is_some()); Ok(()) } - /// Permanently roots a managed reference exposed through the copyable host API. - pub(crate) fn pin(&self, value: ValueRef) { - if let Some(handle) = self.handle(value) { - let mut pinned = self.pinned.borrow_mut(); - if let Err(index) = pinned.binary_search(&handle) { - pinned.insert(index, handle); - } - } - } - - pub(crate) fn copy_within(&mut self, object: ValueRef, src: core::ops::Range, dst: usize) -> Option<()> { - let object = self.get_mut(object)?; + pub(crate) fn copy_within(&mut self, object: Handle, src: core::ops::Range, dst: usize) -> Option<()> { + let object = self.objects.get_mut(object)?; object.values.copy_within(src.clone(), dst); if let Some(references) = &mut object.references { references.copy_within(src, dst); @@ -204,13 +212,11 @@ impl GcHeap { /// Copies values and tracing metadata between two distinct objects. pub(crate) fn copy_between( &mut self, - src: ValueRef, + src: Handle, src_range: core::ops::Range, - dst: ValueRef, + dst: Handle, dst_index: usize, ) -> Option<()> { - let src = self.handle(src)?; - let dst = self.handle(dst)?; let (src, dst) = self.objects.get_disjoint_mut(src, dst)?; let src_values = src.values.get(src_range.clone())?; let dst_end = dst_index.checked_add(src_values.len())?; @@ -254,13 +260,27 @@ mod tests { #[test] fn collection_reclaims_object_cycles() { let mut heap = GcHeap::default(); - let first = heap.alloc(0, alloc::vec![TinyWasmValue::ValueRef(ValueRef::NULL)], true).unwrap(); - let second = heap.alloc(0, alloc::vec![TinyWasmValue::ValueRef(first)], true).unwrap(); - heap.set(first, 0, TinyWasmValue::ValueRef(second)).unwrap(); + let first = heap.alloc(0, alloc::vec![RuntimeValue::ValueRef(ValueRef::NULL)], true).unwrap(); + let second = heap.alloc(0, alloc::vec![RuntimeValue::ValueRef(first)], true).unwrap(); + heap.set(heap.handle(first).unwrap(), 0, RuntimeValue::ValueRef(second)).unwrap(); heap.collect([]).unwrap(); assert!(heap.get(first).is_none()); assert!(heap.get(second).is_none()); } + + #[test] + fn exception_payload_traces_managed_objects() { + let mut heap = GcHeap::default(); + let payload = heap.alloc(0, Vec::new(), false).unwrap(); + let exception = heap.alloc_exception(0, alloc::vec![RuntimeValue::ValueRef(payload)], &[true]).unwrap(); + + heap.collect([exception]).unwrap(); + assert!(heap.get(payload).is_some()); + + heap.collect([]).unwrap(); + assert!(heap.get(exception).is_none()); + assert!(heap.get(payload).is_none()); + } } diff --git a/crates/tinywasm/src/store/gc/roots.rs b/crates/tinywasm/src/store/gc/roots.rs new file mode 100644 index 0000000..7107295 --- /dev/null +++ b/crates/tinywasm/src/store/gc/roots.rs @@ -0,0 +1,58 @@ +use alloc::{sync::Arc, sync::Weak, vec::Vec}; + +use crate::Trap; +use crate::interpreter::ValueRef; + +pub(crate) struct Roots { + entries: Vec>, + free: Vec, +} + +struct Root { + value: ValueRef, + token: Weak<()>, +} + +impl Roots { + pub(crate) const fn new() -> Self { + Self { entries: Vec::new(), free: Vec::new() } + } + + pub(crate) fn reserve(&mut self, additional: usize) -> Result<(), Trap> { + self.remove_dead(); + let needed = additional.saturating_sub(self.free.len()); + self.entries.try_reserve(needed).map_err(|_| Trap::OutOfMemory) + } + + pub(crate) fn insert(&mut self, value: ValueRef) -> Result, Trap> { + if self.free.is_empty() && self.entries.len() == self.entries.capacity() { + self.remove_dead(); + } + let index = if let Some(index) = self.free.pop() { + index + } else { + let index = u32::try_from(self.entries.len()).map_err(|_| Trap::OutOfMemory)?; + self.entries.try_reserve(1).map_err(|_| Trap::OutOfMemory)?; + self.entries.push(None); + index + }; + + let token = Arc::new(()); + self.entries[index as usize] = Some(Root { value, token: Arc::downgrade(&token) }); + Ok(token) + } + + pub(crate) fn values(&mut self) -> impl Iterator + '_ { + self.remove_dead(); + self.entries.iter().flatten().map(|root| root.value) + } + + fn remove_dead(&mut self) { + for (index, root) in self.entries.iter_mut().enumerate() { + if root.as_ref().is_some_and(|root| root.token.strong_count() == 0) { + *root = None; + self.free.push(index as u32); + } + } + } +} diff --git a/crates/tinywasm/src/store/global.rs b/crates/tinywasm/src/store/global.rs index 69e538f..4052eb8 100644 --- a/crates/tinywasm/src/store/global.rs +++ b/crates/tinywasm/src/store/global.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; use tinywasm_types::*; -use crate::interpreter::{TinyWasmValue, Value32, Value64, Value128}; +use crate::interpreter::{RuntimeValue, Value32, Value64, Value128}; struct GlobalLane { values: Vec, @@ -98,18 +98,18 @@ impl Globals { } /// Adds a global and returns its packed store address. - pub(crate) fn push(&mut self, ty: GlobalType, value: TinyWasmValue) -> GlobalAddr { + pub(crate) fn push(&mut self, ty: GlobalType, value: RuntimeValue) -> GlobalAddr { match (ty.ty, value) { - (WasmType::I32 | WasmType::F32, TinyWasmValue::Value32(value)) => { + (WasmType::I32 | WasmType::F32, RuntimeValue::Value32(value)) => { Self::addr(Self::LANE_32, self.globals_32.push(ty, value)) } - (WasmType::Ref(_), TinyWasmValue::ValueRef(value)) => { + (WasmType::Ref(_), RuntimeValue::ValueRef(value)) => { Self::addr(Self::LANE_32, self.globals_32.push(ty, value.raw())) } - (WasmType::I64 | WasmType::F64, TinyWasmValue::Value64(value)) => { + (WasmType::I64 | WasmType::F64, RuntimeValue::Value64(value)) => { Self::addr(Self::LANE_64, self.globals_64.push(ty, value)) } - (WasmType::V128, TinyWasmValue::Value128(value)) => { + (WasmType::V128, RuntimeValue::Value128(value)) => { Self::addr(Self::LANE_128, self.globals_128.push(ty, value)) } _ => unreachable!("global value does not match its declared type"), diff --git a/crates/tinywasm/src/store/mod.rs b/crates/tinywasm/src/store/mod.rs index 2ddd43c..d3f1479 100644 --- a/crates/tinywasm/src/store/mod.rs +++ b/crates/tinywasm/src/store/mod.rs @@ -1,17 +1,16 @@ use alloc::{boxed::Box, format, vec::Vec}; use core::hint::cold_path; -use core::sync::atomic::{AtomicU32, Ordering}; use tinywasm_types::*; use crate::func::FromWasmValues; use crate::interpreter::stack::{CallStack, StackBase, ValueStack}; -use crate::interpreter::{TinyWasmValue, ValueRef}; -use crate::{Engine, Error, ModuleInstance, Result, Trap}; +use crate::interpreter::{RuntimeValue, ValueRef}; +use crate::reference::{ReferentKind, RootedItem, StoreId, StoredRef}; +use crate::{Engine, Error, ExnRef, FuncRef, ModuleInstance, RefValue, Result, Trap, WasmValue}; mod const_expr; mod data; mod element; -mod exception; mod function; mod gc; mod global; @@ -22,14 +21,11 @@ mod tag; mod types; use const_expr::eval_const; -pub(crate) use gc::{decode_data, default_value, pop_value, push_value}; +pub(crate) use gc::{GcObjectKind, decode_data, default_value, pop_value, push_value}; pub(crate) use memory::{MemValue, MemoryInstance}; pub(crate) use state::State; pub(crate) use types::{canonicalize_ref_type, canonicalize_value_type}; -pub(crate) use {data::*, element::*, exception::*, function::*, global::*, table::*, tag::*}; - -// global store id counter -static STORE_ID: AtomicU32 = AtomicU32::new(0); +pub(crate) use {data::*, element::*, function::*, global::*, table::*, tag::*}; /// Controls resource usage by WebAssembly instances. /// @@ -60,7 +56,7 @@ static STORE_ID: AtomicU32 = AtomicU32::new(0); /// /// let config = Config::new().with_resource_limiter(Arc::new(MemoryLimit(64 * 1024))); /// let mut store = Store::new(Engine::new(config)); -/// let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?; +/// let memory = Memory::try_new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?; /// assert_eq!(memory.grow(&mut store, 1)?, None); /// # Ok::<(), tinywasm::Error>(()) /// ``` @@ -97,9 +93,9 @@ pub trait ResourceLimiter: Send + Sync { /// Returns whether a GC object allocation is allowed. /// - /// Sizes are the logical bytes retained by live GC objects after collection. `maximum` is - /// currently always `None`. `Ok(false)` rejects the allocation with [`Trap::OutOfMemory`], while - /// `Err` returns the provided trap. The default implementation allows the request. + /// Sizes are logical allocated bytes before and after the requested allocation. Unreachable + /// objects remain included until collection. `maximum` is currently always `None`. `Ok(false)` + /// rejects the allocation with [`Trap::OutOfMemory`], while `Err` returns the provided trap. fn gc_growing( &self, _current: usize, @@ -124,7 +120,7 @@ pub trait ResourceLimiter: Send + Sync { /// /// See pub struct Store { - id: u32, + id: StoreId, pub(crate) module_instances: Vec, pub(crate) engine: Engine, @@ -136,6 +132,52 @@ pub struct Store { pub(crate) host_params: Vec, } +#[derive(Clone, Copy)] +pub(crate) enum FuncValueTypes { + Params, + Results, +} + +pub(crate) struct StackValueIter<'a> { + store: &'a mut Store, + type_addr: TypeAddr, + types: FuncValueTypes, + index: StackBase, + position: usize, + len: usize, +} + +impl Iterator for StackValueIter<'_> { + type Item = WasmValue; + + fn next(&mut self) -> Option { + if self.position == self.len { + return None; + } + let ty = { + let func_ty = self.store.state.get_canonical_func_type(self.type_addr); + match self.types { + FuncValueTypes::Params => func_ty.params()[self.position], + FuncValueTypes::Results => func_ty.results()[self.position], + } + }; + self.position += 1; + let value = self.store.stack_value(ty, &mut self.index); + Some( + value + .into_wasm(self.store, ty) + .unwrap_or_else(|_| unreachable!("invalid internal value at a typed host boundary")), + ) + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.len - self.position; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for StackValueIter<'_> {} + #[cfg(feature = "debug")] impl core::fmt::Debug for Store { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { @@ -162,8 +204,7 @@ impl Default for Store { impl Store { /// Create a new store pub fn new(engine: Engine) -> Self { - let id = - STORE_ID.try_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1)).expect("too many stores"); + let id = StoreId::fresh(); let state = State::new(engine.config()); Self { id, @@ -180,9 +221,154 @@ impl Store { /// Get the store's ID (unique per process) pub fn id(&self) -> u32 { + self.id.get() + } + + pub(crate) const fn store_id(&self) -> StoreId { self.id } + pub(crate) fn root_reference(&mut self, value: ValueRef, kind: ReferentKind) -> Result { + let token = match kind { + ReferentKind::Struct | ReferentKind::Array | ReferentKind::Exception => { + Some(self.state.roots.insert(value)?) + } + ReferentKind::I31 | ReferentKind::HostExtern => None, + }; + Ok(T::from_rooted_item(RootedItem { store: self.id, value, kind, _token: token })) + } + + pub(crate) fn root_exception(&mut self, value: ValueRef) -> Result { + if !matches!(self.state.gc.get(value).map(|object| object.kind), Some(gc::GcObjectKind::Exception(_))) { + return Err(Trap::InvalidReference.into()); + } + self.root_reference(value, ReferentKind::Exception) + } + + pub(crate) fn resolve_ref(&self, reference: &T) -> Result { + let item = reference.rooted_item(); + if item.store != self.id { + return Err(Trap::InvalidStore); + } + Ok(item.value) + } + + pub(crate) fn encode_ref(&self, value: &RefValue) -> Result { + Ok(match value { + RefValue::Null => ValueRef::NULL, + RefValue::Func(value) => ValueRef::from_category_addr(value.addr(self.id).ok_or(Trap::InvalidStore)?), + RefValue::Any(value) => self.resolve_ref(value)?, + RefValue::Extern(value) => self.resolve_ref(value)?, + RefValue::Exn(value) => self.resolve_ref(value)?, + }) + } + + pub(crate) fn decode_ref(&mut self, value: ValueRef, ty: RefType) -> Result { + if value.is_null() { + return Ok(RefValue::Null); + } + if ty.type_index().is_some_and(|addr| self.state.get_type(addr).as_func().is_some()) || ty.is_func() { + return Ok(RefValue::Func(FuncRef::new(self.id, value.addr().ok_or(Trap::InvalidReference)?))); + } + if ty.is_exn() { + return self.root_exception(value).map(RefValue::Exn); + } + let kind = if value.is_i31() { + ReferentKind::I31 + } else if value.is_host_any() { + ReferentKind::HostExtern + } else { + match self.state.gc.get(value).map(|object| object.kind) { + Some(gc::GcObjectKind::Composite(type_addr)) => match &self.state.get_type(type_addr).composite { + CompositeType::Struct(_) => ReferentKind::Struct, + CompositeType::Array(_) => ReferentKind::Array, + CompositeType::Func(_) => return Err(Trap::InvalidReference.into()), + }, + _ => return Err(Trap::InvalidReference.into()), + } + }; + if ty.is_extern() { + Ok(RefValue::Extern(self.root_reference(value, kind)?)) + } else { + Ok(RefValue::Any(self.root_reference(value, kind)?)) + } + } + + fn stack_value(&self, ty: WasmType, index: &mut StackBase) -> RuntimeValue { + match ty { + WasmType::I32 | WasmType::F32 | WasmType::Ref(_) => { + let value = *self.value_stack.stack_32.get(index.s32 as usize); + index.s32 += 1; + if matches!(ty, WasmType::Ref(_)) { + RuntimeValue::ValueRef(ValueRef::from_raw(value)) + } else { + RuntimeValue::Value32(value) + } + } + WasmType::I64 | WasmType::F64 => { + let value = *self.value_stack.stack_64.get(index.s64 as usize); + index.s64 += 1; + RuntimeValue::Value64(value) + } + WasmType::V128 => { + let value = *self.value_stack.stack_128.get(index.s128 as usize); + index.s128 += 1; + RuntimeValue::Value128(value) + } + } + } + + pub(crate) fn stack_value_iter( + &mut self, + type_addr: TypeAddr, + types: FuncValueTypes, + index: StackBase, + ) -> Result, Trap> { + let canonical = self.state.get_canonical_func_type(type_addr); + let canonical = match types { + FuncValueTypes::Params => canonical.params(), + FuncValueTypes::Results => canonical.results(), + }; + let len = canonical.len(); + let reference_count = canonical.iter().filter(|ty| matches!(ty, WasmType::Ref(_))).count(); + self.state.roots.reserve(reference_count)?; + Ok(StackValueIter { store: self, type_addr, types, index, position: 0, len }) + } + + pub(crate) fn push_wasm_values(&mut self, values: impl IntoIterator) -> Result<()> { + for value in values { + let value = value.to_runtime(self)?; + self.value_stack.push_dyn(value)?; + } + Ok(()) + } + + pub(crate) fn pop_stack_values(&mut self, types: &[WasmType]) -> Result> { + let base = self.value_stack.base_before(types.iter().collect()); + let values = (|| { + let mut index = base; + let mut values = Vec::new(); + values.try_reserve_exact(types.len()).map_err(|_| Trap::OutOfMemory)?; + for &ty in types { + let value = self.stack_value(ty, &mut index); + values.push(value.into_wasm(self, ty)?); + } + Ok(values) + })(); + self.value_stack.truncate_to_base(base); + values + } + + /// Reclaims unreachable managed objects. + /// + /// Globals, tables, operand stacks, and owned host references are traced as + /// roots. Objects also become eligible for automatic threshold collection + /// after the last owned handle is dropped. + pub fn gc(&mut self) -> Result<()> { + let stack_roots = self.value_stack.stack_32.into_iter().copied().map(ValueRef::from_raw).collect::>(); + self.state.collect_gc(stack_roots).map_err(|_| Trap::OutOfMemory.into()) + } + /// Get a module instance by the internal id pub fn get_module_instance(&self, id: ModuleInstanceId) -> Option<&ModuleInstance> { self.module_instances.get(id as usize) @@ -197,10 +383,27 @@ impl Store { self.module_instances.push(instance); } - /// Returns whether a public value has the requested runtime type. - #[doc(hidden)] - pub fn value_matches_type(&self, value: WasmValue, ty: WasmType) -> bool { - self.state.value_matches_type(value, ty) + pub(crate) fn value_matches_type(&self, value: &WasmValue, ty: WasmType) -> bool { + let (WasmValue::Ref(value), WasmType::Ref(expected)) = (value, ty) else { + return value.matches_type(ty); + }; + let category_matches = match value { + RefValue::Null => return expected.is_nullable(), + RefValue::Func(_) => { + expected.is_func() + || expected.type_index().is_some_and(|addr| self.state.get_type(addr).as_func().is_some()) + } + RefValue::Extern(_) => expected.is_extern(), + RefValue::Exn(_) => expected.is_exn(), + RefValue::Any(_) => !expected.is_func() && !expected.is_extern() && !expected.is_exn(), + }; + if !category_matches { + return false; + } + match self.encode_ref(value) { + Ok(value) => self.state.value_ref_matches(value, expected), + _ => false, + } } /// Marks the store as executing and rejects nested root calls. @@ -233,10 +436,11 @@ impl Store { let mut values = values; for &ty in expected { let value = values.next().ok_or_else(|| Error::other("not enough typed function values"))?; - if !self.state.value_matches_type(value, ty) { + let internal = value.to_runtime(self)?; + if !self.value_matches_type(&value, ty) { return Err(Error::other("typed function value does not match its signature")); } - self.value_stack.extend_wasmvalues(core::iter::once(value))?; + self.value_stack.push_dyn(internal)?; } if values.next().is_some() { return Err(Error::other("too many typed function values")); @@ -254,18 +458,11 @@ impl Store { &mut self, type_addr: TypeAddr, stack_base: StackBase, - pin_refs: bool, ) -> Result { - let types = self.state.get_canonical_func_type(type_addr).results(); - let mut values = self.value_stack.wasm_values(&self.state, types, stack_base, pin_refs); - let result = R::from_wasm_values(&mut values).and_then(|result| { - if values.next().is_some() { - Err(Error::other("typed conversion did not consume all WebAssembly values")) - } else { - Ok(result) - } - }); - drop(values); + let result = { + let mut values = self.stack_value_iter(type_addr, FuncValueTypes::Results, stack_base)?; + R::from_wasm_values_exact(&mut values) + }; self.value_stack.truncate_to_base(stack_base); result } @@ -283,11 +480,9 @@ impl Store { self.state.funcs.reserve_exact(funcs.len()); for (func, &type_idx) in funcs.iter().cloned().zip(module_type_idxs) { let type_addr = type_addrs[type_idx as usize]; - self.state.funcs.push(FunctionInstance { - type_addr, - gc: self.state.func_gc_metadata(type_addr), - kind: FunctionKind::Wasm(WasmFunctionInstance { func, owner }), - }); + self.state + .funcs + .push(FunctionInstance { type_addr, kind: FunctionKind::Wasm(WasmFunctionInstance { func, owner }) }); } start..start + funcs.len() as FuncAddr } @@ -318,7 +513,7 @@ impl Store { for table in tables { let init = match &table.init { Some(expr) => match eval_const(&mut self.state, expr, global_addrs, func_addrs, type_addrs)? { - TinyWasmValue::ValueRef(value) => value, + RuntimeValue::ValueRef(value) => value, _ => return Err(Error::other("table initializer is not a reference value")), }, None => ValueRef::NULL, @@ -374,7 +569,7 @@ impl Store { ) -> Result { match item { ElementItem::Expr(expr) => match eval_const(&mut self.state, expr, globals, funcs, type_addrs)? { - TinyWasmValue::ValueRef(value) => Ok(value), + RuntimeValue::ValueRef(value) => Ok(value), other => { cold_path(); Err(Error::Other(format!("expected ref type, got {other:?}"))) @@ -426,8 +621,8 @@ impl Store { // this one is active, so we need to initialize it (essentially a `table.init` instruction) ElementKind::Active { offset, table } => { let offset = match eval_const(&mut self.state, offset, global_addrs, func_addrs, type_addrs)? { - TinyWasmValue::Value32(value) => u64::from(value), - TinyWasmValue::Value64(value) => value, + RuntimeValue::Value32(value) => u64::from(value), + RuntimeValue::Value64(value) => value, other => return Err(Error::Other(format!("expected i32 or i64, got {other:?}"))), }; let table_addr = table_addrs @@ -493,8 +688,8 @@ impl Store { }; let offset = match eval_const(&mut self.state, offset, global_addrs, func_addrs, type_addrs)? { - TinyWasmValue::Value32(value) => u64::from(value), - TinyWasmValue::Value64(value) => value, + RuntimeValue::Value32(value) => u64::from(value), + RuntimeValue::Value64(value) => value, other => return Err(Error::Other(format!("expected i32 or i64, got {other:?}"))), }; let Some(mem) = self.state.memories.get_mut(*mem_addr as usize) else { diff --git a/crates/tinywasm/src/store/state.rs b/crates/tinywasm/src/store/state.rs index 90d1fb0..60e27ee 100644 --- a/crates/tinywasm/src/store/state.rs +++ b/crates/tinywasm/src/store/state.rs @@ -2,7 +2,6 @@ use alloc::vec::Vec; use super::*; use crate::engine::Config; -use crate::interpreter::Value128; /// Global state that can be manipulated by WebAssembly programs /// @@ -17,10 +16,10 @@ pub(crate) struct State { pub(crate) memories: Vec, pub(crate) globals: Globals, pub(crate) tags: Vec, - pub(crate) exceptions: Vec, pub(crate) elements: Vec, pub(crate) data: Vec, pub(crate) gc: gc::GcHeap, + pub(crate) roots: gc::Roots, } impl State { @@ -33,16 +32,37 @@ impl State { memories: Vec::new(), globals: Globals::default(), tags: Vec::new(), - exceptions: Vec::new(), elements: Vec::new(), data: Vec::new(), gc: gc::GcHeap::new(config), + roots: gc::Roots::new(), } } - /// Returns whether values of this type can contain a managed GC object. - pub(crate) fn type_may_contain_gc(&self, ty: &WasmType) -> bool { - Self::type_may_contain_gc_in(&self.canonical_types, ty) + pub(crate) fn collect_gc(&mut self, additional: impl IntoIterator) -> Result<(), gc::AllocError> { + let canonical_types = &self.canonical_types; + let store_roots = self + .globals + .globals_32() + .filter(|(_, ty)| Self::type_may_contain_gc_in(canonical_types, &ty.ty)) + .map(|(value, _)| ValueRef::from_raw(*value)) + .chain( + self.tables + .iter() + .filter(|table| { + Self::type_may_contain_gc_in(canonical_types, &WasmType::Ref(table.kind.element_type)) + }) + .flat_map(|table| table.elements.iter().copied()), + ) + .chain( + self.elements + .iter() + .filter(|element| Self::type_may_contain_gc_in(canonical_types, &WasmType::Ref(element.ty))) + .flat_map(|element| element.items.iter().flatten().copied()), + ) + .chain(additional); + let roots = self.roots.values().chain(store_roots); + self.gc.collect(roots) } fn type_may_contain_gc_in(types: &[SubType], ty: &WasmType) -> bool { @@ -58,25 +78,28 @@ impl State { | AbstractHeapType::Struct | AbstractHeapType::Array | AbstractHeapType::Extern + | AbstractHeapType::Exn ) ) } - /// Precomputes whether a canonical function signature can carry GC objects. - pub(crate) fn func_gc_metadata(&self, type_addr: TypeAddr) -> FunctionGcMetadata { - let ty = self.get_canonical_func_type(type_addr); - FunctionGcMetadata { - params: ty.params().iter().any(|ty| self.type_may_contain_gc(ty)), - results: ty.results().iter().any(|ty| self.type_may_contain_gc(ty)), - } + pub(crate) fn check_gc_allocation(&self, type_addr: TypeAddr, value_count: usize) -> Result<(), Trap> { + let trace_references = match &self.get_type(type_addr).composite { + CompositeType::Struct(ty) => { + ty.fields.iter().any(|field| matches!(field.storage, StorageType::Value(WasmType::Ref(_)))) + } + CompositeType::Array(ty) => matches!(ty.field.storage, StorageType::Value(WasmType::Ref(_))), + CompositeType::Func(_) => unreachable!("GC object type is not a function"), + }; + self.gc.check_allocation(value_count, trace_references) } /// Allocates an object, collecting from all runtime roots when needed. pub(crate) fn alloc_gc_object( &mut self, type_addr: TypeAddr, - values: Vec, - stack_32: impl IntoIterator, + values: Vec, + additional_roots: impl IntoIterator, ) -> Result { let trace_references = match &self.get_type(type_addr).composite { CompositeType::Struct(ty) => { @@ -86,66 +109,43 @@ impl State { CompositeType::Func(_) => unreachable!("GC object type is not a function"), }; if self.gc.should_collect(values.len(), trace_references) { - let canonical_types = &self.canonical_types; - let roots = stack_32 - .into_iter() - .map(ValueRef::from_raw) - .chain( - self.globals - .globals_32() - .filter(|(_, ty)| Self::type_may_contain_gc_in(canonical_types, &ty.ty)) - .map(|(value, _)| ValueRef::from_raw(*value)), - ) - .chain( - self.tables - .iter() - .filter(|table| { - Self::type_may_contain_gc_in(canonical_types, &WasmType::Ref(table.kind.element_type)) - }) - .flat_map(|table| table.elements.iter().copied()), - ) - .chain( - self.elements - .iter() - .filter(|element| Self::type_may_contain_gc_in(canonical_types, &WasmType::Ref(element.ty))) - .flat_map(|element| element.items.iter().flatten().copied()), - ) - .chain(self.exceptions.iter().flat_map(|exception| { - exception.payload.iter().filter_map(|value| match value { - TinyWasmValue::ValueRef(value) => Some(*value), - _ => None, - }) - })) - .chain(values.iter().filter_map(|value| match value { - TinyWasmValue::ValueRef(value) => Some(*value), - _ => None, - })); - cold_err!(self.gc.collect(roots)).map_err(|_| Trap::OutOfMemory)?; + let roots = additional_roots.into_iter().chain(values.iter().filter_map(|value| match value { + RuntimeValue::ValueRef(value) => Some(*value), + _ => None, + })); + cold_err!(self.collect_gc(roots)).map_err(|_| Trap::OutOfMemory)?; } self.gc.alloc(type_addr, values, trace_references) } - /// Pins a host-visible reference when it resolves to a managed GC object. - pub(crate) fn pin_host_ref(&self, value: RefValue) { - let raw = match value { - RefValue::Any(value) => value.raw(), - RefValue::Extern(value) => value.raw(), - RefValue::Null | RefValue::Func(_) | RefValue::Exn(_) => return, - }; - self.gc.pin(ValueRef::from_raw(raw)); - } - - /// Pins GC references that have crossed into host-visible values. - pub(crate) fn pin_host_values(&self, values: &[WasmValue]) { - for &value in values { - if let WasmValue::Ref(value) = value { - self.pin_host_ref(value); - } + /// Allocates a traced exception object, collecting with its payload as temporary roots. + pub(crate) fn alloc_exception( + &mut self, + tag_addr: TagAddr, + payload: Vec, + additional_roots: impl IntoIterator, + ) -> Result { + let type_addr = self.get_tag(tag_addr).type_addr; + let mut trace_fields = Vec::new(); + trace_fields.try_reserve_exact(payload.len()).map_err(|_| Trap::OutOfMemory)?; + trace_fields.extend( + self.get_canonical_func_type(type_addr) + .params() + .iter() + .map(|ty| Self::type_may_contain_gc_in(&self.canonical_types, ty)), + ); + if self.gc.should_collect(payload.len(), true) { + let roots = additional_roots.into_iter().chain(payload.iter().filter_map(|value| match value { + RuntimeValue::ValueRef(value) => Some(*value), + _ => None, + })); + cold_err!(self.collect_gc(roots)).map_err(|_| Trap::OutOfMemory)?; } + self.gc.alloc_exception(tag_addr, payload, &trace_fields) } /// Resolves a non-null object of the expected canonical type. - pub(crate) fn gc_object(&self, reference: ValueRef, expected_type: TypeAddr) -> Result<&gc::GcObject, Trap> { + pub(crate) fn gc_object(&self, reference: ValueRef, expected_type: TypeAddr) -> Result { if reference.is_null() { return Err(if self.get_type(expected_type).as_array().is_some() { Trap::NullArrayReference @@ -154,36 +154,13 @@ impl State { }); } let object = self.gc.get(reference).ok_or(Trap::Other("invalid GC reference"))?; - if !self.type_addr_is_subtype(object.type_addr, expected_type) { + let gc::GcObjectKind::Composite(type_addr) = object.kind else { + return Err(Trap::Other("GC reference is not a struct or array")); + }; + if !self.type_addr_is_subtype(type_addr, expected_type) { return Err(Trap::Other("GC reference type mismatch")); } - Ok(object) - } - - /// Converts an internal reference using canonical heap type information. - pub(crate) fn to_ref_value(&self, value: ValueRef, ty: RefType) -> RefValue { - if value.is_null() { - return RefValue::Null; - } - - if let Some(type_addr) = ty.type_index() { - return match &self.get_type(type_addr).composite { - CompositeType::Func(_) => { - RefValue::Func(FuncRef::new(value.addr().expect("non-null reference has an address"))) - } - CompositeType::Struct(_) | CompositeType::Array(_) => RefValue::Any(AnyRef::from_raw(value.raw())), - }; - } - if ty.is_func() { - return RefValue::Func(FuncRef::new(value.addr().expect("non-null reference has an address"))); - } - if ty.is_extern() { - return RefValue::Extern(ExternRef::from_raw(value.raw())); - } - if ty.is_exn() { - return RefValue::Exn(ExnRef::new(value.addr().expect("non-null reference has an address"))); - } - RefValue::Any(AnyRef::from_raw(value.raw())) + self.gc.handle(reference).ok_or(Trap::Other("invalid GC reference")) } /// Returns whether one canonical type is a subtype of another. @@ -276,7 +253,7 @@ impl State { return self.ref_type_is_subtype(RefType::new_concrete(false, func.type_addr), expected); } if expected.abstract_heap_type() == Some(AbstractHeapType::Exn) { - return value.addr().is_some_and(|addr| self.exceptions.get(addr as usize).is_some()); + return matches!(self.gc.get(value).map(|object| object.kind), Some(gc::GcObjectKind::Exception(_))); } if value.is_i31() { return self.ref_type_is_subtype(RefType::new_abstract(false, AbstractHeapType::I31), expected); @@ -286,7 +263,8 @@ impl State { } let Some(object) = self.gc.get(value) else { return false }; - let actual = RefType::new_concrete(false, object.type_addr); + let gc::GcObjectKind::Composite(type_addr) = object.kind else { return false }; + let actual = RefType::new_concrete(false, type_addr); self.ref_type_is_subtype(actual, expected) } @@ -305,30 +283,6 @@ impl State { self.get_type(addr).as_func().expect("validated function address references a function type") } - pub(crate) fn value_matches_type(&self, value: WasmValue, expected: WasmType) -> bool { - match (value, expected) { - (WasmValue::Ref(RefValue::Null), WasmType::Ref(expected)) => expected.is_nullable(), - (WasmValue::Ref(RefValue::Func(func)), WasmType::Ref(expected)) => self - .funcs - .get(func.addr() as usize) - .is_some_and(|func| self.ref_type_is_subtype(RefType::new_concrete(false, func.type_addr), expected)), - (WasmValue::Ref(RefValue::Any(_)), WasmType::Ref(expected)) - if expected.is_func() || expected.is_extern() || expected.is_exn() => - { - false - } - (WasmValue::Ref(RefValue::Exn(value)), WasmType::Ref(expected)) => { - expected.abstract_heap_type() == Some(AbstractHeapType::Exn) - && self.exceptions.get(value.addr() as usize).is_some() - } - (WasmValue::Ref(RefValue::Any(value)), WasmType::Ref(expected)) => { - self.value_ref_matches(ValueRef::from_raw(value.raw()), expected) - } - (_, WasmType::Ref(expected)) if expected.is_concrete() => false, - _ => value.matches_type(expected), - } - } - pub(super) fn get<'a, T>(items: &'a [T], addr: Addr, kind: &str) -> &'a T { items.get(addr as usize).unwrap_or_else(|| unreachable!("invalid {kind} address: {addr}")) } @@ -406,36 +360,28 @@ impl State { } /// Converts a global directly to its public value representation. - pub(crate) fn get_global_wasmvalue(&self, addr: GlobalAddr) -> WasmValue { + pub(crate) fn get_global_internal(&self, addr: GlobalAddr) -> RuntimeValue { let ty = self.globals.ty(addr).ty; match ty { - WasmType::I32 => WasmValue::I32(self.globals.get_32(addr) as i32), - WasmType::I64 => WasmValue::I64(self.globals.get_64(addr) as i64), - WasmType::F32 => WasmValue::F32(f32::from_bits(self.globals.get_32(addr))), - WasmType::F64 => WasmValue::F64(f64::from_bits(self.globals.get_64(addr))), - WasmType::Ref(ty) => WasmValue::Ref(self.to_ref_value(ValueRef::from_raw(self.globals.get_32(addr)), ty)), - WasmType::V128 => WasmValue::V128(self.globals.get_128(addr).0), + WasmType::I32 | WasmType::F32 => RuntimeValue::Value32(self.globals.get_32(addr)), + WasmType::I64 | WasmType::F64 => RuntimeValue::Value64(self.globals.get_64(addr)), + WasmType::Ref(_) => RuntimeValue::ValueRef(ValueRef::from_raw(self.globals.get_32(addr))), + WasmType::V128 => RuntimeValue::Value128(self.globals.get_128(addr)), } } /// Validates and sets a global from its public value representation. - pub(crate) fn set_global_wasmvalue(&mut self, addr: GlobalAddr, value: WasmValue) -> Result<()> { + pub(crate) fn set_global_wasmvalue(&mut self, addr: GlobalAddr, value: RuntimeValue) -> Result<()> { let ty = self.globals.ty(addr); if !ty.mutable { cold_path(); return Err(Error::other("global is immutable")); } - if !self.value_matches_type(value, ty.ty) { - cold_path(); - return Err(Error::other("invalid global value type")); - } match value { - WasmValue::I32(value) => self.globals.set_32(addr, value as u32), - WasmValue::I64(value) => self.globals.set_64(addr, value as u64), - WasmValue::F32(value) => self.globals.set_32(addr, value.to_bits()), - WasmValue::F64(value) => self.globals.set_64(addr, value.to_bits()), - WasmValue::Ref(value) => self.globals.set_32(addr, ValueRef::from(value).raw()), - WasmValue::V128(value) => self.globals.set_128(addr, Value128(value)), + RuntimeValue::Value32(value) => self.globals.set_32(addr, value), + RuntimeValue::Value64(value) => self.globals.set_64(addr, value), + RuntimeValue::ValueRef(value) => self.globals.set_32(addr, value.raw()), + RuntimeValue::Value128(value) => self.globals.set_128(addr, value), } Ok(()) } diff --git a/crates/tinywasm/src/store/table.rs b/crates/tinywasm/src/store/table.rs index da52660..cb8ded0 100644 --- a/crates/tinywasm/src/store/table.rs +++ b/crates/tinywasm/src/store/table.rs @@ -17,6 +17,9 @@ pub(crate) struct TableInstance { impl TableInstance { /// Creates a table filled with the given initial reference. pub(crate) fn new(kind: TableType, init: ValueRef, limiter: Option<&dyn ResourceLimiter>) -> Result { + if kind.size_max.is_some_and(|maximum| maximum < kind.size_initial) { + return Err(Trap::OutOfMemory.into()); + } let size = cold_err!(usize::try_from(kind.size_initial)).map_err(|_| Trap::OutOfMemory)?; if size > MAX_TABLE_SIZE { return Err(Trap::OutOfMemory.into()); diff --git a/crates/tinywasm/tests/gc_refs.rs b/crates/tinywasm/tests/gc_refs.rs index d3f2994..f5159a6 100644 --- a/crates/tinywasm/tests/gc_refs.rs +++ b/crates/tinywasm/tests/gc_refs.rs @@ -1,4 +1,4 @@ -use tinywasm::types::{ExternRef, RefValue, WasmValue}; +use tinywasm::types::{RefValue, WasmValue}; use tinywasm::{Engine, ExecProgress, ModuleInstance, Store, engine::Config}; const MODULE: &str = r#" @@ -28,7 +28,7 @@ fn store() -> Store { } #[test] -fn host_result_is_pinned_and_rejected_by_another_store() { +fn host_result_is_rooted_and_rejected_by_another_store() { let module = tinywasm::parse_bytes(&wat::parse_str(MODULE).unwrap()).unwrap(); let mut first_store = store(); let first = ModuleInstance::instantiate(&mut first_store, &module, None).unwrap(); @@ -39,7 +39,7 @@ fn host_result_is_pinned_and_rejected_by_another_store() { let value = new.call(&mut first_store, &[]).unwrap().pop().unwrap(); assert!(matches!(value, WasmValue::Ref(RefValue::Any(_)))); churn.call(&mut first_store, &[]).unwrap(); - assert_eq!(read.call(&mut first_store, &[value]).unwrap(), [WasmValue::I32(42)]); + assert_eq!(read.call(&mut first_store, std::slice::from_ref(&value)).unwrap(), [WasmValue::I32(42)]); let mut second_store = store(); let second = ModuleInstance::instantiate(&mut second_store, &module, None).unwrap(); @@ -48,7 +48,7 @@ fn host_result_is_pinned_and_rejected_by_another_store() { } #[test] -fn externalized_gc_result_is_pinned() { +fn externalized_gc_result_is_rooted() { let module = tinywasm::parse_bytes(&wat::parse_str(MODULE).unwrap()).unwrap(); let mut store = store(); let instance = ModuleInstance::instantiate(&mut store, &module, None).unwrap(); @@ -71,12 +71,12 @@ fn host_externref_does_not_alias_a_gc_object() { let read = instance.func_untyped(&store, "read-extern").unwrap(); new.call(&mut store, &[]).unwrap(); - let host_ref = WasmValue::Ref(RefValue::Extern(ExternRef::new(0))); + let host_ref = tinywasm::ExternRef::try_new(&mut store, 0).unwrap().into(); assert!(read.call(&mut store, &[host_ref]).is_err()); } #[test] -fn resumable_gc_result_is_pinned() { +fn resumable_gc_result_is_rooted() { let module = tinywasm::parse_bytes(&wat::parse_str(MODULE).unwrap()).unwrap(); let mut store = store(); let instance = ModuleInstance::instantiate(&mut store, &module, None).unwrap(); diff --git a/crates/tinywasm/tests/host_func_signature_check.rs b/crates/tinywasm/tests/host_func_signature_check.rs index 38a85ef..b1c2d2b 100644 --- a/crates/tinywasm/tests/host_func_signature_check.rs +++ b/crates/tinywasm/tests/host_func_signature_check.rs @@ -1,5 +1,5 @@ use std::fmt::Write; -use tinywasm::types::{ExternRef, FuncType, RefType, RefValue, WasmType, WasmValue}; +use tinywasm::types::{FuncType, RefType, RefValue, WasmType, WasmValue}; use tinywasm::{FuncContext, HostFunction, Imports, Module, ModuleInstance, Store}; const VAL_LISTS: &[&[WasmValue]] = &[ @@ -8,7 +8,6 @@ const VAL_LISTS: &[&[WasmValue]] = &[ &[WasmValue::I32(0), WasmValue::I32(0)], &[WasmValue::I32(0), WasmValue::I32(0), WasmValue::F64(0.0)], &[WasmValue::I32(0), WasmValue::F64(0.0), WasmValue::I32(0)], - &[WasmValue::Ref(RefValue::Extern(ExternRef::new(0))), WasmValue::F64(0.0), WasmValue::I32(0)], ]; fn module_cases() -> Vec<(Module, FuncType, Vec)> { @@ -105,7 +104,7 @@ fn test_linking_invalid_typed_func() -> Result<(), Box> } #[test] -fn concrete_host_references_use_canonical_types() -> Result<(), Box> { +fn standalone_host_functions_reject_concrete_types() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module @@ -125,19 +124,11 @@ fn concrete_host_references_use_canonical_types() -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box ModuleInstance { + let wasm = wat::parse_str(MODULE).unwrap(); + let module = tinywasm::parse_bytes(&wasm).unwrap(); + ModuleInstance::instantiate(store, &module, None).unwrap() +} + +fn exception(error: Error) -> ExnRef { + let Error::Exception(exception) = error else { panic!("expected exception") }; + exception +} + +#[test] +fn exception_accessors_validate_store_and_expose_payload() { + let mut store = Store::default(); + let instance = instantiate(&mut store); + let throw = instance.func_untyped(&store, "throw-scalar").unwrap(); + let exception = exception(throw.call(&mut store, &[WasmValue::I32(17)]).unwrap_err()); + + assert_eq!(exception.tag(&store).unwrap(), instance.tag("scalar-tag").unwrap()); + assert_eq!(exception.field(&mut store, 0).unwrap(), WasmValue::I32(17)); + assert_eq!(exception.fields(&mut store).unwrap(), [WasmValue::I32(17)]); + assert!(exception.field(&mut store, 1).is_err()); + + let other = Store::default(); + assert_eq!(exception.tag(&other).unwrap_err(), Error::Trap(Trap::InvalidStore)); +} + +#[test] +fn exception_roots_keep_payload_graphs_live() { + let mut store = Store::new(Engine::new(Config::new().with_gc_collection_threshold(1))); + let instance = instantiate(&mut store); + let throw = instance.func_untyped(&store, "throw-graph").unwrap(); + let read = instance.func_untyped(&store, "read-node").unwrap(); + let exception = exception(throw.call(&mut store, &[]).unwrap_err()); + + store.gc().unwrap(); + let payload = exception.field(&mut store, 0).unwrap(); + assert!(matches!(payload, WasmValue::Ref(RefValue::Any(_)))); + assert_eq!(read.call(&mut store, &[payload]).unwrap(), [WasmValue::I32(42)]); +} + +#[test] +fn owned_exception_references_survive_collection_and_reject_another_store() { + let mut store = Store::default(); + let instance = instantiate(&mut store); + let throw = instance.func_untyped(&store, "throw-scalar").unwrap(); + let first = exception(throw.call(&mut store, &[WasmValue::I32(1)]).unwrap_err()); + + store.gc().unwrap(); + let current = exception(throw.call(&mut store, &[WasmValue::I32(2)]).unwrap_err()); + assert_eq!(current.field(&mut store, 0).unwrap(), WasmValue::I32(2)); + assert_eq!(first.field(&mut store, 0).unwrap(), WasmValue::I32(1)); + let rethrow = instance.func_untyped(&store, "rethrow").unwrap(); + let rethrown = exception(rethrow.call(&mut store, &[WasmValue::Ref(RefValue::Exn(first))]).unwrap_err()); + assert_eq!(rethrown.field(&mut store, 0).unwrap(), WasmValue::I32(1)); + + let mut other = Store::default(); + let other_instance = instantiate(&mut other); + let other_rethrow = other_instance.func_untyped(&other, "rethrow").unwrap(); + assert_eq!( + other_rethrow.call(&mut other, &[WasmValue::Ref(RefValue::Exn(current))]).unwrap_err(), + Error::Trap(Trap::InvalidStore) + ); +} + +#[test] +fn host_tags_reject_unbranded_concrete_types() { + let mut store = Store::default(); + let ty = FuncType::new(&[WasmType::Ref(RefType::new_concrete(true, 0))], &[]); + + assert!(Tag::try_new(&mut store, ty).is_err()); +} + +struct GcUsage(Arc>>); + +impl ResourceLimiter for GcUsage { + fn gc_growing(&self, current: usize, desired: usize, _maximum: Option) -> Result { + self.0.lock().unwrap().push((current, desired)); + Ok(true) + } +} + +#[test] +fn dropped_exception_roots_release_counted_gc_bytes() { + let usage = Arc::new(Mutex::new(Vec::new())); + let config = + Config::new().with_gc_collection_threshold(usize::MAX).with_resource_limiter(Arc::new(GcUsage(usage.clone()))); + let mut store = Store::new(Engine::new(config)); + let instance = instantiate(&mut store); + let catch = instance.func_untyped(&store, "catch-scalar").unwrap(); + + for value in 0..32 { + catch.call(&mut store, &[WasmValue::I32(value)]).unwrap(); + } + store.gc().unwrap(); + usage.lock().unwrap().clear(); + + catch.call(&mut store, &[WasmValue::I32(33)]).unwrap(); + assert_eq!(usage.lock().unwrap()[0].0, 0); +} diff --git a/crates/tinywasm/tests/memory.rs b/crates/tinywasm/tests/memory.rs index 213a15f..819fe85 100644 --- a/crates/tinywasm/tests/memory.rs +++ b/crates/tinywasm/tests/memory.rs @@ -16,7 +16,7 @@ fn store_with_limiter(limiter: Arc) -> Store { #[test] fn memory_read_write_roundtrip() -> TestResult { let mut store = Store::default(); - let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?; + let memory = Memory::try_new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?; memory.copy_from_slice(&mut store, 0, &[1, 2, 3, 4, 5])?; assert_eq!(memory.read_vec(&store, 0, 5)?, &[1, 2, 3, 4, 5]); @@ -28,7 +28,7 @@ fn memory_read_write_roundtrip() -> TestResult { #[test] fn read_returns_short_count_at_end_of_memory() -> TestResult { let mut store = Store::default(); - let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, Some(1), Some(4)))?; + let memory = Memory::try_new(&mut store, MemoryType::new(MemoryArch::I32, 1, Some(1), Some(4)))?; memory.copy_from_slice(&mut store, 0, &[1, 2, 3, 4])?; let mut dst = [9; 8]; @@ -84,7 +84,7 @@ impl ResourceLimiter for TrapGrowth { #[test] fn resource_limiter_can_reject_growth() -> TestResult { let mut store = store_with_limiter(Arc::new(DenyGrowth)); - let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?; + let memory = Memory::try_new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None))?; assert_eq!(memory.grow(&mut store, 1)?, None); assert_eq!(memory.page_count(&store)?, 1); @@ -134,7 +134,7 @@ fn resource_limiter_can_trap_guest_memory_grow() -> TestResult { #[test] fn resource_limiter_rejects_host_memory_initial_size() { let mut store = store_with_limiter(Arc::new(DenyAll)); - let result = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None)); + let result = Memory::try_new(&mut store, MemoryType::new(MemoryArch::I32, 1, None, None)); assert!(matches!(result, Err(tinywasm::Error::Trap(Trap::OutOfMemory)))); } @@ -153,7 +153,15 @@ fn resource_limiter_rejects_module_memory_initial_size() -> TestResult { #[test] fn resource_limiter_rejects_table_initial_size() { let mut store = store_with_limiter(Arc::new(DenyAll)); - let result = Table::new(&mut store, TableType::new(RefType::FUNCREF, 1, None), RefValue::Null.into()); + let result = Table::try_new(&mut store, TableType::new(RefType::FUNCREF, 1, None), RefValue::Null.into()); + + assert!(matches!(result, Err(tinywasm::Error::Trap(Trap::OutOfMemory)))); +} + +#[test] +fn table_rejects_initial_size_above_maximum() { + let mut store = Store::default(); + let result = Table::try_new(&mut store, TableType::new(RefType::FUNCREF, 2, Some(1)), RefValue::Null.into()); assert!(matches!(result, Err(tinywasm::Error::Trap(Trap::OutOfMemory)))); } @@ -161,7 +169,7 @@ fn resource_limiter_rejects_table_initial_size() { #[test] fn resource_limiter_can_reject_table_growth() -> TestResult { let mut store = store_with_limiter(Arc::new(DenyGrowth)); - let table = Table::new(&mut store, TableType::new(RefType::FUNCREF, 1, None), RefValue::Null.into())?; + let table = Table::try_new(&mut store, TableType::new(RefType::FUNCREF, 1, None), RefValue::Null.into())?; assert_eq!(table.grow(&mut store, 1, RefValue::Null.into())?, None); assert_eq!(table.size(&store)?, 1); @@ -171,7 +179,7 @@ fn resource_limiter_can_reject_table_growth() -> TestResult { #[test] fn resource_limiter_can_trap_table_growth() -> TestResult { let mut store = store_with_limiter(Arc::new(TrapGrowth)); - let table = Table::new(&mut store, TableType::new(RefType::FUNCREF, 1, None), RefValue::Null.into())?; + let table = Table::try_new(&mut store, TableType::new(RefType::FUNCREF, 1, None), RefValue::Null.into())?; assert!(matches!(table.grow(&mut store, 1, RefValue::Null.into()), Err(tinywasm::Error::Trap(Trap::Unreachable)))); Ok(()) diff --git a/crates/tinywasm/tests/reference_roots.rs b/crates/tinywasm/tests/reference_roots.rs new file mode 100644 index 0000000..3a4dd92 --- /dev/null +++ b/crates/tinywasm/tests/reference_roots.rs @@ -0,0 +1,96 @@ +use std::sync::{Arc, Mutex}; +use tinywasm::types::{ExternRef, FuncType, RefType, RefValue, WasmType, WasmValue}; +use tinywasm::{HostFunction, Imports, ModuleInstance, Store, Trap}; + +#[test] +fn direct_references_stay_live_until_the_last_clone_is_dropped() { + let mut store = Store::default(); + let reference = ExternRef::try_new(&mut store, 7).unwrap(); + let clone = reference.clone(); + drop(reference); + store.gc().unwrap(); + + assert_eq!(clone.key(&store), Ok(7)); +} + +#[test] +fn roots_and_function_references_reject_another_store() { + let mut first = Store::default(); + let mut second = Store::default(); + let root = ExternRef::try_new(&mut first, 1).unwrap(); + let function = HostFunction::from(|_, ()| -> tinywasm::Result<()> { Ok(()) }).instantiate(&mut first).unwrap(); + let func_ref = function.as_func_ref(&first).unwrap(); + let accepts_func_ref = + HostFunction::from(|_, _: Option| -> tinywasm::Result<()> { Ok(()) }) + .instantiate(&mut second) + .unwrap(); + + assert!(matches!(root.key(&second), Err(tinywasm::Error::Trap(Trap::InvalidStore)))); + assert!(matches!( + accepts_func_ref.call(&mut second, &[WasmValue::Ref(RefValue::Func(func_ref))]), + Err(tinywasm::Error::Trap(Trap::InvalidStore)) + )); +} + +#[test] +fn callback_results_and_captured_clones_remain_valid() { + let mut store = Store::default(); + let captured = Arc::new(Mutex::new(None)); + let callback_root = captured.clone(); + let ty = FuncType::new(&[], &[WasmType::Ref(RefType::EXTERNREF)]); + let function = HostFunction::from_untyped(&ty, move |mut context, _| { + let root = ExternRef::try_new(context.store_mut(), 17)?; + *callback_root.lock().unwrap() = Some(root.clone()); + Ok(vec![root.into()]) + }) + .instantiate(&mut store) + .unwrap(); + + let result = function.call(&mut store, &[]).unwrap().pop().unwrap(); + let WasmValue::Ref(RefValue::Extern(result)) = result else { panic!("expected externref") }; + assert_eq!(result.key(&store), Ok(17)); + assert_eq!(captured.lock().unwrap().as_ref().unwrap().key(&store), Ok(17)); +} + +#[test] +fn guest_callback_arguments_are_rooted_before_entering_untyped_host_code() { + let wasm = wat::parse_str( + r#" + (module + (import "host" "make" (func $make (result (ref extern)))) + (import "host" "check" (func $check (param externref))) + (func (export "call") + call $make + call $check)) + "#, + ) + .unwrap(); + let module = tinywasm::parse_bytes(&wasm).unwrap(); + let mut store = Store::default(); + let mut imports = Imports::new(); + imports.define("host", "make", HostFunction::from(|mut context, ()| ExternRef::try_new(context.store_mut(), 23))); + imports.define( + "host", + "check", + HostFunction::from_untyped(&FuncType::new(&[WasmType::Ref(RefType::EXTERNREF)], &[]), |context, args| { + let WasmValue::Ref(RefValue::Extern(value)) = &args[0] else { panic!("expected externref") }; + assert_eq!(value.key(context.store()), Ok(23)); + Ok(Vec::new()) + }), + ); + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports)).unwrap(); + + instance.func::<(), ()>(&store, "call").unwrap().call(&mut store, ()).unwrap(); +} + +#[test] +fn typed_option_reference_signatures_are_nullable() { + let mut store = Store::default(); + let function = + HostFunction::from(|_context, value: Option| -> tinywasm::Result> { Ok(value) }) + .instantiate(&mut store) + .unwrap(); + + assert_eq!(function.ty(&store).unwrap().params(), &[WasmType::Ref(RefType::EXTERNREF)]); + assert_eq!(function.call(&mut store, &[WasmValue::Ref(RefValue::Null)]).unwrap(), [WasmValue::Ref(RefValue::Null)]); +} diff --git a/crates/tinywasm/tests/store_ownership.rs b/crates/tinywasm/tests/store_ownership.rs index a508817..b14f5ef 100644 --- a/crates/tinywasm/tests/store_ownership.rs +++ b/crates/tinywasm/tests/store_ownership.rs @@ -58,8 +58,8 @@ fn global_access_rejects_wrong_store() -> Result<(), Box let instance = ModuleInstance::instantiate(&mut owner_store, &module, None)?; let global = instance.global("g")?; - let other_store = Store::default(); - let err = global.get(&other_store).unwrap_err(); + let mut other_store = Store::default(); + let err = global.get(&mut other_store).unwrap_err(); assert_eq!(err, tinywasm::Error::Trap(tinywasm::Trap::InvalidStore)); Ok(()) @@ -92,7 +92,7 @@ fn global_import_rejects_wrong_store() -> Result<(), Box let wasm = wat::parse_str(r#"(module (import "env" "g" (global i32)))"#)?; let module = tinywasm::parse_bytes(&wasm)?; let mut owner_store = Store::default(); - let global = Global::new(&mut owner_store, GlobalType::new(WasmType::I32, false), 1.into())?; + let global = Global::try_new(&mut owner_store, GlobalType::new(WasmType::I32, false), 1.into())?; let mut imports = Imports::default(); imports.define("env", "g", global); diff --git a/crates/tinywasm/tests/typed_gc_access.rs b/crates/tinywasm/tests/typed_gc_access.rs new file mode 100644 index 0000000..26b2362 --- /dev/null +++ b/crates/tinywasm/tests/typed_gc_access.rs @@ -0,0 +1,49 @@ +use tinywasm::types::{ArrayRef, RefValue, StructRef, WasmValue}; +use tinywasm::{GcStorageType, ModuleInstance, Store}; + +#[test] +fn typed_struct_and_array_access() -> tinywasm::Result<()> { + let wasm = wat::parse_str( + r#"(module + (type $s (struct (field (mut i32)) (field i8))) + (type $refs (struct (field (ref $s)) (field (ref null $s)))) + (type $a (array (mut i16))) + (func $make-struct (result (ref $s)) + i32.const 7 + i32.const 258 + struct.new $s) + (func (export "struct") (result (ref struct)) + call $make-struct) + (func (export "refs") (result (ref struct)) + call $make-struct + ref.null $s + struct.new $refs) + (func (export "array") (result (ref array)) + i32.const 65537 + i32.const 2 + array.new $a))"#, + ) + .expect("valid WAT"); + let mut store = Store::default(); + let instance = ModuleInstance::instantiate(&mut store, &tinywasm::parse_bytes(&wasm)?, None)?; + + let structure: StructRef = instance.func::<(), StructRef>(&store, "struct")?.call(&mut store, ())?; + assert_eq!(structure.fields(&mut store)?, [WasmValue::I32(7), WasmValue::I32(2)]); + structure.set_field(&mut store, 0, WasmValue::I32(9))?; + assert_eq!(structure.field(&mut store, 0)?, WasmValue::I32(9)); + assert_eq!(structure.ty(&store)?.field(&store, 1)?.storage(), GcStorageType::I8); + + let references: StructRef = instance.func::<(), StructRef>(&store, "refs")?.call(&mut store, ())?; + let fields = references.fields(&mut store)?; + let WasmValue::Ref(RefValue::Any(nested)) = &fields[0] else { panic!("expected struct reference") }; + assert_eq!(fields[1], WasmValue::Ref(RefValue::Null)); + store.gc()?; + assert_eq!(nested.as_struct().unwrap().field(&mut store, 0)?, WasmValue::I32(7)); + + let array: ArrayRef = instance.func::<(), ArrayRef>(&store, "array")?.call(&mut store, ())?; + assert_eq!(array.len(&store)?, 2); + assert_eq!(array.get(&mut store, 0)?, WasmValue::I32(1)); + array.set(&mut store, 1, WasmValue::I32(3))?; + assert_eq!(array.get(&mut store, 1)?, WasmValue::I32(3)); + Ok(()) +} diff --git a/crates/tinywasm/tests/typed_globals.rs b/crates/tinywasm/tests/typed_globals.rs index e57309f..6e83045 100644 --- a/crates/tinywasm/tests/typed_globals.rs +++ b/crates/tinywasm/tests/typed_globals.rs @@ -60,7 +60,7 @@ fn globals_use_typed_instructions_and_roundtrip_values() -> Result<(), Box(&store, "add-i32")?.call(&mut store, 2)?, 13); assert_eq!(instance.func::(&store, "add-i64")?.call(&mut store, 2)?, 15); @@ -81,12 +81,12 @@ fn imported_global_keeps_its_typed_store_address() -> Result<(), Box(&store, "roundtrip")?.call(&mut store, 42)?, 42); - assert_eq!(global.get(&store)?, WasmValue::I64(42)); + assert_eq!(global.get(&mut store)?, WasmValue::I64(42)); Ok(()) } diff --git a/crates/types/src/archive.rs b/crates/types/src/archive.rs index db40263..b568155 100644 --- a/crates/types/src/archive.rs +++ b/crates/types/src/archive.rs @@ -7,7 +7,7 @@ use crate::Module; #[rustfmt::skip] const TWASM_MAGIC: [u8; 16] = [ TWASM_MAGIC_PREFIX[0], TWASM_MAGIC_PREFIX[1], TWASM_MAGIC_PREFIX[2], TWASM_MAGIC_PREFIX[3], TWASM_VERSION[0], TWASM_VERSION[1], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; const TWASM_MAGIC_PREFIX: &[u8; 4] = b"TWAS"; -const TWASM_VERSION: &[u8; 2] = b"05"; +const TWASM_VERSION: &[u8; 2] = b"06"; fn validate_magic(wasm: &[u8]) -> Result { if wasm.len() < TWASM_MAGIC.len() || &wasm[..TWASM_MAGIC_PREFIX.len()] != TWASM_MAGIC_PREFIX { @@ -64,7 +64,10 @@ impl Module { #[cfg(test)] mod tests { use super::*; - use crate::{Instruction, ModuleInner, V128Operand, WasmFunction}; + use crate::{ + AbstractHeapType, ConstInstruction, Global, GlobalType, Instruction, ModuleFuncIdx, ModuleInner, RefType, + V128Operand, WasmFunction, WasmType, + }; use crate::{OperandIdx, OperandType}; use alloc::{boxed::Box, sync::Arc, vec}; @@ -94,7 +97,7 @@ mod tests { let module = Module::from(ModuleInner { funcs: Box::new([Arc::new(function)]), ..ModuleInner::default() }); let archive = module.serialize_twasm().expect("serialize archive"); - assert_eq!(&archive[..6], b"TWAS05"); + assert_eq!(&archive[..6], b"TWAS06"); let decoded = Module::try_from_twasm(&archive).expect("deserialize archive"); let function = &decoded.funcs[0]; @@ -106,4 +109,37 @@ mod tests { assert_eq!(index.get(&function.data).value, bytes); } } + + #[test] + fn const_reference_expressions_round_trip_archive() { + let null_type = RefType::new_abstract(true, AbstractHeapType::Extern); + let module = Module::from(ModuleInner { + globals: vec![ + Global { + ty: GlobalType::new(WasmType::Ref(null_type), false), + init: vec![ConstInstruction::RefNull(null_type)].into_boxed_slice(), + }, + Global { + ty: GlobalType::new(WasmType::Ref(RefType::FUNCREF), false), + init: vec![ConstInstruction::RefFunc(ModuleFuncIdx::new(7))].into_boxed_slice(), + }, + Global { + ty: GlobalType::new(WasmType::Ref(RefType::new_abstract(false, AbstractHeapType::I31)), false), + init: vec![ConstInstruction::I32Const(42), ConstInstruction::RefI31].into_boxed_slice(), + }, + ] + .into_boxed_slice(), + ..ModuleInner::default() + }); + + let archive = module.serialize_twasm().expect("serialize archive"); + let decoded = Module::try_from_twasm(&archive).expect("deserialize archive"); + + assert!(matches!(decoded.globals[0].init.as_ref(), [ConstInstruction::RefNull(ty)] if *ty == null_type)); + assert!(matches!( + decoded.globals[1].init.as_ref(), + [ConstInstruction::RefFunc(index)] if index.index() == 7 + )); + assert!(matches!(decoded.globals[2].init.as_ref(), [ConstInstruction::I32Const(42), ConstInstruction::RefI31])); + } } diff --git a/crates/types/src/instructions.rs b/crates/types/src/instructions.rs index c9786d3..e3d35d0 100644 --- a/crates/types/src/instructions.rs +++ b/crates/types/src/instructions.rs @@ -2,7 +2,7 @@ use alloc::boxed::Box; use super::{FuncAddr, GlobalAddr, LocalAddr, TableAddr, TagAddr, TypeAddr, ValueCounts}; use crate::operands::sealed; -use crate::{DataAddr, ElemAddr, MemAddr, Operand64, Operand128, OperandIdx, OperandType, RefType, RefValue}; +use crate::{DataAddr, ElemAddr, MemAddr, ModuleFuncIdx, Operand64, Operand128, OperandIdx, OperandType, RefType}; /// Represents a memory immediate in a WebAssembly memory instruction. #[derive(Copy, Clone, PartialEq, Eq)] @@ -429,7 +429,8 @@ pub enum ConstInstruction { GlobalGet64(GlobalAddr), GlobalGet128(GlobalAddr), GlobalGetRef(GlobalAddr), - Ref(RefValue), + RefNull(RefType), + RefFunc(ModuleFuncIdx), RefI31, AnyConvertExtern, ExternConvertAny, diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index d1f7453..71eac04 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -326,13 +326,35 @@ pub type TableAddr = Addr; pub type MemAddr = Addr; pub type GlobalAddr = Addr; pub type TagAddr = Addr; -pub type ExnAddr = Addr; pub type ElemAddr = Addr; pub type DataAddr = Addr; pub type ExternAddr = Addr; pub type ConstIdx = Addr; // additional internal addresses +/// A function index in a WebAssembly module's function index space. +/// +/// This remains module-local until instantiation resolves it to a [`FuncAddr`]. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "debug", derive(Debug))] +#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "archive", serde(transparent))] +pub struct ModuleFuncIdx(u32); + +impl ModuleFuncIdx { + /// Creates a module-local function index. + #[inline] + pub const fn new(index: u32) -> Self { + Self(index) + } + + /// Returns the module-local function index. + #[inline] + pub const fn index(self) -> u32 { + self.0 + } +} + /// An address in the current type space. /// /// Parsed modules use module-local addresses; instantiated types use their store's canonical addresses. diff --git a/crates/types/src/reference.rs b/crates/types/src/reference.rs index ed59ae5..a38f4c0 100644 --- a/crates/types/src/reference.rs +++ b/crates/types/src/reference.rs @@ -1,12 +1,3 @@ -const HOST_REF_TAG: u32 = 1 << 30; - -const fn encode_host_ref(addr: u32) -> Option { - if addr >= HOST_REF_TAG - 1 { - return None; - } - Some((addr | HOST_REF_TAG).wrapping_add(1).wrapping_mul(2)) -} - /// An abstract WebAssembly heap type. /// /// This contains exactly the abstract heap types in core Wasm 3.0. @@ -148,156 +139,3 @@ impl RefType { matches!(self.abstract_heap_type(), Some(AbstractHeapType::Exn | AbstractHeapType::NoExn)) } } - -/// A host-facing WebAssembly reference value. -#[derive(Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "debug", derive(Debug))] -#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] -pub enum RefValue { - Null, - Func(FuncRef), - Extern(ExternRef), - Any(AnyRef), - Exn(ExnRef), -} - -/// A reference to a function in a store. -/// -/// The payload is the function's store-local address. -#[derive(Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "debug", derive(Debug))] -#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] -pub struct FuncRef(u32); - -impl FuncRef { - #[inline] - pub const fn new(addr: u32) -> Self { - Self(addr) - } - - #[inline] - pub const fn addr(self) -> u32 { - self.0 - } -} - -/// An opaque external reference. -/// -/// Packed as `[payload:31 i31:1]`. Host addresses use the upper payload -/// category, Store-managed objects use the lower category, and odd values -/// contain an externalized i31. -#[derive(Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "debug", derive(Debug))] -#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] -pub struct ExternRef(u32); - -impl ExternRef { - #[inline] - pub const fn new(addr: u32) -> Self { - let Some(value) = Self::try_new(addr) else { panic!("external reference address is too large") }; - value - } - - /// Creates an external reference when `addr` fits the runtime encoding. - #[inline] - pub const fn try_new(addr: u32) -> Option { - match encode_host_ref(addr) { - Some(encoded) => Some(Self(encoded)), - None => None, - } - } - - #[doc(hidden)] - #[inline] - pub const fn from_raw(raw: u32) -> Self { - Self(raw) - } - - #[doc(hidden)] - #[inline] - pub const fn raw(self) -> u32 { - self.0 - } -} - -/// A reference to an exception in a store. -/// -/// The payload is the exception's store-local address. -#[derive(Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "debug", derive(Debug))] -#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] -pub struct ExnRef(u32); - -impl ExnRef { - #[inline] - pub const fn new(addr: u32) -> Self { - Self(addr) - } - - #[inline] - pub const fn addr(self) -> u32 { - self.0 - } -} - -/// A WebAssembly `anyref` value. -/// -/// Packed as: -/// -/// ```text -/// [payload:31 i31:1] -/// ``` -/// -/// Odd values contain an inline signed i31. Non-zero even values are reserved -/// for store-managed references, and zero is reserved for null by the runtime. -#[derive(Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "debug", derive(Debug))] -#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] -pub struct AnyRef(u32); - -impl AnyRef { - /// Creates a host reference when `addr` fits the runtime encoding. - #[inline] - pub const fn from_host(addr: u32) -> Option { - match encode_host_ref(addr) { - Some(encoded) => Some(Self(encoded)), - None => None, - } - } - - #[doc(hidden)] - #[inline] - pub const fn from_raw(raw: u32) -> Self { - Self(raw) - } - - pub const fn from_i31(value: i32) -> Option { - if value < -(1 << 30) || value >= (1 << 30) { - return None; - } - - Some(Self(((value as u32) << 1) | 1)) - } - - pub const fn as_i31(self) -> Option { - if self.0 & 1 == 1 { Some((self.0 as i32) >> 1) } else { None } - } - - #[inline] - pub const fn raw(self) -> u32 { - self.0 - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn host_reference_encoding_is_checked_and_unique() { - assert_ne!(AnyRef::from_host(0), AnyRef::from_host(1)); - assert!(AnyRef::from_host(HOST_REF_TAG - 2).is_some()); - assert!(AnyRef::from_host(HOST_REF_TAG - 1).is_none()); - assert!(ExternRef::try_new(u32::MAX).is_none()); - } -} diff --git a/crates/types/src/value.rs b/crates/types/src/value.rs index 5049ba2..45fc2bc 100644 --- a/crates/types/src/value.rs +++ b/crates/types/src/value.rs @@ -1,307 +1,36 @@ -use core::fmt::Debug; - -use crate::{ConstInstruction, RefType, RefValue}; - -/// A WebAssembly value. -/// -/// See -#[derive(Clone, Copy, PartialEq)] -pub enum WasmValue { - // Num types - /// A 32-bit integer - I32(i32), - /// A 64-bit integer - I64(i64), - /// A 32-bit float - F32(f32), - /// A 64-bit float - F64(f64), - /// A 128-bit vector - V128([u8; 16]), - /// A reference type - Ref(RefValue), -} - -impl WasmValue { - /// Return this value's broad WebAssembly type. - /// - /// A null reference has no intrinsic heap type and returns `None`. - pub const fn ty(self) -> Option { - match self { - Self::I32(_) => Some(WasmType::I32), - Self::I64(_) => Some(WasmType::I64), - Self::F32(_) => Some(WasmType::F32), - Self::F64(_) => Some(WasmType::F64), - Self::V128(_) => Some(WasmType::V128), - Self::Ref(RefValue::Null) => None, - Self::Ref(RefValue::Func(_)) => Some(WasmType::Ref(RefType::FUNCREF)), - Self::Ref(RefValue::Extern(_)) => Some(WasmType::Ref(RefType::EXTERNREF)), - Self::Ref(RefValue::Exn(_)) => Some(WasmType::Ref(RefType::EXNREF)), - Self::Ref(RefValue::Any(_)) => { - Some(WasmType::Ref(RefType::new_abstract(true, crate::AbstractHeapType::Any))) - } - } - } - - #[inline] - pub const fn matches_type(self, ty: WasmType) -> bool { - // Concrete references require a store lookup; this method only classifies their broad heap type. - match (self, ty) { - (Self::I32(_), WasmType::I32) - | (Self::I64(_), WasmType::I64) - | (Self::F32(_), WasmType::F32) - | (Self::F64(_), WasmType::F64) - | (Self::V128(_), WasmType::V128) => true, - (Self::Ref(RefValue::Null), WasmType::Ref(ty)) => ty.is_nullable(), - (Self::Ref(RefValue::Func(_)), WasmType::Ref(ty)) => { - matches!(ty.abstract_heap_type(), Some(crate::AbstractHeapType::Func)) - } - (Self::Ref(RefValue::Extern(_)), WasmType::Ref(ty)) => { - matches!(ty.abstract_heap_type(), Some(crate::AbstractHeapType::Extern)) - } - (Self::Ref(RefValue::Exn(_)), WasmType::Ref(ty)) => { - matches!(ty.abstract_heap_type(), Some(crate::AbstractHeapType::Exn)) - } - (Self::Ref(RefValue::Any(value)), WasmType::Ref(ty)) => matches!( - (value.as_i31(), ty.abstract_heap_type()), - ( - Some(_), - Some(crate::AbstractHeapType::I31 | crate::AbstractHeapType::Eq | crate::AbstractHeapType::Any) - ) | (None, Some(crate::AbstractHeapType::Any)) - ), - _ => false, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{AbstractHeapType, AnyRef, FuncRef, RefType}; - - #[test] - fn concrete_references_require_store_type_information() { - let concrete = WasmType::Ref(RefType::new_concrete(false, 0)); - - assert!(!WasmValue::from(FuncRef::new(0)).matches_type(concrete)); - assert!(!WasmValue::from(AnyRef::from_host(0).unwrap()).matches_type(concrete)); - } - - #[test] - fn i31_only_matches_its_store_independent_supertypes() { - let value = WasmValue::from(AnyRef::from_i31(0).unwrap()); - - for ty in [AbstractHeapType::I31, AbstractHeapType::Eq, AbstractHeapType::Any] { - assert!(value.matches_type(WasmType::Ref(RefType::new_abstract(false, ty)))); - } - assert!(!value.matches_type(WasmType::Ref(RefType::new_abstract(false, AbstractHeapType::Struct)))); - } -} - -impl Debug for WasmValue { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::I32(i) => write!(f, "i32({i})"), - Self::I64(i) => write!(f, "i64({i})"), - Self::F32(i) => write!(f, "f32({i})"), - Self::F64(i) => write!(f, "f64({i})"), - Self::V128(i) => write!(f, "v128({i:?})"), - #[cfg(feature = "debug")] - Self::Ref(i) => write!(f, "ref({i:?})"), - #[cfg(not(feature = "debug"))] - Self::Ref(_) => write!(f, "ref(...)"), - } - } -} - -impl WasmValue { - #[inline] - /// Get the matching [`ConstInstruction`] for this value. - pub fn const_instr(&self) -> alloc::boxed::Box<[ConstInstruction]> { - alloc::boxed::Box::new([match self { - Self::I32(i) => ConstInstruction::I32Const(*i), - Self::I64(i) => ConstInstruction::I64Const(*i), - Self::F32(i) => ConstInstruction::F32Const(*i), - Self::F64(i) => ConstInstruction::F64Const(*i), - Self::V128(i) => ConstInstruction::V128Const(*i), - Self::Ref(i) => ConstInstruction::Ref(*i), - }]) - } - - #[inline] - /// Get the default value for a given type. - pub const fn default_for(ty: WasmType) -> Option { - match ty { - WasmType::I32 => Some(Self::I32(0)), - WasmType::I64 => Some(Self::I64(0)), - WasmType::F32 => Some(Self::F32(0.0)), - WasmType::F64 => Some(Self::F64(0.0)), - WasmType::V128 => Some(Self::V128([0; 16])), - WasmType::Ref(ty) if ty.is_nullable() => Some(Self::Ref(RefValue::Null)), - WasmType::Ref(_) => None, - } - } - - #[inline] - /// Check if two values are equal, ignoring differences in NaN values. - pub fn eq_loose(&self, other: &Self) -> bool { - match (self, other) { - (Self::I32(a), Self::I32(b)) => a == b, - (Self::I64(a), Self::I64(b)) => a == b, - (Self::V128(a), Self::V128(b)) => a == b || Self::v128_nan_eq(*a, *b), - (Self::Ref(a), Self::Ref(b)) => a == b, - (Self::F32(a), Self::F32(b)) => a.is_nan() && b.is_nan() || a.to_bits() == b.to_bits(), - (Self::F64(a), Self::F64(b)) => a.is_nan() && b.is_nan() || a.to_bits() == b.to_bits(), - _ => false, - } - } - - fn v128_nan_eq(a: [u8; 16], b: [u8; 16]) -> bool { - let a_f32x4: [f32; 4] = [ - f32::from_le_bytes([a[0], a[1], a[2], a[3]]), - f32::from_le_bytes([a[4], a[5], a[6], a[7]]), - f32::from_le_bytes([a[8], a[9], a[10], a[11]]), - f32::from_le_bytes([a[12], a[13], a[14], a[15]]), - ]; - let b_f32x4: [f32; 4] = [ - f32::from_le_bytes([b[0], b[1], b[2], b[3]]), - f32::from_le_bytes([b[4], b[5], b[6], b[7]]), - f32::from_le_bytes([b[8], b[9], b[10], b[11]]), - f32::from_le_bytes([b[12], b[13], b[14], b[15]]), - ]; - - let all_nan_match = a_f32x4.iter().zip(b_f32x4.iter()).all(|(x, y)| { - if x.is_nan() && y.is_nan() { - true - } else if x.is_nan() || y.is_nan() { - false - } else { - x.to_bits() == y.to_bits() - } - }); - - if all_nan_match && a_f32x4.iter().any(|x| x.is_nan()) { - return true; - } - - let a_f64x2: [f64; 2] = [ - f64::from_le_bytes([a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]]), - f64::from_le_bytes([a[8], a[9], a[10], a[11], a[12], a[13], a[14], a[15]]), - ]; - let b_f64x2: [f64; 2] = [ - f64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]), - f64::from_le_bytes([b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]]), - ]; - - a_f64x2.iter().zip(b_f64x2.iter()).all(|(x, y)| { - if x.is_nan() && y.is_nan() { - true - } else if x.is_nan() || y.is_nan() { - false - } else { - x.to_bits() == y.to_bits() - } - }) && a_f64x2.iter().any(|x| x.is_nan()) - } -} +use crate::{RefType, StorageType}; /// Type of a WebAssembly value. #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] pub enum WasmType { - /// A 32-bit integer + /// A 32-bit integer. I32, - /// A 64-bit integer + /// A 64-bit integer. I64, - /// A 32-bit float + /// A 32-bit float. F32, - /// A 64-bit float + /// A 64-bit float. F64, - /// A 128-bit vector + /// A 128-bit vector. V128, - /// A reference type + /// A reference type. Ref(RefType), } impl WasmType { #[inline] - pub const fn default_value(&self) -> Option { - WasmValue::default_for(*self) - } - - #[inline] - pub const fn is_simd(&self) -> bool { + pub const fn is_simd(self) -> bool { matches!(self, Self::V128) } } -macro_rules! impl_conversion_for_wasmvalue { - ($($t:ty => $variant:ident, $accessor:ident, $doc:literal);* $(;)?) => { - impl WasmValue { - $( - #[doc = $doc] - pub const fn $accessor(&self) -> Option<$t> { - match self { - Self::$variant(value) => Some(*value), - _ => None, - } - } - )* +impl From for WasmType { + fn from(value: StorageType) -> Self { + match value { + StorageType::I8 | StorageType::I16 => Self::I32, + StorageType::Value(value) => value, } - - $( - impl From<$t> for WasmValue { - #[inline] - fn from(i: $t) -> Self { - Self::$variant(i) - } - } - - impl TryFrom for $t { - type Error = (); - - #[inline] - fn try_from(value: WasmValue) -> Result { - if let WasmValue::$variant(i) = value { Ok(i) } else { Err(()) } - } - } - )* } } - -impl_conversion_for_wasmvalue! { - i32 => I32, as_i32, "Return the `i32` from a `WasmValue`, if it is an `I32`."; - i64 => I64, as_i64, "Return the `i64` from a `WasmValue`, if it is an `I64`."; - f32 => F32, as_f32, "Return the `f32` from a `WasmValue`, if it is a `F32`."; - f64 => F64, as_f64, "Return the `f64` from a `WasmValue`, if it is a `F64`."; - [u8; 16] => V128, as_v128, "Return the raw little-endian bytes from a `WasmValue`, if it is a `V128`."; - RefValue => Ref, as_ref, "Return the `RefValue` from a `WasmValue`, if it is a `Ref`."; -} - -macro_rules! impl_ref_conversion_for_wasmvalue { - ($($ty:ty => $variant:ident);* $(;)?) => { - $( - impl From<$ty> for WasmValue { - fn from(value: $ty) -> Self { - Self::Ref(RefValue::$variant(value)) - } - } - - impl TryFrom for $ty { - type Error = (); - - fn try_from(value: WasmValue) -> Result { - if let WasmValue::Ref(RefValue::$variant(value)) = value { Ok(value) } else { Err(()) } - } - } - )* - }; -} - -impl_ref_conversion_for_wasmvalue! { - crate::FuncRef => Func; - crate::ExternRef => Extern; - crate::AnyRef => Any; - crate::ExnRef => Exn; -} diff --git a/examples/archive.rs b/examples/archive.rs index 3f64704..3f118f2 100644 --- a/examples/archive.rs +++ b/examples/archive.rs @@ -11,13 +11,13 @@ const WASM: &str = r#" "#; fn main() -> Result<()> { - let wasm = wat::parse_str(WASM).expect("Failed to parse WAT"); + let wasm = wat::parse_str(WASM)?; let module = Parser::default().parse_module_bytes(wasm)?; - let twasm = module.serialize_twasm()?; - // Now, you could e.g. write `twasm` to a file called `add.twasm` - // and load it later in a different program. + // Serialize the optimized module for storage or distribution. + let twasm = module.serialize_twasm()?; + // Archived modules load without parsing Wasm again and should only come from trusted sources. let module = Module::try_from_twasm(&twasm)?; let mut store = Store::default(); let instance = ModuleInstance::instantiate(&mut store, &module, None)?; diff --git a/examples/simple.rs b/examples/basic.rs similarity index 72% rename from examples/simple.rs rename to examples/basic.rs index 0d57265..1f717c5 100644 --- a/examples/simple.rs +++ b/examples/basic.rs @@ -11,10 +11,14 @@ const WASM: &str = r#" "#; fn main() -> Result<()> { - let wasm = wat::parse_str(WASM).expect("failed to parse wat"); + let wasm = wat::parse_str(WASM)?; let module = tinywasm::parse_bytes(&wasm)?; + + // Module is reusable, while Store owns the runtime state for this instance. let mut store = Store::default(); let instance = ModuleInstance::instantiate(&mut store, &module, None)?; + + // Typed handles validate parameters and results. Use func_untyped for dynamic values. let add = instance.func::<(i32, i32), i32>(&store, "add")?; assert_eq!(add.call(&mut store, (1, 2))?, 3); diff --git a/examples/exceptions.rs b/examples/exceptions.rs new file mode 100644 index 0000000..7c2f88e --- /dev/null +++ b/examples/exceptions.rs @@ -0,0 +1,28 @@ +use anyhow::{Result, bail}; +use tinywasm::{Error, ModuleInstance, Store, WasmValue}; + +const WASM: &str = r#" +(module + (tag $failure (export "failure") (param i32)) + (func (export "fail") (param i32) + local.get 0 + throw $failure)) +"#; + +fn main() -> Result<()> { + let wasm = wat::parse_str(WASM)?; + let module = tinywasm::parse_bytes(&wasm)?; + let mut store = Store::default(); + let instance = ModuleInstance::instantiate(&mut store, &module, None)?; + let fail = instance.func::(&store, "fail")?; + + // Uncaught guest exceptions cross the host boundary as owned ExnRef handles. + let Err(Error::Exception(exception)) = fail.call(&mut store, 42) else { + bail!("expected a guest exception"); + }; + + // The handle exposes the exception tag and typed payload values. + assert_eq!(exception.tag(&store)?, instance.tag("failure")?); + assert_eq!(exception.fields(&mut store)?, [WasmValue::I32(42)]); + Ok(()) +} diff --git a/examples/funcref_callbacks.rs b/examples/funcref_callbacks.rs deleted file mode 100644 index 959d798..0000000 --- a/examples/funcref_callbacks.rs +++ /dev/null @@ -1,123 +0,0 @@ -use anyhow::Result; -use tinywasm::{FuncContext, HostFunction, Imports, ModuleInstance, Store, types::FuncRef}; - -const LHS: i32 = 5; -const RHS: i32 = 3; - -fn main() -> Result<()> { - run_passed_funcref_example()?; - run_returned_funcref_example()?; - Ok(()) -} - -fn run_passed_funcref_example() -> Result<()> { - // Host receives funcref and calls it via an exported proxy. - const WASM: &str = r#" - (module - (import "host" "call_this" (func $host_callback_caller (param funcref))) - (import "host" "mul" (func $host_mul (param $x i32) (param $y i32) (result i32))) - - (func $tell_host_to_call (export "tell_host_to_call") - (call $host_callback_caller (ref.func $add)) - (call $host_callback_caller (ref.func $sub)) - (call $host_callback_caller (ref.func $host_mul)) - ) - - (type $binop (func (param i32 i32) (result i32))) - - (table 3 funcref) - (elem (i32.const 0) $add $sub $host_mul) - (func $add (param $x i32) (param $y i32) (result i32) - local.get $x - local.get $y - i32.add - ) - (func $sub (param $x i32) (param $y i32) (result i32) - local.get $x - local.get $y - i32.sub - ) - - (table $callback_register 1 funcref) - (func (export "call_binop_by_ref") (param funcref i32 i32) (result i32) - (table.set $callback_register (i32.const 0) (local.get 0)) - (call_indirect $callback_register (type $binop) (local.get 1)(local.get 2)(i32.const 0)) - ) - ) - "#; - - let wasm = wat::parse_str(WASM).expect("failed to parse wat"); - let module = tinywasm::parse_bytes(&wasm)?; - let mut store = Store::default(); - - let mul = HostFunction::from(|_, (lhs, rhs): (i32, i32)| -> tinywasm::Result { Ok(lhs * rhs) }); - let call_this = HostFunction::from(|mut ctx: FuncContext<'_>, func_ref: FuncRef| -> tinywasm::Result<()> { - // Host cannot call a funcref directly, so it routes through Wasm. - let call_by_ref = ctx.module().func::<(FuncRef, i32, i32), i32>(ctx.store(), "call_binop_by_ref")?; - let _result = call_by_ref.call(ctx.store_mut(), (func_ref, LHS, RHS))?; - Ok(()) - }); - - let mut imports = Imports::new(); - imports.define("host", "call_this", call_this).define("host", "mul", mul); - - let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; - let caller = instance.func::<(), ()>(&store, "tell_host_to_call")?; - - caller.call(&mut store, ())?; - - Ok(()) -} - -fn run_returned_funcref_example() -> Result<()> { - // Wasm returns funcref values, host executes them through the same proxy. - const WASM: &str = r#" - (module - (import "host" "mul" (func $host_mul (param $x i32) (param $y i32) (result i32))) - (type $binop (func (param i32 i32) (result i32))) - (table 3 funcref) - (elem (i32.const 0) $add $sub $host_mul) - (func $add (param $x i32) (param $y i32) (result i32) - local.get $x - local.get $y - i32.add - ) - (func $sub (param $x i32) (param $y i32) (result i32) - local.get $x - local.get $y - i32.sub - ) - (func $ref_to_funcs (export "what_should_host_call") (result funcref funcref funcref) - (ref.func $add) - (ref.func $sub) - (ref.func $host_mul) - ) - - (table $callback_register 1 funcref) - (func $call (export "call_binop_by_ref") (param funcref i32 i32) (result i32) - (table.set $callback_register (i32.const 0) (local.get 0)) - (call_indirect $callback_register (type $binop) (local.get 1)(local.get 2)(i32.const 0)) - ) - ) - "#; - - let wasm = wat::parse_str(WASM).expect("failed to parse wat"); - let module = tinywasm::parse_bytes(&wasm)?; - let mut store = Store::default(); - let mut imports = Imports::new(); - - let mul = HostFunction::from(|_, (lhs, rhs): (i32, i32)| -> tinywasm::Result { Ok(lhs * rhs) }); - imports.define("host", "mul", mul); - - let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; - let (add_ref, sub_ref, mul_ref) = { - let get_funcrefs = instance.func::<(), (FuncRef, FuncRef, FuncRef)>(&store, "what_should_host_call")?; - get_funcrefs.call(&mut store, ())? - }; - - let call_by_ref = instance.func::<(FuncRef, i32, i32), i32>(&store, "call_binop_by_ref")?; - for func_ref in [add_ref, sub_ref, mul_ref] { - let _result = call_by_ref.call(&mut store, (func_ref, LHS, RHS))?; - } - Ok(()) -} diff --git a/examples/gc.rs b/examples/gc.rs new file mode 100644 index 0000000..9a73d04 --- /dev/null +++ b/examples/gc.rs @@ -0,0 +1,33 @@ +use anyhow::Result; +use tinywasm::{ModuleInstance, Store, StructRef, WasmValue}; + +const WASM: &str = r#" +(module + (type $cube (struct (field (mut i32)) (field (mut i32)))) + (func $make-cube (result (ref $cube)) + i32.const 3 + i32.const 4 + struct.new $cube) + (func (export "cube") (result (ref struct)) + call $make-cube)) +"#; + +fn main() -> Result<()> { + let wasm = wat::parse_str(WASM)?; + let module = tinywasm::parse_bytes(&wasm)?; + let mut store = Store::default(); + let instance = ModuleInstance::instantiate(&mut store, &module, None)?; + let cube = instance.func::<(), StructRef>(&store, "cube")?.call(&mut store, ())?; + + // GC handles are Store-bound and expose typed field access through that Store. + assert_eq!(cube.fields(&mut store)?, [WasmValue::I32(3), WasmValue::I32(4)]); + cube.set_field(&mut store, 0, WasmValue::I32(5))?; + + // Cloned handles keep their guest objects live across explicit collection. + let retained = cube.clone(); + drop(cube); + store.gc()?; + + assert_eq!(retained.field(&mut store, 0)?, WasmValue::I32(5)); // still alive + Ok(()) +} diff --git a/examples/linking.rs b/examples/linking.rs index 2df7133..92c0b7d 100644 --- a/examples/linking.rs +++ b/examples/linking.rs @@ -1,7 +1,6 @@ use anyhow::Result; use tinywasm::{ModuleInstance, Store}; -// WebAssembly module defining and exporting an `add` function. const WASM_ADD: &str = r#" (module (func $add (param $lhs i32) (param $rhs i32) (result i32) @@ -11,7 +10,6 @@ const WASM_ADD: &str = r#" (export "add" (func $add))) "#; -// WebAssembly module importing an `add` function and using it. const WASM_IMPORT: &str = r#" (module (import "adder" "add" (func $add (param i32 i32) (result i32))) @@ -24,25 +22,23 @@ const WASM_IMPORT: &str = r#" "#; fn main() -> Result<()> { - let wasm_add = wat::parse_str(WASM_ADD).expect("failed to parse wat"); - let wasm_import = wat::parse_str(WASM_IMPORT).expect("failed to parse wat"); + let wasm_add = wat::parse_str(WASM_ADD)?; + let wasm_import = wat::parse_str(WASM_IMPORT)?; let add_module = tinywasm::parse_bytes(&wasm_add)?; let import_module = tinywasm::parse_bytes(&wasm_import)?; let mut store = Store::default(); - // Instantiate the `add` module. let add_instance = ModuleInstance::instantiate(&mut store, &add_module, None)?; - // Link the `adder` namespace to the `add` module's instance. + // Imports can link a module namespace and define individual host items together. let mut imports = tinywasm::Imports::new(); imports.link_module("adder", add_instance)?; - // Instantiate the `import` module with the linked imports. let import_instance = ModuleInstance::instantiate(&mut store, &import_module, Some(&imports))?; - // Call the `main` function, which uses the imported `add` function. + // Calling `main` crosses the linked module boundary to `add`. let main = import_instance.func::<(), i32>(&store, "main")?; assert_eq!(main.call(&mut store, ())?, 3); diff --git a/examples/reentrance.rs b/examples/reentrance.rs index 0084a61..cf57891 100644 --- a/examples/reentrance.rs +++ b/examples/reentrance.rs @@ -23,10 +23,11 @@ fn main() -> Result<()> { let mut store = Store::default(); let call_add_twice = HostFunction::from(|mut ctx: FuncContext<'_>, value: i32| { + // FuncContext exposes the active module and its Store to the callback. let add_one = ctx.module().func::(ctx.store(), "add_one")?; - // Use ctx.call for reentrant calls from host functions. Function::call - // starts a root invocation and cannot preserve the active call stacks. + // Use ctx.call while a host callback has an active Wasm invocation. + // Function::call only starts top-level invocations. let value = ctx.call(&add_one, value)?; ctx.call(&add_one, value) }); diff --git a/examples/references.rs b/examples/references.rs new file mode 100644 index 0000000..b02dd07 --- /dev/null +++ b/examples/references.rs @@ -0,0 +1,34 @@ +use anyhow::Result; +use tinywasm::{FuncRef, HostFunction, Imports, ModuleInstance, Store}; + +const WASM: &str = r#" +(module + (type $callback (func (param i32) (result i32))) + (import "host" "double" (func $double (type $callback))) + (elem declare func $double) + (func (export "callback") (result funcref) + ref.func $double) + (func (export "apply") (param funcref i32) (result i32) + local.get 1 + local.get 0 + ref.cast (ref $callback) + call_ref $callback)) +"#; + +fn main() -> Result<()> { + let mut store = Store::default(); + let mut imports = Imports::new(); + // Imports resolve reusable host definitions against each module's runtime types. + // HostFunction::instantiate is available when a standalone Function is needed. + imports.define("host", "double", HostFunction::from(|_, value: i32| -> tinywasm::Result { Ok(value * 2) })); + + let wasm = wat::parse_str(WASM)?; + let module = tinywasm::parse_bytes(&wasm)?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; + // Option maps nullable funcref. Bare FuncRef maps a non-null reference. + let callback = instance.func::<(), Option>(&store, "callback")?.call(&mut store, ())?; + let apply = instance.func::<(Option, i32), i32>(&store, "apply")?; + + assert_eq!(apply.call(&mut store, (callback, 21))?, 42); + Ok(()) +} diff --git a/examples/resumable.rs b/examples/resumable.rs index 26b5f47..70b0db1 100644 --- a/examples/resumable.rs +++ b/examples/resumable.rs @@ -29,10 +29,12 @@ fn main() -> Result<()> { let instance = ModuleInstance::instantiate(&mut store, &module, None)?; let count_down = instance.func::(&store, "count_down")?; + // A resumable call returns execution state instead of running to completion. let mut execution = count_down.call_resumable(&mut store, 10_000)?; let fuel_per_round = 128; let mut fuel_rounds = 0; + // Each resume call limits one round. std builds also support wall-clock budgets. let result = loop { fuel_rounds += 1; match execution.resume_with_fuel(fuel_per_round)? { diff --git a/examples/wasm-rust.rs b/examples/rust.rs similarity index 95% rename from examples/wasm-rust.rs rename to examples/rust.rs index 0cfb0ce..24b3b73 100644 --- a/examples/wasm-rust.rs +++ b/examples/rust.rs @@ -5,8 +5,8 @@ use tinywasm::{FuncContext, HostFunction, Imports, ModuleInstance, Store}; /// Examples of using WebAssembly compiled from Rust with tinywasm. /// -/// These examples are meant to be run with `cargo run --example wasm-rust `. -/// For example, `cargo run --example wasm-rust hello`. +/// These examples are meant to be run with `cargo run --example rust `. +/// For example, `cargo run --example rust hello`. /// /// To run these, you first need to compile the Rust examples to WebAssembly: /// @@ -23,12 +23,12 @@ fn main() -> Result<()> { pretty_env_logger::init(); if !std::path::Path::new("./examples/rust/out/").exists() { - return Err(anyhow!("No WebAssembly files found. See examples/wasm-rust.rs for instructions.")); + return Err(anyhow!("No WebAssembly files found. See examples/rust.rs for instructions.")); } let args = std::env::args().collect::>(); if args.len() < 2 { - println!("Usage: cargo run --example wasm-rust "); + println!("Usage: cargo run --example rust "); println!("Available examples:"); println!(" hello"); println!(" printi32"); @@ -105,6 +105,7 @@ fn hello() -> Result<()> { let module = tinywasm::parse_file("./examples/rust/out/hello.opt.wasm")?; let mut store = Store::default(); + // The host callback reads the guest's exported memory through FuncContext. let print_utf8 = HostFunction::from(|ctx: FuncContext<'_>, (ptr, len): (i64, i32)| { let mem = ctx.memory("memory")?; let string = mem.read_string(ctx.store(), ptr as usize, len as usize)?; @@ -119,6 +120,7 @@ fn hello() -> Result<()> { let arg_ptr = instance.func::<(), i32>(&store, "arg_ptr")?.call(&mut store, ())?; let arg = b"world"; + // Write the host argument into guest memory before calling the Rust export. instance.memory("memory")?.copy_from_slice(&mut store, arg_ptr as usize, arg)?; let hello = instance.func::(&store, "hello")?; hello.call(&mut store, arg.len() as i32)?; diff --git a/examples/rust/README.md b/examples/rust/README.md index 8d3178a..305a29c 100644 --- a/examples/rust/README.md +++ b/examples/rust/README.md @@ -1,9 +1,9 @@ # WebAssembly Rust Examples This is a separate crate that generates WebAssembly from Rust code. -It is used by the `wasm-rust` example. +It is used by the `rust` example. Requires the `wasm32-unknown-unknown` target to be installed. -To build the example artifacts used by `cargo run --example wasm-rust -- `, run `./examples/rust/build.sh`. +To build the example artifacts used by `cargo run --example rust -- `, run `./examples/rust/build.sh`. That script also requires `binaryen` and `wabt` to be installed. diff --git a/examples/simple2.rs b/examples/simple2.rs deleted file mode 100644 index f673dfe..0000000 --- a/examples/simple2.rs +++ /dev/null @@ -1,21 +0,0 @@ -use anyhow::Result; -use tinywasm::{ModuleInstance, Store}; - -const WASM: &str = r#" -(module - (func $return (param $lhs i32) (param $rhs i64) (result i32 i64) - local.get $lhs - local.get $rhs) - (export "return" (func $return))) -"#; - -fn main() -> Result<()> { - let wasm = wat::parse_str(WASM).expect("failed to parse wat"); - let module = tinywasm::parse_bytes(&wasm)?; - let mut store = Store::default(); - let instance = ModuleInstance::instantiate(&mut store, &module, None)?; - let add = instance.func::<(i32, i64), (i32, i64)>(&store, "return")?; - - assert_eq!(add.call(&mut store, (1, 2))?, (1, 2)); - Ok(()) -} From 49844055b58e7d99f3b407002ddca304d37a68cb Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 24 Aug 2026 00:22:38 +0200 Subject: [PATCH 2/3] feat: write function results into caller-provided slices - update dynamic calls, host functions, docs, tests, CLI, and benchmarks - reuse result buffers and validate parameter and result arity - fix 32-bit GC array traps and WAST failure output Signed-off-by: Henry --- CHANGELOG.md | 59 ++++---- README.md | 4 +- crates/cli/src/cmd/run.rs | 5 +- crates/cli/src/testsuite.rs | 17 +-- crates/cli/src/wast_runner.rs | 41 +++--- crates/tinywasm/src/func/context.rs | 42 +++--- crates/tinywasm/src/func/host.rs | 56 +++++--- crates/tinywasm/src/func/mod.rs | 73 ++++++---- crates/tinywasm/src/func/resume.rs | 69 +++++----- crates/tinywasm/src/func/values.rs | 1 + crates/tinywasm/src/instance.rs | 7 +- crates/tinywasm/src/interpreter/executor.rs | 51 ++++--- crates/tinywasm/src/reference/store.rs | 19 +-- crates/tinywasm/src/reference/value.rs | 22 ++- crates/tinywasm/src/store/gc/mod.rs | 18 ++- crates/tinywasm/src/store/mod.rs | 53 ++++++-- crates/tinywasm/src/store/state.rs | 8 +- crates/tinywasm/tests/gc_refs.rs | 51 ++++--- .../tests/host_func_signature_check.rs | 127 ++++++++++++++++-- crates/tinywasm/tests/internal_refs.rs | 10 +- crates/tinywasm/tests/managed_exceptions.rs | 20 +-- crates/tinywasm/tests/reference_roots.rs | 28 ++-- crates/tinywasm/tests/resume_execution.rs | 16 ++- crates/tinywasm/tests/store_ownership.rs | 2 +- crates/tinywasm/tests/typed_globals.rs | 4 +- crates/types/src/types.rs | 10 ++ 26 files changed, 531 insertions(+), 282 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9941450..6509552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,27 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added support for the WebAssembly function-references proposal -- Added support for the WebAssembly garbage-collection proposal -- Added support for the WebAssembly exception-handling proposal, including tags, `try_table`, `throw`, and `throw_ref` -- Added support for the WebAssembly compact-imports proposal -- Added `WasmValue::ty` and `WasmValue::matches_type`. Non-null concrete reference types require Store-aware validation. -- Added `ValueLane` for mapping WebAssembly value types to their physical 32-bit, 64-bit, or 128-bit storage lane. -- Added a `validate` feature to `tinywasm` and `tinywasm-parser` (enabled by default) to optionally skip wasmparser validation for faster parsing of trusted modules. -- Added optional parse-time operand deduplication to reduce precompiled module and `.twasm` archive size. -- Added a `ResourceLimiter` trait, configurable through `engine::Config::with_resource_limiter`, to bound guest memory, table, and logical GC heap growth. -- Added Store-aware owned references, explicit GC collection, and typed host access to GC objects and exceptions. +- Support for the function-references, garbage-collection, exception-handling, and compact-imports proposals. +- Store-aware owned references, explicit GC collection, and typed host access to GC objects and exceptions. +- `WasmValue::ty`, `WasmValue::matches_type`, and `ValueLane` for inspecting value types and storage lanes. +- `ResourceLimiter` for limiting guest memory, table, and logical GC heap growth. +- A default `validate` Cargo feature that can be disabled when parsing trusted modules. +- Optional parse-time operand deduplication for smaller `.twasm` archives. ### Changed -- `HostFunction` is now a reusable definition, and module instantiation borrows `Imports` so host imports can be shared across stores. -- Host function callbacks now require `Send + Sync` so the same definition can be used safely with multiple stores. -- Typed function tuples now support up to 20 parameters or results. Use untyped functions for larger signatures. -- Module types now use one dense recursive type space, while function types are resolved through `Function::ty(&Store)`. -- Globals are stored in separate 32-bit, 64-bit, and 128-bit value lanes, avoiding tagged value conversion during guest execution. -- Linear memory now uses a single contiguous `Vec`-backed storage with const-generic fixed-width loads and stores. -- Exceptions are stored as traced managed objects, allowing unreachable exceptions and their payload graphs to be collected. -- Increased the minimum supported Rust version from 1.95 to 1.98. +- Typed functions support tuples up to arity 20 and `[u8; 16]` values for `v128`. +- Module types use one recursive type space, with runtime function types available through `Function::ty(&Store)`. +- Linear memory uses contiguous `Vec`-backed storage. +- The minimum supported Rust version is 1.98. ### Fixed @@ -41,23 +33,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking Changes -- `HostFunction::from` and `HostFunction::from_untyped` no longer take a `Store` and now return reusable `HostFunction` definitions -- `ModuleInstance::instantiate` and `instantiate_no_start` now borrow `Imports`. -- `Store::id` now returns `u32` instead of `usize`. -- `Parser::new` now takes `ParserOptions`, use `Parser::default()` for default settings. `Parser::with_options` was removed. -- Renamed `ModuleInstanceAddr` to `ModuleInstanceId`. -- Removed `HostFunction::ty` and `WasmFunction::ty`. Use `Function::ty(&Store)` for runtime function types. -- Changed `TableType::element_type` and `Element::ty` from `WasmType` to `RefType`, and replaced module `table_types` with `TableDefinition { ty, init }`. -- Removed the pluggable memory backend system (`LinearMemory`, `MemoryBackend`, `VecMemory`, `PagedMemory`, `LazyLinearMemory`, and `Config::with_memory_backend`). Linear memory is always `Vec`-backed. To limit initial memory allocation and growth, configure a `ResourceLimiter` with `Config::with_resource_limiter`. -- Removed the local-memory allocation analysis (`LocalMemoryAllocation` and `ParserOptions::optimize_local_memory_allocation`). Local memories are always allocated eagerly. -- Removed `Config::with_trap_on_oom`. A `ResourceLimiter` can return a trap when rejecting a memory or table allocation or growth request. -- `Table::grow` now returns `Result>`, matching `Memory::grow`. Growth limits and allocation failures return `None`, while limiter-provided traps return an error. -- Public function and managed references are Store-aware. Managed references such as `StructRef`, `ArrayRef`, `ExternRef`, and `ExnRef` are owned handles that keep their referents live until the last clone is dropped. -- `ExternRef::try_new` and `I31Ref::try_new` create host references. Reference data and GC object access are provided by inherent methods on each reference type. -- Renamed the fallible `Memory`, `Table`, `Global`, and `Tag` constructors from `new` to `try_new`. -- `WasmValue` is no longer `Copy` because it can contain owned references. -- Nullable typed reference parameters and results use `Option`. Bare typed reference values are non-null. -- Removed `WasmTupleChain`. Use direct tuples up to arity 20 or untyped functions for larger signatures. +- Dynamic `Function::call` and `Function::call_resumable` calls write to caller-provided result slices. Untyped host callbacks also receive a result slice and return `Result<()>`. +- `HostFunction` definitions are reusable, require `Send + Sync`, and no longer take a `Store`. +- Module instantiation borrows `Imports`, allowing imports to be shared across stores. +- `HostFunction::ty` and `WasmFunction::ty` were removed. Use `Function::ty(&Store)` for runtime types. +- Function and managed reference handles are Store-aware. Managed references keep their referents live, and `WasmValue` is no longer `Copy`. +- Nullable typed reference parameters and results use `Option`. Bare typed references are non-null. +- Host references are created with `ExternRef::try_new` and `I31Ref::try_new`. Reference and GC object access is provided by methods on each reference type. +- Fallible `Memory`, `Table`, `Global`, and `Tag` constructors are named `try_new`. +- `Store::id` returns `u32`, and `ModuleInstanceAddr` is renamed to `ModuleInstanceId`. +- Table element types use `RefType`, and module table definitions use `TableDefinition { ty, init }`. +- `Parser::new` takes `ParserOptions`. Use `Parser::default()` for default settings. `Parser::with_options` and local-memory allocation analysis were removed. +- Pluggable memory backends and `Config::with_trap_on_oom` were removed. Linear memory is always `Vec`-backed, and allocation limits use `ResourceLimiter`. +- `Table::grow` returns `Result>`, matching `Memory::grow`. +- `WasmTupleChain` was removed. Use direct tuples up to arity 20 or untyped functions for larger signatures. ## [0.10.0] - 2026-07-24 diff --git a/README.md b/README.md index 186e4dc..956d160 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ let result = func.call(&mut store, (1, 2))?; assert_eq!(result, 3); ``` -See the [examples](./examples), including the [GC reference example](./examples/gc.rs), and the [documentation](https://docs.rs/tinywasm) for more information. +See the [examples](./examples) directory and [documentation](https://docs.rs/tinywasm) for more information. ## Precompiled Modules @@ -95,8 +95,8 @@ TinyWasm targets non-JavaScript core proposals through [phase 3](https://github. | [**Typed Function References**](https://github.com/WebAssembly/function-references) | 🟢 | `next` | | [**Garbage Collection**](https://github.com/WebAssembly/gc) | 🟢 | `next` | | [**Exception Handling**](https://github.com/WebAssembly/exception-handling) | 🟢 | `next` | -| [**Stack Switching**](https://github.com/WebAssembly/stack-switching) | 🌑 | - | | [**Compact Import Section**](https://github.com/WebAssembly/compact-import-section) | 🟢 | `next` | +| [**Stack Switching**](https://github.com/WebAssembly/stack-switching) | 🌑 | - | | [**Threads**](https://github.com/WebAssembly/threads) | 🌑 | - | **Legend**\ diff --git a/crates/cli/src/cmd/run.rs b/crates/cli/src/cmd/run.rs index 3d25641..52cd273 100644 --- a/crates/cli/src/cmd/run.rs +++ b/crates/cli/src/cmd/run.rs @@ -29,7 +29,8 @@ pub fn run(args: RunArgs) -> Result<()> { .ok_or_else(|| anyhow::anyhow!("export is not a function: {export}"))?; let func = instance.func_untyped(&store, export)?; let params = parse_invocation_args(func_ty, &args.args)?; - let results = func.call(&mut store, ¶ms)?; + let mut results = vec![tinywasm::WasmValue::I32(0); func.ty(&store)?.results().len()]; + func.call(&mut store, ¶ms, &mut results)?; print_results(&results); Ok(()) } @@ -44,7 +45,7 @@ pub fn run(args: RunArgs) -> Result<()> { "module has no start function or `_start` export. Use `tinywasm inspect {module_path}` or `tinywasm run --invoke {module_path}`" ) })?; - start.call(&mut store, &[])?; + start.call(&mut store, &[], &mut [])?; Ok(()) } } diff --git a/crates/cli/src/testsuite.rs b/crates/cli/src/testsuite.rs index 032aa88..b23b2c8 100644 --- a/crates/cli/src/testsuite.rs +++ b/crates/cli/src/testsuite.rs @@ -7,9 +7,14 @@ use std::io::{BufRead, BufReader, Seek, SeekFrom}; /// Result type used by the WAST test utilities. pub type TestResult = core::result::Result>; -#[derive(Debug)] struct TestFailure(String); +impl core::fmt::Debug for TestFailure { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(self, f) + } +} + impl Display for TestFailure { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0) @@ -89,13 +94,9 @@ impl TestSuite { } pub fn report_status(&self) -> TestResult<()> { - if self.runner.failed() { - println!(); - Err(TestFailure(format!("{}:\n{self}", "failed one or more tests".red().bold())).into()) - } else { - println!("{self}"); - Ok(()) - } + self.print_errors(); + println!("{self}"); + if self.runner.failed() { Err(TestFailure("failed one or more tests".into()).into()) } else { Ok(()) } } pub fn save_csv(&self, path: &str, version: &str) -> TestResult<()> { diff --git a/crates/cli/src/wast_runner.rs b/crates/cli/src/wast_runner.rs index 65fba7e..3f905d4 100644 --- a/crates/cli/src/wast_runner.rs +++ b/crates/cli/src/wast_runner.rs @@ -225,6 +225,7 @@ impl WastRunner { let mut store = Store::default(); let mut module_registry = ModuleRegistry::default(); + let mut call_results = Vec::new(); println!("running {} tests for group: {}", directives.len(), file.name()); for (i, directive) in directives.into_iter().enumerate() { @@ -332,8 +333,9 @@ impl WastRunner { AssertExhaustion { call, message, span } => { let module = module_registry.get_idx(call.module); let args = convert_wastargs(&mut store, call.args)?; - let res = - catch_unwind_silent(|| exec_fn_instance(module, &mut store, call.name, &args).map(|_| ())); + let res = catch_unwind_silent(|| { + exec_fn_instance(module, &mut store, call.name, &args, &mut call_results) + }); let Ok(Err(tinywasm::Error::Trap(trap))) = res else { test_group.add_result( &format!("AssertExhaustion({i})"), @@ -368,7 +370,7 @@ impl WastRunner { let module = module_registry.get_idx(invoke.module); let args = convert_wastargs(&mut store, invoke.args) .map_err(|err| tinywasm::Error::Other(err.to_string()))?; - exec_fn_instance(module, &mut store, invoke.name, &args).map(|_| ()) + exec_fn_instance(module, &mut store, invoke.name, &args, &mut call_results) }); match res { Err(err) => test_group.add_result( @@ -415,7 +417,7 @@ impl WastRunner { let module = module_registry.get_idx(invoke.module); let args = convert_wastargs(&mut store, invoke.args) .map_err(|err| tinywasm::Error::Other(err.to_string()))?; - exec_fn_instance(module, &mut store, invoke.name, &args).map(|_| ()) + exec_fn_instance(module, &mut store, invoke.name, &args, &mut call_results) }); let result = match res { Err(err) => Err(anyhow!("test panicked: {}", try_downcast_panic(err))), @@ -469,7 +471,7 @@ impl WastRunner { let res: Result, _> = catch_unwind_silent(|| { let args = convert_wastargs(&mut store, invoke.args)?; let module = module_registry.get_idx(invoke.module); - exec_fn_instance(module, &mut store, invoke.name, &args).map_err(|e| { + exec_fn_instance(module, &mut store, invoke.name, &args, &mut call_results).map_err(|e| { error!("failed to execute function: {e:?}"); e })?; @@ -554,21 +556,21 @@ impl WastRunner { let module = module_registry .get(invoke.module) .ok_or_else(|| anyhow!("module instance was not found"))?; - let outcomes = - exec_fn_instance(Some(module.id()), &mut store, invoke.name, &args).map_err(|e| { + exec_fn_instance(Some(module.id()), &mut store, invoke.name, &args, &mut call_results) + .map_err(|e| { error!("failed to execute function: {e:?}"); e })?; - if !expected_alternatives.iter().any(|expected| expected.len() == outcomes.len()) { + if !expected_alternatives.iter().any(|expected| expected.len() == call_results.len()) { return Err(anyhow!( "expected {} results, got {}", expected_alternatives.first().map_or(0, |v| v.len()), - outcomes.len() + call_results.len() )); } if expected_alternatives.iter().any(|expected| { - expected.len() == outcomes.len() - && outcomes + expected.len() == call_results.len() + && call_results .iter() .zip(expected.iter()) .all(|(outcome, exp)| exp.matches(outcome, &store, &module)) @@ -716,12 +718,12 @@ fn exec_with_budget( func: &tinywasm::Function, store: &mut Store, args: &[WasmValue], -) -> Result, tinywasm::Error> { - let mut exec = func.call_resumable(store, args)?; + results: &mut [WasmValue], +) -> Result<(), tinywasm::Error> { + let mut exec = func.call_resumable(store, args, results)?; for _ in 0..TEST_MAX_SUSPENSIONS { - match exec.resume_with_time_budget(TEST_TIME_SLICE)? { - ExecProgress::Completed(values) => return Ok(values), - ExecProgress::Suspended => {} + if let ExecProgress::Completed(()) = exec.resume_with_time_budget(TEST_TIME_SLICE)? { + return Ok(()); } } Err(tinywasm::Error::Other(format!( @@ -742,7 +744,8 @@ fn exec_fn_instance( store: &mut Store, name: &str, args: &[WasmValue], -) -> Result, tinywasm::Error> { + results: &mut Vec, +) -> Result<(), tinywasm::Error> { let Some(instance) = instance else { return Err(tinywasm::Error::Other("no instance found".to_string())); }; @@ -750,7 +753,9 @@ fn exec_fn_instance( return Err(tinywasm::Error::Other("no instance found".to_string())); }; let func = instance.func_untyped(store, name)?; - exec_with_budget(&func, store, args) + results.clear(); + results.resize(func.ty(store)?.results().len(), WasmValue::I32(0)); + exec_with_budget(&func, store, args, results) } fn catch_unwind_silent(f: impl FnOnce() -> R) -> std::thread::Result { diff --git a/crates/tinywasm/src/func/context.rs b/crates/tinywasm/src/func/context.rs index 3df522f..7e289f6 100644 --- a/crates/tinywasm/src/func/context.rs +++ b/crates/tinywasm/src/func/context.rs @@ -1,4 +1,3 @@ -use alloc::vec::Vec; use tinywasm_types::ModuleInstanceId; use crate::{Error, FromWasmValues, FuncRef, Function, FunctionTyped, IntoWasmValues, Result, WasmValue}; @@ -84,27 +83,26 @@ impl FuncContext<'_> { /// Nested calls are currently blocking only. If the surrounding invocation /// is resumed with fuel or a time budget, this method does not suspend and /// later continue the host function in the middle of the nested call. - pub fn call_untyped(&mut self, func: &Function, args: &[WasmValue]) -> Result> { + pub fn call_untyped(&mut self, func: &Function, args: &[WasmValue], results: &mut [WasmValue]) -> Result<()> { if !self.store.execution_active { return Err(Error::other("FuncContext::call requires an active host-function invocation")); } - func.item.validate_store(self.store)?; - func.validate_params(self.store, args)?; + func.validate_call(self.store, args, results.len())?; let call_stack_base = self.store.call_stack.len(); let value_stack_base = self.store.value_stack.base(); - func.call_untyped(self.store, args, call_stack_base, value_stack_base) + func.call_untyped(self.store, args, results, call_stack_base, value_stack_base) } /// Calls a Store-aware function reference in the current module context. - pub fn call_ref(&mut self, func: FuncRef, args: &[WasmValue]) -> Result> { + pub fn call_ref(&mut self, func: FuncRef, args: &[WasmValue], results: &mut [WasmValue]) -> Result<()> { let addr = func.addr(self.store.store_id()).ok_or(crate::Trap::InvalidStore)?; if self.store.state.funcs.get(addr as usize).is_none() { return Err(crate::Trap::InvalidReference.into()); } let function = Function { item: crate::StoreItem::new(self.store.store_id(), addr), module_id: self.module_id }; - self.call_untyped(&function, args) + self.call_untyped(&function, args, results) } /// Call a typed function from within the current host-function invocation. @@ -122,15 +120,29 @@ impl FuncContext<'_> { func.func.item.validate_store(self.store)?; let func_instance = self.store.state.get_func(func.func.addr()).clone(); if matches!(&func_instance.kind, crate::store::FunctionKind::Host(host) if host.typed_callback().is_none()) { - let params = params.into_wasm_values().collect::>(); - let results = self.call_untyped(&func.func, ¶ms)?; - let mut values = results.into_iter(); - return R::from_wasm_values_exact(&mut values); + let ty = self.store.state.get_canonical_func_type(func_instance.type_addr); + let (param_count, result_count) = (ty.params().len(), ty.results().len()); + self.store.with_scratch_values(param_count + result_count, |store, values| { + super::write_typed_params(&mut values[..param_count], params.into_wasm_values())?; + let (params, results) = values.split_at_mut(param_count); + func.func.validate_call(store, params, results.len())?; + let call_stack_base = store.call_stack.len(); + let value_stack_base = store.value_stack.base(); + func.func.call_untyped(store, params, results, call_stack_base, value_stack_base)?; + values.drain(..param_count); + R::from_wasm_values_exact(&mut values.drain(..)) + }) + } else { + let call_stack_base = self.store.call_stack.len(); + let value_stack_base = self.store.value_stack.base(); + func.func.call_typed( + self.store, + &func_instance, + params.into_wasm_values(), + call_stack_base, + value_stack_base, + ) } - - let call_stack_base = self.store.call_stack.len(); - let value_stack_base = self.store.value_stack.base(); - func.func.call_typed(self.store, &func_instance, params.into_wasm_values(), call_stack_base, value_stack_base) } } diff --git a/crates/tinywasm/src/func/host.rs b/crates/tinywasm/src/func/host.rs index d1c4605..8f36761 100644 --- a/crates/tinywasm/src/func/host.rs +++ b/crates/tinywasm/src/func/host.rs @@ -42,20 +42,26 @@ impl HostFunction { module_id: ModuleInstanceId, type_addr: TypeAddr, args: &[WasmValue], - ) -> Result> { - let result = match &self.0.callback { - HostCallback::Untyped(func) => func(FuncContext { store, module_id }, args), - HostCallback::Typed(func) => func.call(FuncContext { store, module_id }, args), - }; + results: &mut [WasmValue], + ) -> Result<()> { let expected = store.state.get_canonical_func_type(type_addr).clone(); - let result = result?; - if result.len() == expected.results().len() - && result.iter().zip(expected.results()).all(|(value, &ty)| store.value_matches_type(value, ty)) - { - Ok(result) - } else { - Err(crate::Error::InvalidHostFnReturn { expected: Box::new(expected.clone()), actual: result }) + if results.len() != expected.results().len() { + return Err(crate::Error::other("host result buffer has the wrong length")); + } + for (result, ty) in results.iter_mut().zip(expected.results()) { + *result = match ty { + WasmType::I32 => WasmValue::I64(0), + _ => WasmValue::I32(0), + }; + } + match &self.0.callback { + HostCallback::Untyped(func) => func(FuncContext { store, module_id }, args, results)?, + HostCallback::Typed(func) => func.call(FuncContext { store, module_id }, args, results)?, + } + if !results.iter().zip(expected.results()).all(|(value, &ty)| store.value_matches_type(value, ty)) { + return Err(crate::Error::InvalidHostFnReturn { expected: Box::new(expected), actual: results.to_vec() }); } + Ok(()) } /// Returns the allocation-free typed callback when one is available. @@ -92,7 +98,9 @@ impl HostFunction { /// let add_one = HostFunction::from(|_ctx, value: i32| Ok(value + 1)); /// let function = add_one.instantiate(&mut store)?; /// - /// assert_eq!(function.call(&mut store, &[WasmValue::I32(41)])?, [WasmValue::I32(42)]); + /// let mut results = [WasmValue::I32(0)]; + /// function.call(&mut store, &[WasmValue::I32(41)], &mut results)?; + /// assert_eq!(results, [WasmValue::I32(42)]); /// # Ok(()) /// # } /// ``` @@ -131,11 +139,12 @@ impl HostFunction { /// # let module = tinywasm::parse_bytes(&wasm)?; /// let mut store = Store::default(); /// let ty = FuncType::new(&[WasmType::I32], &[WasmType::I32]); - /// let add_one = HostFunction::from_untyped(&ty, |_ctx: FuncContext<'_>, args| { + /// let add_one = HostFunction::from_untyped(&ty, |_ctx: FuncContext<'_>, args, results| { /// let WasmValue::I32(value) = args[0] else { /// return Err(tinywasm::Error::Other("expected i32".into())); /// }; - /// Ok(vec![WasmValue::I32(value + 1)]) + /// results[0] = WasmValue::I32(value + 1); + /// Ok(()) /// }); /// /// let mut imports = Imports::new(); @@ -148,7 +157,7 @@ impl HostFunction { /// ``` pub fn from_untyped( ty: &FuncType, - func: impl Fn(FuncContext<'_>, &[WasmValue]) -> Result> + Send + Sync + 'static, + func: impl Fn(FuncContext<'_>, &[WasmValue], &mut [WasmValue]) -> Result<()> + Send + Sync + 'static, ) -> Self { Self(Arc::new(HostFunctionInner { ty: ty.clone(), callback: HostCallback::Untyped(Box::new(func)) })) } @@ -202,10 +211,10 @@ enum HostCallback { Typed(Box), } -type UntypedHostCallback = dyn Fn(FuncContext<'_>, &[WasmValue]) -> Result> + Send + Sync; +type UntypedHostCallback = dyn Fn(FuncContext<'_>, &[WasmValue], &mut [WasmValue]) -> Result<()> + Send + Sync; pub(crate) trait TypedHostCallback: Send + Sync { - fn call(&self, ctx: FuncContext<'_>, args: &[WasmValue]) -> Result>; + fn call(&self, ctx: FuncContext<'_>, args: &[WasmValue], results: &mut [WasmValue]) -> Result<()>; fn call_stack(&self, store: &mut Store, module_id: ModuleInstanceId, type_addr: TypeAddr) -> Result<()>; } @@ -220,10 +229,17 @@ where P: FromWasmValues, R: IntoWasmValues, { - fn call(&self, ctx: FuncContext<'_>, args: &[WasmValue]) -> Result> { + fn call(&self, ctx: FuncContext<'_>, args: &[WasmValue], results: &mut [WasmValue]) -> Result<()> { let mut values = args.iter().cloned(); let params = P::from_wasm_values_exact(&mut values)?; - Ok((self.func)(ctx, params)?.into_wasm_values().collect()) + let mut values = (self.func)(ctx, params)?.into_wasm_values(); + for result in results { + *result = values.next().ok_or_else(|| crate::Error::other("not enough typed function results"))?; + } + if values.next().is_some() { + return Err(crate::Error::other("too many typed function results")); + } + Ok(()) } fn call_stack(&self, store: &mut Store, module_id: ModuleInstanceId, type_addr: TypeAddr) -> Result<()> { diff --git a/crates/tinywasm/src/func/mod.rs b/crates/tinywasm/src/func/mod.rs index 81a3ec8..a5abf13 100644 --- a/crates/tinywasm/src/func/mod.rs +++ b/crates/tinywasm/src/func/mod.rs @@ -1,7 +1,7 @@ use crate::interpreter::stack::{CallFrame, StackBase}; use crate::reference::StoreItem; use crate::{Error, FunctionInstance, InterpreterRuntime, Result, Store}; -use alloc::{format, vec::Vec}; +use alloc::format; use core::hint::cold_path; use tinywasm_types::{FuncAddr, FuncType, ModuleInstanceId}; @@ -16,6 +16,16 @@ pub use host::HostFunction; pub use resume::{ExecProgress, FuncExecution, FuncExecutionTyped}; pub use values::{FromWasmValues, IntoWasmValues, ToWasmType, ToWasmTypes}; +fn write_typed_params(params: &mut [WasmValue], mut values: impl Iterator) -> Result<()> { + for param in params { + *param = values.next().ok_or_else(|| Error::other("not enough typed function parameters"))?; + } + if values.next().is_some() { + return Err(Error::other("too many typed function parameters")); + } + Ok(()) +} + /// A function handle #[derive(Clone)] #[cfg_attr(feature = "debug", derive(core::fmt::Debug))] @@ -45,23 +55,24 @@ impl Function { Ok(store.state.get_func_type(self.addr())) } - /// Call a function (Invocation) + /// Calls the function and writes its results to `results`. /// - /// See + /// The initial result values are ignored. The result slice length must + /// match the function signature. #[inline] - pub fn call(&self, store: &mut Store, params: &[WasmValue]) -> Result> { - self.item.validate_store(store)?; - self.validate_params(store, params)?; + pub fn call(&self, store: &mut Store, params: &[WasmValue], results: &mut [WasmValue]) -> Result<()> { + self.validate_call(store, params, results.len())?; store.enter_execution()?; store.call_stack.clear(); store.value_stack.clear(); - let result = self.call_untyped(store, params, 0, StackBase::default()); + let result = self.call_untyped(store, params, results, 0, StackBase::default()); store.exit_execution(); result } - fn validate_params(&self, store: &Store, params: &[WasmValue]) -> Result<()> { + fn validate_call(&self, store: &Store, params: &[WasmValue], result_count: usize) -> Result<()> { + self.item.validate_store(store)?; let func_ty = store.state.get_func_type(self.addr()); if func_ty.params().len() != params.len() { cold_path(); @@ -84,11 +95,17 @@ impl Function { return Err(Error::Other(format!( "param type mismatch: expected {:?}, got {:?}", func_ty.params(), - params.iter().map(|v| v.ty()).collect::>() + params.iter().map(|v| v.ty()).collect::>() ))); #[cfg(not(feature = "debug"))] return Err(Error::Other("param type mismatch".into())); } + if func_ty.results().len() != result_count { + return Err(Error::Other(format!( + "result count mismatch: expected {}, got {result_count}", + func_ty.results().len() + ))); + } Ok(()) } @@ -97,15 +114,16 @@ impl Function { &self, store: &mut Store, params: &[WasmValue], + results: &mut [WasmValue], call_stack_base: u32, value_stack_base: StackBase, - ) -> Result> { + ) -> Result<()> { let instance = store.state.get_func(self.addr()); let type_addr = instance.type_addr; match &instance.kind { crate::store::FunctionKind::Host(host) => { let host = host.clone(); - host.call_values(store, self.module_id, type_addr, params) + host.call_values(store, self.module_id, type_addr, params, results) } crate::store::FunctionKind::Wasm(wasm) => { let wasm_params = wasm.func.params; @@ -123,7 +141,7 @@ impl Function { store.value_stack.truncate_to_base(value_stack_base); })?; let result_type = store.state.get_canonical_func_type(type_addr).clone(); - store.pop_stack_values(result_type.results()) + store.pop_stack_values(result_type.results(), results) } } } @@ -190,19 +208,24 @@ impl FunctionTyped { self.func.item.validate_store(store)?; let func = store.state.get_func(self.func.addr()).clone(); if matches!(&func.kind, crate::store::FunctionKind::Host(host) if host.typed_callback().is_none()) { - let params = params.into_wasm_values().collect::>(); - let result = self.func.call(store, ¶ms)?; - let mut values = result.into_iter(); - return R::from_wasm_values_exact(&mut values); + let ty = store.state.get_canonical_func_type(func.type_addr); + let (param_count, result_count) = (ty.params().len(), ty.results().len()); + store.with_scratch_values(param_count + result_count, |store, values| { + write_typed_params(&mut values[..param_count], params.into_wasm_values())?; + let (params, results) = values.split_at_mut(param_count); + self.func.call(store, params, results)?; + values.drain(..param_count); + R::from_wasm_values_exact(&mut values.drain(..)) + }) + } else { + store.enter_execution()?; + let result: Result = { + store.call_stack.clear(); + store.value_stack.clear(); + self.func.call_typed(store, &func, params.into_wasm_values(), 0, StackBase::default()) + }; + store.exit_execution(); + result } - - store.enter_execution()?; - let result: Result = { - store.call_stack.clear(); - store.value_stack.clear(); - self.func.call_typed(store, &func, params.into_wasm_values(), 0, StackBase::default()) - }; - store.exit_execution(); - result } } diff --git a/crates/tinywasm/src/func/resume.rs b/crates/tinywasm/src/func/resume.rs index ee9d2e1..6772eb8 100644 --- a/crates/tinywasm/src/func/resume.rs +++ b/crates/tinywasm/src/func/resume.rs @@ -1,4 +1,3 @@ -use alloc::vec::Vec; use tinywasm_types::{FuncAddr, TypeAddr}; use super::{FromWasmValues, Function, FunctionTyped, IntoWasmValues}; @@ -17,6 +16,12 @@ pub enum ExecProgress { /// Resumable execution for an untyped function call. #[cfg_attr(feature = "debug", derive(core::fmt::Debug))] pub struct FuncExecution<'store> { + execution: ExecutionCore<'store>, + results: &'store mut [WasmValue], +} + +#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] +struct ExecutionCore<'store> { store: &'store mut Store, state: FuncExecutionState, } @@ -30,14 +35,14 @@ enum FuncExecutionState { #[cfg_attr(feature = "debug", derive(core::fmt::Debug))] enum CallResult { Stack { type_addr: TypeAddr }, - Values(Vec), + Written, } /// Resumable execution for a typed function call. #[cfg_attr(feature = "debug", derive(core::fmt::Debug))] pub struct FuncExecutionTyped<'store, R> { - execution: FuncExecution<'store>, - marker: core::marker::PhantomData, + execution: ExecutionCore<'store>, + result: Option, } impl Function { @@ -50,18 +55,17 @@ impl Function { &self, store: &'store mut Store, params: &[WasmValue], + results: &'store mut [WasmValue], ) -> Result> { - self.item.validate_store(store)?; - self.validate_params(store, params)?; + self.validate_call(store, params, results.len())?; store.enter_execution()?; let result: Result = (|| { let func_instance = store.state.get_func(self.addr()).clone(); match &func_instance.kind { crate::store::FunctionKind::Host(host_func) => { - let result = - host_func.clone().call_values(store, self.module_id, func_instance.type_addr, params)?; - Ok(FuncExecutionState::Completed(Some(CallResult::Values(result)))) + host_func.clone().call_values(store, self.module_id, func_instance.type_addr, params, results)?; + Ok(FuncExecutionState::Completed(Some(CallResult::Written))) } crate::store::FunctionKind::Wasm(wasm_func) => { store.call_stack.clear(); @@ -78,11 +82,11 @@ impl Function { store.exit_execution(); let state = result?; - Ok(FuncExecution { store, state }) + Ok(FuncExecution { execution: ExecutionCore { store, state }, results }) } } -impl<'store> FuncExecution<'store> { +impl ExecutionCore<'_> { fn resume_raw( &mut self, run: impl FnOnce(&mut Store, CallFrame) -> Result, @@ -127,18 +131,20 @@ impl<'store> FuncExecution<'store> { } } } +} +impl FuncExecution<'_> { fn resume( &mut self, run: impl FnOnce(&mut Store, CallFrame) -> Result, - ) -> Result>> { - match self.resume_raw(run)? { + ) -> Result> { + match self.execution.resume_raw(run)? { ExecProgress::Completed(CallResult::Stack { type_addr }) => { - let ty = self.store.state.get_canonical_func_type(type_addr).clone(); - let values = self.store.pop_stack_values(ty.results())?; - Ok(ExecProgress::Completed(values)) + let ty = self.execution.store.state.get_canonical_func_type(type_addr).clone(); + self.execution.store.pop_stack_values(ty.results(), self.results)?; + Ok(ExecProgress::Completed(())) } - ExecProgress::Completed(CallResult::Values(values)) => Ok(ExecProgress::Completed(values)), + ExecProgress::Completed(CallResult::Written) => Ok(ExecProgress::Completed(())), ExecProgress::Suspended => Ok(ExecProgress::Suspended), } } @@ -149,13 +155,13 @@ impl<'store> FuncExecution<'store> { /// fuel before returning [`ExecProgress::Suspended`] (currently the chunk size is 128 instructions between fuel checks, but this may change in the future). /// /// Returns [`ExecProgress::Suspended`] when fuel is exhausted, or - /// [`ExecProgress::Completed`] with the final values once the invocation - /// returns. + /// [`ExecProgress::Completed`] after writing the final values to the result + /// slice supplied to [`Function::call_resumable`]. /// /// Reentrant calls made by host functions through [`crate::FuncContext::call`] are /// currently blocking. They do not suspend and later resume the host /// function in the middle of the nested call. - pub fn resume_with_fuel(&mut self, fuel: u32) -> Result>> { + pub fn resume_with_fuel(&mut self, fuel: u32) -> Result> { self.resume(|store, callframe| InterpreterRuntime::exec_with_fuel(store, callframe, fuel)) } @@ -168,10 +174,7 @@ impl<'store> FuncExecution<'store> { /// /// Reentrant calls made by host functions through [`crate::FuncContext::call`] /// are blocking and do not suspend in the middle of the host callback. - pub fn resume_with_time_budget( - &mut self, - time_budget: crate::std::time::Duration, - ) -> Result>> { + pub fn resume_with_time_budget(&mut self, time_budget: crate::std::time::Duration) -> Result> { self.resume(|store, callframe| InterpreterRuntime::exec_with_time_budget(store, callframe, time_budget)) } } @@ -203,9 +206,9 @@ impl FunctionTyped { self.func.item.validate_store(store)?; let func = store.state.get_func(self.func.addr()).clone(); if matches!(&func.kind, crate::store::FunctionKind::Host(host) if host.typed_callback().is_none()) { - let params = params.into_wasm_values().collect::>(); - let execution = self.func.call_resumable(store, ¶ms)?; - return Ok(FuncExecutionTyped { execution, marker: core::marker::PhantomData }); + let result = self.call(store, params)?; + let execution = ExecutionCore { store, state: FuncExecutionState::Completed(None) }; + return Ok(FuncExecutionTyped { execution, result: Some(result) }); } store.enter_execution()?; @@ -218,8 +221,8 @@ impl FunctionTyped { } })(); store.exit_execution(); - let execution = FuncExecution { store, state: result? }; - Ok(FuncExecutionTyped { execution, marker: core::marker::PhantomData }) + let execution = ExecutionCore { store, state: result? }; + Ok(FuncExecutionTyped { execution, result: None }) } } @@ -228,14 +231,14 @@ impl<'store, R: FromWasmValues> FuncExecutionTyped<'store, R> { &mut self, run: impl FnOnce(&mut Store, CallFrame) -> Result, ) -> Result> { + if let Some(result) = self.result.take() { + return Ok(ExecProgress::Completed(result)); + } match self.execution.resume_raw(run)? { ExecProgress::Completed(CallResult::Stack { type_addr }) => { Ok(ExecProgress::Completed(self.execution.store.take_typed_results(type_addr, StackBase::default())?)) } - ExecProgress::Completed(CallResult::Values(values)) => { - let mut values = values.into_iter(); - Ok(ExecProgress::Completed(R::from_wasm_values_exact(&mut values)?)) - } + ExecProgress::Completed(CallResult::Written) => unreachable!("untyped result in typed execution"), ExecProgress::Suspended => Ok(ExecProgress::Suspended), } } diff --git a/crates/tinywasm/src/func/values.rs b/crates/tinywasm/src/func/values.rs index 9e30b7f..19d0673 100644 --- a/crates/tinywasm/src/func/values.rs +++ b/crates/tinywasm/src/func/values.rs @@ -128,6 +128,7 @@ impl_scalar_wasm_traits!( i64 => WasmType::I64, f32 => WasmType::F32, f64 => WasmType::F64, + [u8; 16] => WasmType::V128, FuncRef => WasmType::Ref(tinywasm_types::RefType::FUNCREF.with_nullability(false)), AnyRef => WasmType::Ref(tinywasm_types::RefType::new_abstract(false, tinywasm_types::AbstractHeapType::Any)), EqRef => WasmType::Ref(tinywasm_types::RefType::new_abstract(false, tinywasm_types::AbstractHeapType::Eq)), diff --git a/crates/tinywasm/src/instance.rs b/crates/tinywasm/src/instance.rs index 7ad3cf5..4a92212 100644 --- a/crates/tinywasm/src/instance.rs +++ b/crates/tinywasm/src/instance.rs @@ -349,8 +349,9 @@ impl ModuleInstance { /// # let mut store = Store::default(); /// let instance = ModuleInstance::instantiate(&mut store, &module, None)?; /// let add = instance.func_untyped(&store, "add")?; - /// let result = add.call(&mut store, &[WasmValue::I32(20), WasmValue::I32(22)])?; - /// assert_eq!(result, vec![WasmValue::I32(42)]); + /// let mut results = [WasmValue::I32(0)]; + /// add.call(&mut store, &[WasmValue::I32(20), WasmValue::I32(22)], &mut results)?; + /// assert_eq!(results, [WasmValue::I32(42)]); /// # Ok(()) /// # } /// ``` @@ -614,7 +615,7 @@ impl ModuleInstance { /// See pub fn start(&self, store: &mut Store) -> Result> { match self.start_func(store)? { - Some(func) => func.call(store, &[]).map(|_| Some(())), + Some(func) => func.call(store, &[], &mut []).map(|_| Some(())), None => Ok(None), } } diff --git a/crates/tinywasm/src/interpreter/executor.rs b/crates/tinywasm/src/interpreter/executor.rs index c8aa188..5f218fd 100644 --- a/crates/tinywasm/src/interpreter/executor.rs +++ b/crates/tinywasm/src/interpreter/executor.rs @@ -1081,32 +1081,28 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { return Ok(false); } - let (param_count, base) = { - let param_types = self.store.state.get_canonical_func_type(type_addr).params(); - (param_types.len(), self.store.value_stack.base_before(param_types.iter().collect())) + let (param_count, result_count, base) = { + let ty = self.store.state.get_canonical_func_type(type_addr); + (ty.params().len(), ty.results().len(), self.store.value_stack.base_before(ty.params().iter().collect())) }; - let mut params = core::mem::take(&mut self.store.host_params); - debug_assert!(params.is_empty()); - if cold_err!(params.try_reserve_exact(param_count)).is_err() { - self.store.host_params = params; - return Err(Trap::OutOfMemory); - } - let host_values = self.store.stack_value_iter(type_addr, crate::store::FuncValueTypes::Params, base)?; - params.extend(host_values); - self.store.value_stack.truncate_to_base(base); - let result = host_func.call_values(self.store, self.module.id(), type_addr, ¶ms); - params.clear(); - self.store.host_params = params; - let res = match result { - Ok(values) => values, - Err(error) => return Err(Trap::HostFunction(Box::new(error))), - }; - - let push_result = self.store.push_wasm_values(res); - push_result.map_err(|error| match error { - Error::Trap(trap) => trap, - other => Trap::HostFunction(Box::new(other)), - })?; + let module_id = self.module.id(); + self.store + .with_scratch_values(param_count + result_count, |store, values| { + let host_values = store.stack_value_iter(type_addr, crate::store::FuncValueTypes::Params, base)?; + for (slot, value) in values[..param_count].iter_mut().zip(host_values) { + *slot = value; + } + store.value_stack.truncate_to_base(base); + let (params, results) = values.split_at_mut(param_count); + host_func + .call_values(store, module_id, type_addr, params, results) + .map_err(|error| Error::Trap(Trap::HostFunction(Box::new(error))))?; + store.push_wasm_values(results.iter().cloned()) + }) + .map_err(|error| match error { + Error::Trap(trap) => trap, + other => Trap::HostFunction(Box::new(other)), + })?; if TAIL { Ok(self.exec_return()) } else { @@ -1605,9 +1601,10 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let src = u32::stack_pop(&mut self.store.value_stack) as usize; let type_addr = self.module.resolve_type_addr(type_index); let storage = self.store.state.get_type(type_addr).as_array().expect("validated array type").field.storage; - self.store.state.check_gc_allocation(type_addr, len)?; let data_addr = self.module.resolve_data_addr(data_index); let data = self.store.state.data[data_addr as usize].data.as_deref().unwrap_or(&[]); + data_range(storage, data, src, len)?; + self.store.state.check_gc_allocation(type_addr, len)?; let values = decode_data(storage, data, src, len)?; self.push_gc_object(type_addr, values) } @@ -1617,9 +1614,9 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let len = u32::stack_pop(&mut self.store.value_stack) as usize; let src = u32::stack_pop(&mut self.store.value_stack) as usize; let type_addr = self.module.resolve_type_addr(type_index); - self.store.state.check_gc_allocation(type_addr, len)?; let elem_addr = self.module.resolve_elem_addr(elem_index); let items = self.store.state.elements[elem_addr as usize].items_range(src, len)?; + self.store.state.check_gc_allocation(type_addr, len)?; let mut values = Vec::new(); cold_err!(values.try_reserve_exact(len)).map_err(|_| Trap::OutOfMemory)?; values.extend(items.iter().copied().map(RuntimeValue::ValueRef)); diff --git a/crates/tinywasm/src/reference/store.rs b/crates/tinywasm/src/reference/store.rs index f2ee6bc..a2fbdf5 100644 --- a/crates/tinywasm/src/reference/store.rs +++ b/crates/tinywasm/src/reference/store.rs @@ -552,13 +552,6 @@ fn table_value_to_element( }) } -fn storage_type(storage: StorageType) -> WasmType { - match storage { - StorageType::I8 | StorageType::I16 => WasmType::I32, - StorageType::Value(ty) => ty, - } -} - impl I31Ref { /// Returns the signed i31 value. pub fn value(&self, store: &Store) -> Result { @@ -592,7 +585,7 @@ impl StructRef { .ok_or_else(|| Error::other("struct field out of bounds"))? .storage; let value = *store.state.gc.get(reference).unwrap().values.get(index).unwrap(); - value.into_wasm(store, storage_type(storage)) + value.into_wasm(store, storage.unpacked()) } /// Reads all fields in declaration order. @@ -606,7 +599,7 @@ impl StructRef { for index in 0..field_count { let storage = store.state.get_type(type_addr).as_struct().unwrap().fields[index].storage; let value = store.state.gc.get(reference).unwrap().values[index]; - values.push(value.into_wasm(store, storage_type(storage))?); + values.push(value.into_wasm(store, storage.unpacked())?); } Ok(values) } @@ -654,7 +647,7 @@ impl ArrayRef { let (reference, type_addr) = rooted_object(store, self, ReferentKind::Array)?; let storage = store.state.get_type(type_addr).as_array().unwrap().field.storage; let value = *store.state.gc.get(reference).unwrap().values.get(index).ok_or(Trap::ArrayOutOfBounds)?; - value.into_wasm(store, storage_type(storage)) + value.into_wasm(store, storage.unpacked()) } /// Writes one mutable array element after validating its canonical value type. @@ -681,7 +674,7 @@ fn set_gc_value( if !field.mutable { return Err(Error::other(format!("{field_name} is immutable"))); } - let expected = storage_type(field.storage); + let expected = field.storage.unpacked(); if !store.value_matches_type(&value, expected) { return Err(Error::other(format!("invalid {field_name} value type"))); } @@ -875,7 +868,7 @@ impl Global { pub fn get(&self, store: &mut Store) -> Result { self.0.validate_store(store)?; let ty = store.state.globals.ty(self.0.addr).ty; - let value = store.state.get_global_internal(self.0.addr); + let value = store.state.global_value(self.0.addr); value.into_wasm(store, ty) } @@ -887,7 +880,7 @@ impl Global { return Err(Error::other("invalid global value type")); } let value = value.to_runtime(store)?; - store.state.set_global_wasmvalue(self.0.addr, value) + store.state.set_global_value(self.0.addr, value) } } diff --git a/crates/tinywasm/src/reference/value.rs b/crates/tinywasm/src/reference/value.rs index bdf7921..4be02d4 100644 --- a/crates/tinywasm/src/reference/value.rs +++ b/crates/tinywasm/src/reference/value.rs @@ -161,7 +161,27 @@ value_conversions! { f32 => F32, as_f32; f64 => F64, as_f64; [u8; 16] => V128, as_v128; - RefValue => Ref, as_ref; +} + +impl WasmValue { + /// Returns the contained reference value. + pub const fn as_ref(&self) -> Option<&RefValue> { + if let Self::Ref(value) = self { Some(value) } else { None } + } +} + +impl From for WasmValue { + fn from(value: RefValue) -> Self { + Self::Ref(value) + } +} + +impl TryFrom for RefValue { + type Error = (); + + fn try_from(value: WasmValue) -> Result { + if let WasmValue::Ref(value) = value { Ok(value) } else { Err(()) } + } } macro_rules! ref_conversions { diff --git a/crates/tinywasm/src/store/gc/mod.rs b/crates/tinywasm/src/store/gc/mod.rs index adccd45..152d651 100644 --- a/crates/tinywasm/src/store/gc/mod.rs +++ b/crates/tinywasm/src/store/gc/mod.rs @@ -3,6 +3,7 @@ mod object; mod roots; use alloc::vec::Vec; +use core::ops::Range; use tinywasm_types::{StorageType, WasmType}; use crate::Trap; @@ -58,12 +59,12 @@ pub(crate) fn push_value( } /// Decodes numeric array elements from a data segment. -pub(crate) fn decode_data( +pub(crate) fn data_range( storage: StorageType, data: &[u8], src: usize, len: usize, -) -> Result, Trap> { +) -> Result<(Range, usize), Trap> { let width = match storage { StorageType::I8 => 1, StorageType::I16 => 2, @@ -76,9 +77,20 @@ pub(crate) fn decode_data( else { return Err(Trap::MemoryOutOfBounds { offset: src, len: len.saturating_mul(width), max: data.len() }); }; + Ok((src..end, width)) +} + +/// Decodes numeric array elements from a data segment. +pub(crate) fn decode_data( + storage: StorageType, + data: &[u8], + src: usize, + len: usize, +) -> Result, Trap> { + let (range, width) = data_range(storage, data, src, len)?; let mut values = Vec::new(); cold_err!(values.try_reserve_exact(len)).map_err(|_| Trap::OutOfMemory)?; - values.extend(data[src..end].chunks_exact(width).map(|bytes| match width { + values.extend(data[range].chunks_exact(width).map(|bytes| match width { 1 => RuntimeValue::Value32(u32::from(bytes[0])), 2 => RuntimeValue::Value32(u32::from(u16::from_le_bytes(bytes.try_into().unwrap()))), 4 => RuntimeValue::Value32(u32::from_le_bytes(bytes.try_into().unwrap())), diff --git a/crates/tinywasm/src/store/mod.rs b/crates/tinywasm/src/store/mod.rs index d3f1479..6f89fc8 100644 --- a/crates/tinywasm/src/store/mod.rs +++ b/crates/tinywasm/src/store/mod.rs @@ -21,7 +21,7 @@ mod tag; mod types; use const_expr::eval_const; -pub(crate) use gc::{GcObjectKind, decode_data, default_value, pop_value, push_value}; +pub(crate) use gc::{GcObjectKind, data_range, decode_data, default_value, pop_value, push_value}; pub(crate) use memory::{MemValue, MemoryInstance}; pub(crate) use state::State; pub(crate) use types::{canonicalize_ref_type, canonicalize_value_type}; @@ -129,7 +129,26 @@ pub struct Store { pub(crate) state: State, pub(crate) call_stack: CallStack, pub(crate) value_stack: ValueStack, - pub(crate) host_params: Vec, + value_scratch: ValueScratch, +} + +#[derive(Default)] +struct ValueScratch(Vec); + +impl ValueScratch { + fn take_resized(&mut self, len: usize) -> Result> { + self.0.clear(); + self.0.try_reserve(len).map_err(|_| Trap::OutOfMemory)?; + self.0.resize(len, WasmValue::I32(0)); + Ok(core::mem::take(&mut self.0)) + } + + fn restore(&mut self, mut values: Vec) { + values.clear(); + if values.capacity() > self.0.capacity() { + self.0 = values; + } + } } #[derive(Clone, Copy)] @@ -212,7 +231,7 @@ impl Store { state, call_stack: CallStack::new(engine.config()), value_stack: ValueStack::new(engine.config()), - host_params: Vec::new(), + value_scratch: ValueScratch::default(), engine, execution_fuel: 0, execution_active: false, @@ -228,6 +247,17 @@ impl Store { self.id } + pub(crate) fn with_scratch_values( + &mut self, + len: usize, + use_values: impl FnOnce(&mut Self, &mut Vec) -> Result, + ) -> Result { + let mut values = self.value_scratch.take_resized(len)?; + let result = use_values(self, &mut values); + self.value_scratch.restore(values); + result + } + pub(crate) fn root_reference(&mut self, value: ValueRef, kind: ReferentKind) -> Result { let token = match kind { ReferentKind::Struct | ReferentKind::Array | ReferentKind::Exception => { @@ -343,20 +373,21 @@ impl Store { Ok(()) } - pub(crate) fn pop_stack_values(&mut self, types: &[WasmType]) -> Result> { + pub(crate) fn pop_stack_values(&mut self, types: &[WasmType], results: &mut [WasmValue]) -> Result<()> { + if types.len() != results.len() { + return Err(Error::other("result buffer has the wrong length")); + } let base = self.value_stack.base_before(types.iter().collect()); - let values = (|| { + let result = (|| { let mut index = base; - let mut values = Vec::new(); - values.try_reserve_exact(types.len()).map_err(|_| Trap::OutOfMemory)?; - for &ty in types { + for (&ty, result) in types.iter().zip(results) { let value = self.stack_value(ty, &mut index); - values.push(value.into_wasm(self, ty)?); + *result = value.into_wasm(self, ty)?; } - Ok(values) + Ok(()) })(); self.value_stack.truncate_to_base(base); - values + result } /// Reclaims unreachable managed objects. diff --git a/crates/tinywasm/src/store/state.rs b/crates/tinywasm/src/store/state.rs index 60e27ee..11ea70d 100644 --- a/crates/tinywasm/src/store/state.rs +++ b/crates/tinywasm/src/store/state.rs @@ -359,8 +359,8 @@ impl State { Self::get_mut(&mut self.elements, addr, "element") } - /// Converts a global directly to its public value representation. - pub(crate) fn get_global_internal(&self, addr: GlobalAddr) -> RuntimeValue { + /// Returns a global in its internal value representation. + pub(crate) fn global_value(&self, addr: GlobalAddr) -> RuntimeValue { let ty = self.globals.ty(addr).ty; match ty { WasmType::I32 | WasmType::F32 => RuntimeValue::Value32(self.globals.get_32(addr)), @@ -370,8 +370,8 @@ impl State { } } - /// Validates and sets a global from its public value representation. - pub(crate) fn set_global_wasmvalue(&mut self, addr: GlobalAddr, value: RuntimeValue) -> Result<()> { + /// Sets a global from its internal value representation. + pub(crate) fn set_global_value(&mut self, addr: GlobalAddr, value: RuntimeValue) -> Result<()> { let ty = self.globals.ty(addr); if !ty.mutable { cold_path(); diff --git a/crates/tinywasm/tests/gc_refs.rs b/crates/tinywasm/tests/gc_refs.rs index f5159a6..9662bee 100644 --- a/crates/tinywasm/tests/gc_refs.rs +++ b/crates/tinywasm/tests/gc_refs.rs @@ -36,15 +36,19 @@ fn host_result_is_rooted_and_rejected_by_another_store() { let read = first.func_untyped(&first_store, "read").unwrap(); let churn = first.func_untyped(&first_store, "churn").unwrap(); - let value = new.call(&mut first_store, &[]).unwrap().pop().unwrap(); + let mut results = [WasmValue::Ref(RefValue::Null)]; + new.call(&mut first_store, &[], &mut results).unwrap(); + let [value] = results; assert!(matches!(value, WasmValue::Ref(RefValue::Any(_)))); - churn.call(&mut first_store, &[]).unwrap(); - assert_eq!(read.call(&mut first_store, std::slice::from_ref(&value)).unwrap(), [WasmValue::I32(42)]); + churn.call(&mut first_store, &[], &mut []).unwrap(); + let mut results = [WasmValue::I32(0)]; + read.call(&mut first_store, std::slice::from_ref(&value), &mut results).unwrap(); + assert_eq!(results, [WasmValue::I32(42)]); let mut second_store = store(); let second = ModuleInstance::instantiate(&mut second_store, &module, None).unwrap(); let other_read = second.func_untyped(&second_store, "read").unwrap(); - assert!(other_read.call(&mut second_store, &[value]).is_err()); + assert!(other_read.call(&mut second_store, &[value], &mut [WasmValue::I32(0)]).is_err()); } #[test] @@ -56,10 +60,14 @@ fn externalized_gc_result_is_rooted() { let read = instance.func_untyped(&store, "read-extern").unwrap(); let churn = instance.func_untyped(&store, "churn").unwrap(); - let value = new.call(&mut store, &[]).unwrap().pop().unwrap(); + let mut results = [WasmValue::Ref(RefValue::Null)]; + new.call(&mut store, &[], &mut results).unwrap(); + let [value] = results; assert!(matches!(value, WasmValue::Ref(RefValue::Extern(_)))); - churn.call(&mut store, &[]).unwrap(); - assert_eq!(read.call(&mut store, &[value]).unwrap(), [WasmValue::I32(42)]); + churn.call(&mut store, &[], &mut []).unwrap(); + let mut results = [WasmValue::I32(0)]; + read.call(&mut store, &[value], &mut results).unwrap(); + assert_eq!(results, [WasmValue::I32(42)]); } #[test] @@ -70,9 +78,9 @@ fn host_externref_does_not_alias_a_gc_object() { let new = instance.func_untyped(&store, "new").unwrap(); let read = instance.func_untyped(&store, "read-extern").unwrap(); - new.call(&mut store, &[]).unwrap(); + new.call(&mut store, &[], &mut [WasmValue::Ref(RefValue::Null)]).unwrap(); let host_ref = tinywasm::ExternRef::try_new(&mut store, 0).unwrap().into(); - assert!(read.call(&mut store, &[host_ref]).is_err()); + assert!(read.call(&mut store, &[host_ref], &mut [WasmValue::I32(0)]).is_err()); } #[test] @@ -84,15 +92,20 @@ fn resumable_gc_result_is_rooted() { let read = instance.func_untyped(&store, "read").unwrap(); let churn = instance.func_untyped(&store, "churn").unwrap(); - let mut execution = new.call_resumable(&mut store, &[]).unwrap(); - let value = match execution.resume_with_fuel(1000).unwrap() { - ExecProgress::Completed(mut values) => values.pop().unwrap(), - ExecProgress::Suspended => panic!("constructor unexpectedly suspended"), - }; - drop(execution); + let mut results = [WasmValue::Ref(RefValue::Null)]; + { + let mut execution = new.call_resumable(&mut store, &[], &mut results).unwrap(); + match execution.resume_with_fuel(1000).unwrap() { + ExecProgress::Completed(()) => {} + ExecProgress::Suspended => panic!("constructor unexpectedly suspended"), + } + } + let [value] = results; - churn.call(&mut store, &[]).unwrap(); - assert_eq!(read.call(&mut store, &[value]).unwrap(), [WasmValue::I32(42)]); + churn.call(&mut store, &[], &mut []).unwrap(); + let mut results = [WasmValue::I32(0)]; + read.call(&mut store, &[value], &mut results).unwrap(); + assert_eq!(results, [WasmValue::I32(42)]); } #[test] @@ -116,5 +129,7 @@ fn element_initializers_root_previous_gc_values() { let mut store = store(); let instance = ModuleInstance::instantiate(&mut store, &module, None).unwrap(); - assert_eq!(instance.func_untyped(&store, "first").unwrap().call(&mut store, &[]).unwrap(), [WasmValue::I32(1)]); + let mut results = [WasmValue::I32(0)]; + instance.func_untyped(&store, "first").unwrap().call(&mut store, &[], &mut results).unwrap(); + assert_eq!(results, [WasmValue::I32(1)]); } diff --git a/crates/tinywasm/tests/host_func_signature_check.rs b/crates/tinywasm/tests/host_func_signature_check.rs index b1c2d2b..484b3cc 100644 --- a/crates/tinywasm/tests/host_func_signature_check.rs +++ b/crates/tinywasm/tests/host_func_signature_check.rs @@ -1,6 +1,30 @@ use std::fmt::Write; use tinywasm::types::{FuncType, RefType, RefValue, WasmType, WasmValue}; -use tinywasm::{FuncContext, HostFunction, Imports, Module, ModuleInstance, Store}; +use tinywasm::{FuncContext, HostFunction, Imports, IntoWasmValues, Module, ModuleInstance, Store, ToWasmTypes}; + +struct TooFewParams; + +impl ToWasmTypes for TooFewParams { + const WASM_TYPES: Option<&'static [WasmType]> = Some(&[WasmType::I32, WasmType::I32]); +} + +impl IntoWasmValues for TooFewParams { + fn into_wasm_values(self) -> impl Iterator { + [WasmValue::I32(1)].into_iter() + } +} + +struct TooManyParams; + +impl ToWasmTypes for TooManyParams { + const WASM_TYPES: Option<&'static [WasmType]> = Some(&[WasmType::I32, WasmType::I32]); +} + +impl IntoWasmValues for TooManyParams { + fn into_wasm_values(self) -> impl Iterator { + [WasmValue::I32(1), WasmValue::I32(2), WasmValue::I32(3)].into_iter() + } +} const VAL_LISTS: &[&[WasmValue]] = &[ &[], @@ -31,7 +55,13 @@ fn test_return_invalid_type() -> Result<(), Box> { for returned_values in VAL_LISTS { let mut store = Store::default(); let mut imports = Imports::new(); - let hfn = HostFunction::from_untyped(&ty, |_: FuncContext<'_>, _| Ok(returned_values.to_vec())); + let hfn = HostFunction::from_untyped(&ty, move |_: FuncContext<'_>, _, results| { + if results.len() != returned_values.len() { + return Err(tinywasm::Error::Other("invalid fixture result count".into())); + } + results.clone_from_slice(returned_values); + Ok(()) + }); imports.define("host", "hfn", hfn); let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports)).unwrap(); @@ -39,7 +69,8 @@ fn test_return_invalid_type() -> Result<(), Box> { // Return-type mismatch is only observable at call time. let should_succeed = returned_values.len() == ty.results().len() && returned_values.iter().zip(ty.results()).all(|(value, ty)| value.matches_type(*ty)); - let call_res = caller.call(&mut store, &args); + let mut results = vec![WasmValue::I32(0); ty.results().len()]; + let call_res = caller.call(&mut store, &args, &mut results); assert_eq!(call_res.is_ok(), should_succeed); } } @@ -53,7 +84,8 @@ fn test_linking_invalid_untyped_func() -> Result<(), Box for (module, expected_func_ty, _) in &cases { for (_, ty, _) in &cases { let mut store = Store::default(); - let tried_fn = HostFunction::from_untyped(ty, |_: FuncContext<'_>, _| panic!("not intended to be called")); + let tried_fn = + HostFunction::from_untyped(ty, |_: FuncContext<'_>, _, _| panic!("not intended to be called")); let mut imports = Imports::new(); imports.define("host", "hfn", tried_fn); @@ -103,6 +135,78 @@ fn test_linking_invalid_typed_func() -> Result<(), Box> Ok(()) } +#[test] +fn typed_v128_values_roundtrip() -> Result<(), Box> { + let module = tinywasm::parse_bytes(&wat::parse_str( + r#" + (module + (import "host" "identity" (func $identity (param v128) (result v128))) + (func (export "identity") (param v128) (result v128) + local.get 0 + call $identity)) + "#, + )?)?; + let mut store = Store::default(); + let mut imports = Imports::new(); + imports.define("host", "identity", HostFunction::from(|_, value: [u8; 16]| Ok(value))); + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; + let identity = instance.func::<[u8; 16], [u8; 16]>(&store, "identity")?; + let value = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + + assert_eq!(identity.call(&mut store, value)?, value); + Ok(()) +} + +#[test] +fn untyped_host_callbacks_must_write_all_results() -> Result<(), Box> { + let ty = FuncType::new(&[], &[WasmType::I32]); + let host = HostFunction::from_untyped(&ty, |_, _, _| Ok(())); + let mut store = Store::default(); + let function = host.clone().instantiate(&mut store)?; + let mut results = [WasmValue::I32(7)]; + + assert!(function.call(&mut store, &[], &mut results).is_err()); + assert!(function.call_resumable(&mut store, &[], &mut results).is_err()); + + let module = tinywasm::parse_bytes(&wat::parse_str( + r#" + (module + (import "host" "value" (func $value (result i32))) + (func (export "value") (result i32) call $value)) + "#, + )?)?; + let mut imports = Imports::new(); + imports.define("host", "value", host); + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; + let function = instance.func_untyped(&store, "value")?; + + assert!(function.call(&mut store, &[], &mut results).is_err()); + Ok(()) +} + +#[test] +fn typed_untyped_bridge_rejects_inexact_parameter_iterators() -> Result<(), Box> { + let module = tinywasm::parse_bytes(&wat::parse_str( + r#" + (module + (import "host" "take" (func $take (param i32 i32))) + (export "take" (func $take))) + "#, + )?)?; + let mut store = Store::default(); + let mut imports = Imports::new(); + imports.define( + "host", + "take", + HostFunction::from_untyped(&FuncType::new(&[WasmType::I32, WasmType::I32], &[]), |_, _, _| Ok(())), + ); + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; + + assert!(instance.func::(&store, "take")?.call(&mut store, TooFewParams).is_err()); + assert!(instance.func::(&store, "take")?.call(&mut store, TooManyParams).is_err()); + Ok(()) +} + #[test] fn standalone_host_functions_reject_concrete_types() -> Result<(), Box> { let wasm = wat::parse_str( @@ -127,8 +231,8 @@ fn standalone_host_functions_reject_concrete_types() -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { let mut store = Store::default(); let ty = FuncType::new(&[WasmType::Ref(tinywasm::types::RefType::FUNCREF)], &[]); - let host = HostFunction::from_untyped(&ty, |_, _| Ok(Vec::new())).instantiate(&mut store)?; + let host = HostFunction::from_untyped(&ty, |_, _, _| Ok(())).instantiate(&mut store)?; let mut other_store = Store::default(); - let other = - HostFunction::from_untyped(&FuncType::new(&[], &[]), |_, _| Ok(Vec::new())).instantiate(&mut other_store)?; + let other = HostFunction::from_untyped(&FuncType::new(&[], &[]), |_, _, _| Ok(())).instantiate(&mut other_store)?; let invalid = WasmValue::Ref(RefValue::Func(other.as_func_ref(&other_store)?)); - assert!(host.call(&mut store, &[invalid]).is_err()); + assert!(host.call(&mut store, &[invalid], &mut []).is_err()); Ok(()) } diff --git a/crates/tinywasm/tests/internal_refs.rs b/crates/tinywasm/tests/internal_refs.rs index 6afddca..ac44e26 100644 --- a/crates/tinywasm/tests/internal_refs.rs +++ b/crates/tinywasm/tests/internal_refs.rs @@ -24,7 +24,9 @@ fn private_items_are_accessible_by_index() -> Result<(), Box Result<(), Box Result<() let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; let ExternItem::Func(func) = instance.extern_item("f")? else { panic!("expected function export") }; - assert_eq!(func.call(&mut store, &[])?, vec![]); + func.call(&mut store, &[], &mut [])?; Ok(()) } diff --git a/crates/tinywasm/tests/managed_exceptions.rs b/crates/tinywasm/tests/managed_exceptions.rs index 750a58b..bb75459 100644 --- a/crates/tinywasm/tests/managed_exceptions.rs +++ b/crates/tinywasm/tests/managed_exceptions.rs @@ -54,7 +54,7 @@ fn exception_accessors_validate_store_and_expose_payload() { let mut store = Store::default(); let instance = instantiate(&mut store); let throw = instance.func_untyped(&store, "throw-scalar").unwrap(); - let exception = exception(throw.call(&mut store, &[WasmValue::I32(17)]).unwrap_err()); + let exception = exception(throw.call(&mut store, &[WasmValue::I32(17)], &mut []).unwrap_err()); assert_eq!(exception.tag(&store).unwrap(), instance.tag("scalar-tag").unwrap()); assert_eq!(exception.field(&mut store, 0).unwrap(), WasmValue::I32(17)); @@ -71,12 +71,14 @@ fn exception_roots_keep_payload_graphs_live() { let instance = instantiate(&mut store); let throw = instance.func_untyped(&store, "throw-graph").unwrap(); let read = instance.func_untyped(&store, "read-node").unwrap(); - let exception = exception(throw.call(&mut store, &[]).unwrap_err()); + let exception = exception(throw.call(&mut store, &[], &mut []).unwrap_err()); store.gc().unwrap(); let payload = exception.field(&mut store, 0).unwrap(); assert!(matches!(payload, WasmValue::Ref(RefValue::Any(_)))); - assert_eq!(read.call(&mut store, &[payload]).unwrap(), [WasmValue::I32(42)]); + let mut results = [WasmValue::I32(0)]; + read.call(&mut store, &[payload], &mut results).unwrap(); + assert_eq!(results, [WasmValue::I32(42)]); } #[test] @@ -84,21 +86,21 @@ fn owned_exception_references_survive_collection_and_reject_another_store() { let mut store = Store::default(); let instance = instantiate(&mut store); let throw = instance.func_untyped(&store, "throw-scalar").unwrap(); - let first = exception(throw.call(&mut store, &[WasmValue::I32(1)]).unwrap_err()); + let first = exception(throw.call(&mut store, &[WasmValue::I32(1)], &mut []).unwrap_err()); store.gc().unwrap(); - let current = exception(throw.call(&mut store, &[WasmValue::I32(2)]).unwrap_err()); + let current = exception(throw.call(&mut store, &[WasmValue::I32(2)], &mut []).unwrap_err()); assert_eq!(current.field(&mut store, 0).unwrap(), WasmValue::I32(2)); assert_eq!(first.field(&mut store, 0).unwrap(), WasmValue::I32(1)); let rethrow = instance.func_untyped(&store, "rethrow").unwrap(); - let rethrown = exception(rethrow.call(&mut store, &[WasmValue::Ref(RefValue::Exn(first))]).unwrap_err()); + let rethrown = exception(rethrow.call(&mut store, &[WasmValue::Ref(RefValue::Exn(first))], &mut []).unwrap_err()); assert_eq!(rethrown.field(&mut store, 0).unwrap(), WasmValue::I32(1)); let mut other = Store::default(); let other_instance = instantiate(&mut other); let other_rethrow = other_instance.func_untyped(&other, "rethrow").unwrap(); assert_eq!( - other_rethrow.call(&mut other, &[WasmValue::Ref(RefValue::Exn(current))]).unwrap_err(), + other_rethrow.call(&mut other, &[WasmValue::Ref(RefValue::Exn(current))], &mut []).unwrap_err(), Error::Trap(Trap::InvalidStore) ); } @@ -130,11 +132,11 @@ fn dropped_exception_roots_release_counted_gc_bytes() { let catch = instance.func_untyped(&store, "catch-scalar").unwrap(); for value in 0..32 { - catch.call(&mut store, &[WasmValue::I32(value)]).unwrap(); + catch.call(&mut store, &[WasmValue::I32(value)], &mut []).unwrap(); } store.gc().unwrap(); usage.lock().unwrap().clear(); - catch.call(&mut store, &[WasmValue::I32(33)]).unwrap(); + catch.call(&mut store, &[WasmValue::I32(33)], &mut []).unwrap(); assert_eq!(usage.lock().unwrap()[0].0, 0); } diff --git a/crates/tinywasm/tests/reference_roots.rs b/crates/tinywasm/tests/reference_roots.rs index 3a4dd92..775047e 100644 --- a/crates/tinywasm/tests/reference_roots.rs +++ b/crates/tinywasm/tests/reference_roots.rs @@ -27,7 +27,7 @@ fn roots_and_function_references_reject_another_store() { assert!(matches!(root.key(&second), Err(tinywasm::Error::Trap(Trap::InvalidStore)))); assert!(matches!( - accepts_func_ref.call(&mut second, &[WasmValue::Ref(RefValue::Func(func_ref))]), + accepts_func_ref.call(&mut second, &[WasmValue::Ref(RefValue::Func(func_ref))], &mut []), Err(tinywasm::Error::Trap(Trap::InvalidStore)) )); } @@ -38,15 +38,18 @@ fn callback_results_and_captured_clones_remain_valid() { let captured = Arc::new(Mutex::new(None)); let callback_root = captured.clone(); let ty = FuncType::new(&[], &[WasmType::Ref(RefType::EXTERNREF)]); - let function = HostFunction::from_untyped(&ty, move |mut context, _| { + let function = HostFunction::from_untyped(&ty, move |mut context, _, results| { let root = ExternRef::try_new(context.store_mut(), 17)?; *callback_root.lock().unwrap() = Some(root.clone()); - Ok(vec![root.into()]) + results[0] = root.into(); + Ok(()) }) .instantiate(&mut store) .unwrap(); - let result = function.call(&mut store, &[]).unwrap().pop().unwrap(); + let mut results = [WasmValue::Ref(RefValue::Null)]; + function.call(&mut store, &[], &mut results).unwrap(); + let [result] = results; let WasmValue::Ref(RefValue::Extern(result)) = result else { panic!("expected externref") }; assert_eq!(result.key(&store), Ok(17)); assert_eq!(captured.lock().unwrap().as_ref().unwrap().key(&store), Ok(17)); @@ -72,11 +75,14 @@ fn guest_callback_arguments_are_rooted_before_entering_untyped_host_code() { imports.define( "host", "check", - HostFunction::from_untyped(&FuncType::new(&[WasmType::Ref(RefType::EXTERNREF)], &[]), |context, args| { - let WasmValue::Ref(RefValue::Extern(value)) = &args[0] else { panic!("expected externref") }; - assert_eq!(value.key(context.store()), Ok(23)); - Ok(Vec::new()) - }), + HostFunction::from_untyped( + &FuncType::new(&[WasmType::Ref(RefType::EXTERNREF)], &[]), + |context, args, _results| { + let WasmValue::Ref(RefValue::Extern(value)) = &args[0] else { panic!("expected externref") }; + assert_eq!(value.key(context.store()), Ok(23)); + Ok(()) + }, + ), ); let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports)).unwrap(); @@ -92,5 +98,7 @@ fn typed_option_reference_signatures_are_nullable() { .unwrap(); assert_eq!(function.ty(&store).unwrap().params(), &[WasmType::Ref(RefType::EXTERNREF)]); - assert_eq!(function.call(&mut store, &[WasmValue::Ref(RefValue::Null)]).unwrap(), [WasmValue::Ref(RefValue::Null)]); + let mut results = [WasmValue::Ref(RefValue::Null)]; + function.call(&mut store, &[WasmValue::Ref(RefValue::Null)], &mut results).unwrap(); + assert_eq!(results, [WasmValue::Ref(RefValue::Null)]); } diff --git a/crates/tinywasm/tests/resume_execution.rs b/crates/tinywasm/tests/resume_execution.rs index 2798e7b..f7dff4f 100644 --- a/crates/tinywasm/tests/resume_execution.rs +++ b/crates/tinywasm/tests/resume_execution.rs @@ -42,15 +42,17 @@ fn untyped_resume_supports_zero_fuel() -> Result<()> { let instance = ModuleInstance::instantiate(&mut store, &module, None)?; let func = instance.func_untyped(&store, "add")?; - let mut exec = func.call_resumable(&mut store, &[WasmValue::I32(20), WasmValue::I32(22)])?; - assert!(matches!(exec.resume_with_fuel(0)?, ExecProgress::Suspended)); - - match exec.resume_with_fuel(16)? { - ExecProgress::Completed(values) => { - assert_eq!(values, vec![WasmValue::I32(42)]) + let mut results = [WasmValue::I32(0)]; + { + let mut exec = func.call_resumable(&mut store, &[WasmValue::I32(20), WasmValue::I32(22)], &mut results)?; + assert!(matches!(exec.resume_with_fuel(0)?, ExecProgress::Suspended)); + + match exec.resume_with_fuel(16)? { + ExecProgress::Completed(()) => {} + ExecProgress::Suspended => panic!("expected completion"), } - ExecProgress::Suspended => panic!("expected completion"), } + assert_eq!(results, [WasmValue::I32(42)]); Ok(()) } diff --git a/crates/tinywasm/tests/store_ownership.rs b/crates/tinywasm/tests/store_ownership.rs index b14f5ef..72c9478 100644 --- a/crates/tinywasm/tests/store_ownership.rs +++ b/crates/tinywasm/tests/store_ownership.rs @@ -21,7 +21,7 @@ fn func_handle_rejects_wrong_store() -> Result<(), Box> let func = instance.func_untyped(&owner_store, "add")?; let mut other_store = Store::default(); - let err = func.call(&mut other_store, &[1.into(), 2.into()]).unwrap_err(); + let err = func.call(&mut other_store, &[1.into(), 2.into()], &mut [0.into()]).unwrap_err(); assert!(err.to_string().contains("invalid store")); Ok(()) diff --git a/crates/tinywasm/tests/typed_globals.rs b/crates/tinywasm/tests/typed_globals.rs index 6e83045..34f402d 100644 --- a/crates/tinywasm/tests/typed_globals.rs +++ b/crates/tinywasm/tests/typed_globals.rs @@ -60,7 +60,9 @@ fn globals_use_typed_instructions_and_roundtrip_values() -> Result<(), Box(&store, "add-i32")?.call(&mut store, 2)?, 13); assert_eq!(instance.func::(&store, "add-i64")?.call(&mut store, 2)?, 15); diff --git a/crates/types/src/types.rs b/crates/types/src/types.rs index 0801849..51558e4 100644 --- a/crates/types/src/types.rs +++ b/crates/types/src/types.rs @@ -156,3 +156,13 @@ pub enum StorageType { I16, Value(WasmType), } + +impl StorageType { + /// Returns the unpacked WebAssembly value type used on the stack. + pub const fn unpacked(self) -> WasmType { + match self { + Self::I8 | Self::I16 => WasmType::I32, + Self::Value(ty) => ty, + } + } +} From 51cb7d8f967d1e4c939c8a6da919e148214f2c0e Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 24 Aug 2026 15:40:01 +0200 Subject: [PATCH 3/3] docs: improve rusdoc / readme / changelog Signed-off-by: Henry --- ARCHITECTURE.md | 65 -------------------------- CHANGELOG.md | 48 +++++++++---------- README.md | 46 ++++++++++-------- crates/tinywasm/src/error.rs | 12 +++-- crates/tinywasm/src/func/mod.rs | 4 +- crates/tinywasm/src/lib.rs | 2 +- crates/tinywasm/src/reference/store.rs | 8 ++++ crates/tinywasm/src/reference/value.rs | 2 + crates/tinywasm/src/store/mod.rs | 41 +++++++++------- crates/types/src/instructions.rs | 2 - crates/types/src/lib.rs | 19 ++++++-- crates/types/src/reference.rs | 4 ++ crates/types/src/types.rs | 14 ++++++ crates/types/src/value.rs | 2 + 14 files changed, 128 insertions(+), 141 deletions(-) delete mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 62fa503..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,65 +0,0 @@ -# TinyWasm Architecture - -TinyWasm follows the general runtime model described in the [WebAssembly specification](https://webassembly.github.io/spec/core/exec/runtime.html). It is a stack-based interpreter with a compact internal bytecode, width-specific value stacks, and a contiguous `Vec`-backed linear memory. - -## Execution Pipeline - -TinyWasm does not execute WebAssembly instructions directly. Parsing lowers them into an internal bytecode designed to make execution simpler and cheaper: - -- structured control flow (`block`, `loop`, `if`, and `br*`) becomes jump-oriented instructions such as `Jump`, `JumpIfZero`, `BranchTable*`, `DropKeep*`, and `Return` -- operand widths are encoded in instruction variants, and branch stack reshaping is explicit -- instructions retain compact module-local indexes, which each instance maps to Store-wide runtime addresses -- when enabled, the optimizer applies local rewrites, including superinstruction fusion, specialized calls and returns, and redundant-instruction removal -- modules can be serialized as `.twasm` archives containing this lowered representation -- execution uses a single iterative dispatch loop over the resulting instruction stream - -## Value Stacks - -WebAssembly combines an operand stack with function-scoped locals. TinyWasm stores both in the same width-specific physical stacks: - -- `stack_32` for `i32`, `f32`, and reference values, including GC and exception references -- `stack_64` for `i64` and `f64` -- `stack_128` for `v128` - -The interpreter does not maintain a runtime type stack or tag individual stack slots. Lowered instructions encode the physical lane they operate on, while WebAssembly validation guarantees type correctness. Splitting values by width lets each value use its natural storage size, reducing stack memory and the data moved by common operations. - -Locals are stored directly in these stacks. Each `CallFrame` records a base for every lane, and lowered local instructions index from those bases. The value stacks and call stack can use either a fixed capacity or dynamic initial and maximum sizes. Dynamic stacks keep the initial allocation small, grow when needed, and retain a hard limit. - -## Interpreter Optimization - -Instruction dispatch is one of the interpreter's main costs. TinyWasm reduces it through superinstructions and by shaping the large Rust dispatch match based on benchmarks and assembly inspection. Most simple arithmetic remains directly in the interpreter loop. Small, frequently used stack, value, and global operations use `#[inline]` or `#[inline(always)]` where measurements show a benefit, while unlikely error paths use `core::hint::cold_path()`. - -Superinstructions also reduce value-stack traffic. They can read locals, globals, and constants directly, perform an operation, and write `set` or `tee` destinations without materializing intermediate operand-stack values. Examples include: - -- fused binary operations such as `BinOpLocalLocal*`, `BinOpLocalConst*`, and `BinOpStackGlobal*` -- fused conditional branches such as `JumpCmpLocalConst*`, `JumpCmpLocalLocal*`, and `JumpCmpStackConst*` - -The default runtime remains safe Rust throughout rather than relying on unchecked operations. - -## SIMD - -SIMD instructions have a portable safe-Rust implementation built from fixed-size arrays and lane operations, relying on the compiler to auto-vectorize where possible. Generated code is inspected with `cargo asm`, and benchmarks determine where architecture-specific alternatives are worthwhile. WebAssembly targets use native SIMD intrinsics where available, while the optional `simd-x86` feature provides selected x86 implementations for operations where the generic code produces worse results. - -## Linear Memory - -Linear memory is a contiguous `Vec` allocation owned by a `MemoryInstance`. The interpreter accesses it through the internal `MemoryStorage` type, a small concrete boundary that keeps the `Vec` representation out of the executor so an mmap-backed storage can be substituted later without touching load and store paths. - -Fixed-width loads and stores use a single const-generic `read_fixed::` / `write_fixed::` pair rather than per-width vtable methods. Scalar operations reduce to an effective-address computation, a bounds check, a slice access, and a `from_le_bytes` / `to_le_bytes` conversion, with out-of-bounds construction kept on cold paths. Bulk operations such as `fill` and `copy_within` map directly to native slice methods. - -Memory growth keeps the Wasm page count and limits on `MemoryInstance`. Before memory or table backing storage is allocated or resized, the configured `ResourceLimiter` is consulted so a host can bound guest resource consumption. The limiter is shared across the stores created from one `Engine` and lives behind an `Arc`. - -For conventional operating systems, a future mmap-backed storage could reserve virtual address space and use guard pages to move more bounds enforcement to the operating system, reducing explicit checks in linear-memory hot paths. This is the same broad approach described in [Wasmtime's linear-memory architecture](https://docs.wasmtime.dev/contributing-architecture.html#linear-memory), where virtual-memory reservations and guard regions eliminate or deduplicate explicit bounds checks. - -## Future Experiments - -Future work may explore additional dispatch and code-generation strategies, including Rust's experimental `loop_match` state-machine work, a tail-call-based interpreter once Rust's explicit tail-call support matures, more aggressive superinstruction fusion, top-of-stack register allocation, or optional JIT compilation. - -## Important Modules - -- [visit.rs](./crates/parser/src/visit.rs) - function-body operator lowering -- [optimize.rs](./crates/parser/src/optimize.rs) - peephole optimizer and superinstruction fusion -- [parallel.rs](./crates/parser/src/parallel.rs) - parallel function parsing -- [instructions.rs](./crates/types/src/instructions.rs) - internal instruction set -- [value_stack.rs](./crates/tinywasm/src/interpreter/stack/value_stack.rs) - width-specific stacks -- [call_stack.rs](./crates/tinywasm/src/interpreter/stack/call_stack.rs) - call frame stack -- [memory/mod.rs](./crates/tinywasm/src/store/memory/mod.rs) - linear memory storage diff --git a/CHANGELOG.md b/CHANGELOG.md index 6509552..bad8caf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,44 +9,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Support for the function-references, garbage-collection, exception-handling, and compact-imports proposals. -- Store-aware owned references, explicit GC collection, and typed host access to GC objects and exceptions. -- `WasmValue::ty`, `WasmValue::matches_type`, and `ValueLane` for inspecting value types and storage lanes. -- `ResourceLimiter` for limiting guest memory, table, and logical GC heap growth. -- A default `validate` Cargo feature that can be disabled when parsing trusted modules. -- Optional parse-time operand deduplication for smaller `.twasm` archives. +- Support for the typed function references, garbage collection, exception handling, and compact import section proposals. +- `ResourceLimiter` callbacks for memory and table allocation or growth +- A default `validate` Cargo feature & parser option to skip wasm validation +- Optional parse-time operand deduplication to reduce `.twasm` archive size ### Changed -- Typed functions support tuples up to arity 20 and `[u8; 16]` values for `v128`. -- Module types use one recursive type space, with runtime function types available through `Function::ty(&Store)`. -- Linear memory uses contiguous `Vec`-backed storage. -- The minimum supported Rust version is 1.98. +- Typed functions now support tuples up to arity 20 and `[u8; 16]` values for `v128`. +- Module types now use one recursive type space. Runtime function types are available through `Function::ty(&Store)`. +- Linear memory now uses contiguous `Vec`-backed storage. +- The minimum supported Rust version increased from 1.95 to 1.98. +- The internal instruction represetaions size was reduced from 16 to 8 bytes. ### Fixed -- Directly defined imports now reject handles from a different `Store`. +- Instantiation now rejects directly defined imports that contain handles from another `Store`. - Tail calls to host functions now return directly to the caller frame. -- Fixed Memory64 bulk-memory operations and optimized stores using the wrong value-stack lane. -- Fixed `memory.init` bounds checks and operand lowering. -- Fixed Memory64 default limits and host-size handling, including 32-bit targets. +- Memory64 bulk-memory operations and optimized stores now use the correct value-stack lanes. +- `memory.init` now performs correct bounds checks and operand lowering. +- Memory64 now uses the correct default limits and host-size conversions, including on 32-bit targets. ### Breaking Changes -- Dynamic `Function::call` and `Function::call_resumable` calls write to caller-provided result slices. Untyped host callbacks also receive a result slice and return `Result<()>`. -- `HostFunction` definitions are reusable, require `Send + Sync`, and no longer take a `Store`. -- Module instantiation borrows `Imports`, allowing imports to be shared across stores. +- `Function::call` and `Function::call_resumable` now write to caller-provided result slices. Untyped host callbacks also receive a result slice and return `Result<()>`. +- Creating a `HostFunction` no longer requires a `Store`. Definitions require `Send + Sync` and can be reused across stores. Module instantiation now borrows `Imports` so the same imports can also be reused. - `HostFunction::ty` and `WasmFunction::ty` were removed. Use `Function::ty(&Store)` for runtime types. -- Function and managed reference handles are Store-aware. Managed references keep their referents live, and `WasmValue` is no longer `Copy`. -- Nullable typed reference parameters and results use `Option`. Bare typed references are non-null. -- Host references are created with `ExternRef::try_new` and `I31Ref::try_new`. Reference and GC object access is provided by methods on each reference type. -- Fallible `Memory`, `Table`, `Global`, and `Tag` constructors are named `try_new`. -- `Store::id` returns `u32`, and `ModuleInstanceAddr` is renamed to `ModuleInstanceId`. -- Table element types use `RefType`, and module table definitions use `TableDefinition { ty, init }`. -- `Parser::new` takes `ParserOptions`. Use `Parser::default()` for default settings. `Parser::with_options` and local-memory allocation analysis were removed. -- Pluggable memory backends and `Config::with_trap_on_oom` were removed. Linear memory is always `Vec`-backed, and allocation limits use `ResourceLimiter`. -- `Table::grow` returns `Result>`, matching `Memory::grow`. +- Function and managed reference handles are tied to their originating `Store`. Managed references keep their referents live, so `WasmValue` is no longer `Copy`. Nullable typed references use `Option`, while bare typed references are non-null. +- The `Memory`, `Table`, `Global`, and `Tag` constructors were renamed from `new` to `try_new`. +- `Store::id` now returns `u32`, and `ModuleInstanceAddr` was renamed to `ModuleInstanceId`. +- Element types now use `RefType`, and module definitions use `TableDefinition { ty, init }`. `Table::grow` now returns `Result>`, matching `Memory::grow`. +- `Parser::new` now takes `ParserOptions`. Use `Parser::default()` for default settings. `Parser::with_options` was removed. +- Pluggable memory backends and `Config::with_trap_on_oom` were removed for performance reasons. Linear memory is always `Vec`-backed. Use `ResourceLimiter` to allow, reject, or trap memory allocation and growth requests. - `WasmTupleChain` was removed. Use direct tuples up to arity 20 or untyped functions for larger signatures. +- The `.twasm` format changed. Regenerate archives created by earlier TinyWasm versions. ## [0.10.0] - 2026-07-24 diff --git a/README.md b/README.md index 956d160..70578dd 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,17 @@ # `tinywasm` -[![docs.rs](https://img.shields.io/docsrs/tinywasm?logo=rust&style=flat-square)](https://docs.rs/tinywasm) [![Crates.io](https://img.shields.io/crates/v/tinywasm.svg?logo=rust&style=flat-square)](https://crates.io/crates/tinywasm) [![Crates.io](https://img.shields.io/crates/l/tinywasm.svg?style=flat-square)](./LICENSE-APACHE) +[![Documentation](https://img.shields.io/badge/docs-latest-blue?style=flat-square)](https://docs.rs/tinywasm/latest/tinywasm/) [![Build](https://img.shields.io/github/actions/workflow/status/explodingcamera/tinywasm/test.yaml?branch=next&style=flat-square&label=build)](https://github.com/explodingcamera/tinywasm/actions/workflows/test.yaml?query=branch%3Anext) [![Crates.io](https://img.shields.io/crates/v/tinywasm.svg?logo=rust&style=flat-square)](https://crates.io/crates/tinywasm) [![Crates.io](https://img.shields.io/crates/l/tinywasm.svg?style=flat-square)](./LICENSE-APACHE) ## Why `tinywasm`? -- **Tiny**: Small by design, while still passing the full WebAssembly 3.0 core testsuite. -- **Portable**: Runs anywhere Rust can target, supports `no_std`, has minimal dependencies, and can itself compile to WebAssembly. -- **Safe**: Written in safe Rust, with optional `unsafe` limited to the `simd-x86` feature. Its sandbox is designed to prevent untrusted Wasm from accessing host memory or escaping the runtime. +- **Tiny**: Small by design, while still passing the full WebAssembly 3.0 core test suite. +- **Portable**: Runs anywhere Rust can target, supports `no_std`, has minimal dependencies[^dependencies], and can itself compile to WebAssembly. +- **Safe by default**: Written entirely in safe Rust[^unsafe]. + +[^dependencies]: The two main external components are [`wasmparser`](https://crates.io/crates/wasmparser) for WebAssembly parsing and validation, and [`postcard`](https://crates.io/crates/postcard) for `.twasm` archives. + +[^unsafe]: The optional `simd-x86` feature is the only exception. It uses `unsafe` internally for selected x86 SIMD intrinsics. ## Installation @@ -42,38 +46,40 @@ assert_eq!(result, 3); See the [examples](./examples) directory and [documentation](https://docs.rs/tinywasm) for more information. -## Precompiled Modules - -TinyWasm modules can be compiled to the internal `twasm` bytecode format, which stores the optimized instruction representation for faster loading and reuse. - ## Cargo Features - **`std`:** Enables `std` and parsing from files and streams. Enabled by default. - **`log`:** Enables integration with the `log` crate. Enabled by default. - **`parser`:** Enables `tinywasm-parser` and top-level parse helpers. Enabled by default. -- **`validate`:** Enables WebAssembly validation while parsing. Enabled by default and configurable through `ParserOptions`. +- **`validate`:** Enables WebAssembly validation while parsing. Enabled by default and configurable through [`ParserOptions`](https://docs.rs/tinywasm/latest/tinywasm/parser/struct.ParserOptions.html). - **`archive`:** Enables serialization and deserialization of the internal `twasm` format. Enabled by default. -- **`canonicalize-nans`:** Canonicalizes NaN values. Enabled by default. +- **`canonicalize-nans`:** Uses a [canonical NaN](https://en.wikipedia.org/wiki/NaN#Canonical_NaN) for normalized NaN results. Enabled by default. - **`debug`:** Derives `Debug` for runtime types. Enabled by default. - **`parallel-parser`:** Parallelizes function parsing when `std` is enabled. Enabled by default. - **`guest-debug`:** Exposes module-internal by-index inspection APIs (`*_by_index`). - **`simd-x86`:** Enables x86-specific SIMD intrinsics and uses `unsafe` internally. -With default features disabled, `tinywasm` depends only on `core`, `alloc`, and `libm`[^libm], making it usable in `no_std + alloc` environments. +With default features disabled, `tinywasm` depends only on `core`, `alloc`, and `libm`, making it usable in `no_std + alloc` environments. + +Use [`Engine`](https://docs.rs/tinywasm/latest/tinywasm/engine/struct.Engine.html) and [`engine::Config`](https://docs.rs/tinywasm/latest/tinywasm/engine/struct.Config.html) for non-default fuel accounting, stack sizing, or GC collection thresholds. A configured `ResourceLimiter` can allow, reject, or trap memory and table allocation or growth requests, and GC object allocations. + +## Precompiled Modules + +TinyWasm can serialize a parsed module to its version-specific `.twasm` format. Loading an archive skips WebAssembly parsing, validation, and optimization. -Use `Engine` and `engine::Config` when you need non-default runtime settings such as fuel accounting, stack sizing, or the GC collection threshold. A `ResourceLimiter` attached to the engine's config bounds guest memory, table, and GC heap growth and can trap rejected requests. +Applications that only load `.twasm` can remove the parser and validator from their binary by only enabling the `archive` feature. Depending on the target and release profile, this can produce binaries smaller than 300 KB. -[^libm]: [rust-lang/rust#137578](https://github.com/rust-lang/rust/issues/137578) — tracking issue for floating-point math support in `no_std`. +## Untrusted Input -## Safety +WebAssembly [validation](https://webassembly.github.io/spec/core/valid/index.html) is enabled by default through the `validate` feature. Keep this feature enabled and leave `ParserOptions::validation` enabled for modules from untrusted sources. Without validation, parsing can produce modules that violate runtime assumptions and may panic during instantiation or execution. -TinyWasm only uses safe Rust by default. The optional `simd-x86` feature enables x86-specific SIMD intrinsics and uses `unsafe` internally. WebAssembly input is validated by default through the `validate` feature. Disabling validation should not let Wasm access host memory or escape the sandbox, but malformed input may panic or otherwise crash the process, so only disable it for trusted input. +Validation does not limit parsing or execution resources. Hosts that run untrusted code should also set input limits, configure stack and `ResourceLimiter` limits, and use fuel- or time-budgeted execution as needed. -The internal `twasm` bytecode format is not currently validated as an untrusted input format. Malformed `twasm` may panic, but should not compromise memory safety or allow sandbox escape. Only run trusted `twasm` bytecode, or generate it through TinyWasm from Wasm input. +Loading `.twasm` checks the archive header and encoding but does not run WebAssembly validation or verify TinyWasm's runtime invariants. Load archives only from trusted sources. For untrusted input, parse a WebAssembly binary with validation enabled. -## Supported Proposals +## WebAssembly Proposal Support -TinyWasm targets non-JavaScript core proposals through [phase 3](https://github.com/WebAssembly/proposals). JavaScript integrations and optional embedding or tooling APIs are not included here. +TinyWasm generally implements non-JavaScript core proposals at [phase 4](https://github.com/WebAssembly/proposals#phase-4---standardize-the-feature-wg) or later, with some proposals implemented earlier. The table shows current support and known exceptions. | Proposal | Status | `tinywasm` Version | | ---------------------------------------------------------------------------------------------------------------- | ------ | ------------------ | @@ -106,11 +112,13 @@ TinyWasm targets non-JavaScript core proposals through [phase 3](https://github. ## See Also -If you need a more mature, production-tested, or performance-focused WebAssembly runtime today, consider one of these projects: +If you're looking for a WebAssembly runtime with JIT compilation, better performance or other advanced features, check out these other runtimes: - [wasmi](https://github.com/wasmi-labs/wasmi) - efficient and versatile WebAssembly interpreter for embedded systems - [wasm3](https://github.com/wasm3/wasm3) - a fast WebAssembly interpreter written in C - [wazero](https://wazero.io/) - a zero-dependency WebAssembly interpreter written in Go +- [wasmer](https://wasmer.io/) - a fast and secure WebAssembly runtime written in Rust +- [wasmtime](https://wasmtime.dev/) - a fast and secure WebAssembly runtime written in Rust ## License diff --git a/crates/tinywasm/src/error.rs b/crates/tinywasm/src/error.rs index a9c05e6..77856af 100644 --- a/crates/tinywasm/src/error.rs +++ b/crates/tinywasm/src/error.rs @@ -107,9 +107,11 @@ impl Error { } } -/// A WebAssembly trap +/// An execution or runtime trap. /// -/// See +/// This includes WebAssembly traps and TinyWasm runtime failures. +/// +/// See #[non_exhaustive] #[cfg_attr(feature = "debug", derive(Debug))] pub enum Trap { @@ -125,7 +127,7 @@ pub enum Trap { offset: usize, /// The size of the access len: usize, - /// The maximum size of the memory + /// The current memory length in bytes. max: usize, }, @@ -135,7 +137,7 @@ pub enum Trap { offset: usize, /// The size of the access len: usize, - /// The maximum size of the memory + /// The current table length in elements. max: usize, }, @@ -163,7 +165,7 @@ pub enum Trap { /// Value stack overflow ValueStackOverflow, - /// The runtime could not allocate memory for a stack or linear memory operation. + /// An allocation or requested resource size could not be satisfied. OutOfMemory, /// An undefined element was encountered diff --git a/crates/tinywasm/src/func/mod.rs b/crates/tinywasm/src/func/mod.rs index a5abf13..c29b3db 100644 --- a/crates/tinywasm/src/func/mod.rs +++ b/crates/tinywasm/src/func/mod.rs @@ -26,7 +26,9 @@ fn write_typed_params(params: &mut [WasmValue], mut values: impl Iterator #[derive(Clone)] #[cfg_attr(feature = "debug", derive(core::fmt::Debug))] pub struct Function { diff --git a/crates/tinywasm/src/lib.rs b/crates/tinywasm/src/lib.rs index 8509be9..15ccd0e 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -68,7 +68,7 @@ //! - **`parser`:** Enables `tinywasm-parser` and top-level parse helpers. Enabled by default. //! - **`validate`:** Enables WebAssembly validation while parsing. Enabled by default and configurable through `ParserOptions`. //! - **`archive`:** Enables serialization and deserialization of the internal `twasm` format. Enabled by default. -//! - **`canonicalize-nans`:** Canonicalizes NaN values. Enabled by default. +//! - **`canonicalize-nans`:** Uses a [canonical NaN](https://en.wikipedia.org/wiki/NaN#Canonical_NaN) for normalized NaN results. Enabled by default. //! - **`debug`:** Derives `Debug` for runtime types. Enabled by default. //! - **`parallel-parser`:** Parallelizes function parsing when `std` is enabled. Enabled by default. //! - **`guest-debug`:** Exposes module-internal by-index inspection APIs (`*_by_index`). diff --git a/crates/tinywasm/src/reference/store.rs b/crates/tinywasm/src/reference/store.rs index a2fbdf5..d0eba6b 100644 --- a/crates/tinywasm/src/reference/store.rs +++ b/crates/tinywasm/src/reference/store.rs @@ -39,6 +39,8 @@ impl StoreItem { /// A memory instance in a store. /// +/// See +/// /// ## Example /// ```rust /// # fn main() -> tinywasm::Result<()> { @@ -61,16 +63,22 @@ impl StoreItem { pub struct Memory(pub(crate) StoreItem); /// A table instance in a store. +/// +/// See #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "debug", derive(Debug))] pub struct Table(pub(crate) StoreItem); /// A global instance in a store. +/// +/// See #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "debug", derive(Debug))] pub struct Global(pub(crate) StoreItem); /// A tag instance in a store. +/// +/// See #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "debug", derive(Debug))] pub struct Tag(pub(crate) StoreItem); diff --git a/crates/tinywasm/src/reference/value.rs b/crates/tinywasm/src/reference/value.rs index 4be02d4..7ef3dea 100644 --- a/crates/tinywasm/src/reference/value.rs +++ b/crates/tinywasm/src/reference/value.rs @@ -6,6 +6,8 @@ use crate::interpreter::{RuntimeValue, Value128}; use crate::{AnyRef, ArrayRef, EqRef, ExnRef, ExternRef, FuncRef, I31Ref, RefValue, Result, Store, StructRef}; /// A host-facing WebAssembly value. +/// +/// See #[derive(Clone, PartialEq)] pub enum WasmValue { /// A 32-bit integer. diff --git a/crates/tinywasm/src/store/mod.rs b/crates/tinywasm/src/store/mod.rs index 6f89fc8..91895b4 100644 --- a/crates/tinywasm/src/store/mod.rs +++ b/crates/tinywasm/src/store/mod.rs @@ -27,13 +27,18 @@ pub(crate) use state::State; pub(crate) use types::{canonicalize_ref_type, canonicalize_value_type}; pub(crate) use {data::*, element::*, function::*, global::*, table::*, tag::*}; -/// Controls resource usage by WebAssembly instances. +/// Controls selected allocation requests from WebAssembly instances. /// /// Configure a limiter with -/// [`Config::with_resource_limiter`](crate::engine::Config::with_resource_limiter). It currently -/// controls guest linear-memory, table, and GC heap growth. It does not account for stacks, runtime +/// [`Config::with_resource_limiter`](crate::engine::Config::with_resource_limiter). The callbacks +/// cover linear memory, tables, and the logical GC heap. They do not account for stacks, runtime /// metadata, temporary buffers, backing-capacity overhead, or other host allocations. /// +/// `Ok(true)` allows an allocation attempt, `Ok(false)` rejects it, and `Err` returns the supplied +/// trap. Allowing a request does not guarantee that the backing allocation will succeed. Rejected +/// growth uses the operation's normal failed-growth result. Rejected initial allocation and GC +/// allocation produce [`Trap::OutOfMemory`]. +/// /// # Example /// ```rust /// use std::sync::Arc; @@ -61,12 +66,12 @@ pub(crate) use {data::*, element::*, function::*, global::*, table::*, tag::*}; /// # Ok::<(), tinywasm::Error>(()) /// ``` pub trait ResourceLimiter: Send + Sync { - /// Returns whether a memory allocation or growth is allowed. + /// Checks a nonzero memory allocation or growth request. /// - /// Sizes are in bytes. `current` is zero for initial allocation, and `maximum` is `None` for an - /// unbounded memory. `Ok(false)` rejects the request, while `Err` traps. Rejected initial - /// allocations return [`Trap::OutOfMemory`], since they have no normal failure result. The - /// default implementation allows the request. + /// Sizes are in bytes. `current` is zero for initial allocation. `maximum` is the declared + /// maximum in bytes, saturated to `usize::MAX` if needed, or `None` if no maximum was declared. + /// Growth limits are checked before this callback. The default implementation allows the + /// request. fn memory_growing( &self, _current: usize, @@ -76,12 +81,11 @@ pub trait ResourceLimiter: Send + Sync { Ok(true) } - /// Returns whether a table allocation or growth is allowed. + /// Checks a nonzero table allocation or growth request. /// - /// Sizes are in elements. `current` is zero for initial allocation, and `maximum` is `None` for - /// an unbounded table. `Ok(false)` rejects the request, while `Err` traps. Rejected initial - /// allocations return [`Trap::OutOfMemory`], since they have no normal failure result. The - /// default implementation allows the request. + /// Sizes are in elements. `current` is zero for initial allocation. `maximum` is the declared + /// maximum element count when it fits `usize`, or `None` otherwise. Table limits are checked + /// before this callback. The default implementation allows the request. fn table_growing( &self, _current: usize, @@ -91,11 +95,12 @@ pub trait ResourceLimiter: Send + Sync { Ok(true) } - /// Returns whether a GC object allocation is allowed. + /// Checks a GC object allocation request. /// - /// Sizes are logical allocated bytes before and after the requested allocation. Unreachable - /// objects remain included until collection. `maximum` is currently always `None`. `Ok(false)` - /// rejects the allocation with [`Trap::OutOfMemory`], while `Err` returns the provided trap. + /// Sizes are the current TinyWasm-accounted heap bytes and that count plus the requested + /// allocation. They include unreachable objects until collection but not all allocator + /// overhead. `maximum` is currently always `None`. This callback runs before threshold-triggered + /// collection. fn gc_growing( &self, _current: usize, @@ -118,7 +123,7 @@ pub trait ResourceLimiter: Send + Sync { /// # _ = store; /// ``` /// -/// See +/// See pub struct Store { id: StoreId, pub(crate) module_instances: Vec, diff --git a/crates/types/src/instructions.rs b/crates/types/src/instructions.rs index e3d35d0..7e0cd28 100644 --- a/crates/types/src/instructions.rs +++ b/crates/types/src/instructions.rs @@ -536,8 +536,6 @@ pub enum BinOp128 { /// These instructions are an internal, version-specific representation and do not /// map one-to-one to WebAssembly instructions. Their variants and serialized form /// may change between TinyWasm releases. -/// -/// See #[rustfmt::skip] #[derive(Clone, Copy, PartialEq)] #[cfg_attr(feature = "debug", derive(Debug))] diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 71eac04..686433d 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -47,11 +47,12 @@ pub mod archive { impl core::error::Error for TwasmError {} } -/// A `TinyWasm` WebAssembly Module +/// TinyWasm's parsed and lowered representation of a WebAssembly module. /// -/// This is the internal representation of a WebAssembly module in `TinyWasm`. /// Modules produced by the parser are validated by default, but validation can be /// disabled for trusted input. Do not trust modules or archives from third parties. +/// +/// See #[derive(Clone, Default, PartialEq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -506,6 +507,9 @@ pub struct Global { pub init: Box<[ConstInstruction]>, } +/// A WebAssembly global type. +/// +/// See #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -539,6 +543,9 @@ impl Default for GlobalType { } } +/// A WebAssembly table type. +/// +/// See #[derive(Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -578,7 +585,9 @@ impl TableType { } } -/// Represents a memory's type. +/// A WebAssembly memory type. +/// +/// See #[derive(Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -619,7 +628,7 @@ impl MemoryType { } } - /// The declared maximum page count, or `None` when the memory is unbounded. + /// Returns the declared maximum page count, or `None` if no maximum was declared. #[inline] pub const fn page_count_max_declared(&self) -> Option { self.page_count_max @@ -670,6 +679,8 @@ pub enum MemoryArch { } /// A WebAssembly tag type. +/// +/// See #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] diff --git a/crates/types/src/reference.rs b/crates/types/src/reference.rs index a38f4c0..2d725ca 100644 --- a/crates/types/src/reference.rs +++ b/crates/types/src/reference.rs @@ -1,6 +1,8 @@ /// An abstract WebAssembly heap type. /// /// This contains exactly the abstract heap types in core Wasm 3.0. +/// +/// See #[repr(u8)] #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "debug", derive(Debug))] @@ -34,6 +36,8 @@ pub enum AbstractHeapType { /// For concrete types, `payload` is a module type index before instantiation /// and a canonical store type address at runtime. /// Otherwise, it is an [`AbstractHeapType`]. +/// +/// See #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] pub struct RefType(u32); diff --git a/crates/types/src/types.rs b/crates/types/src/types.rs index 51558e4..12386f1 100644 --- a/crates/types/src/types.rs +++ b/crates/types/src/types.rs @@ -3,6 +3,8 @@ use alloc::{boxed::Box, sync::Arc}; use crate::{TypeAddr, WasmType}; /// The dense type index space of a WebAssembly module. +/// +/// See #[derive(Clone, PartialEq, Eq, Default)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -31,6 +33,8 @@ impl TypeSection { } /// A type with optional declared subtyping. +/// +/// See #[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -58,6 +62,8 @@ impl SubType { } /// A function, struct, or array type. +/// +/// See #[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -123,6 +129,8 @@ impl FuncType { } /// A WebAssembly struct type. +/// +/// See #[derive(Clone, PartialEq, Eq, Default)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -131,6 +139,8 @@ pub struct StructType { } /// A WebAssembly array type. +/// +/// See #[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -139,6 +149,8 @@ pub struct ArrayType { } /// A struct field or array element type. +/// +/// See #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -148,6 +160,8 @@ pub struct FieldType { } /// A field's packed or unpacked storage type. +/// +/// See #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] diff --git a/crates/types/src/value.rs b/crates/types/src/value.rs index 45fc2bc..847e5e1 100644 --- a/crates/types/src/value.rs +++ b/crates/types/src/value.rs @@ -1,6 +1,8 @@ use crate::{RefType, StorageType}; /// Type of a WebAssembly value. +/// +/// See #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))]