Skip to content

Commit 8951dc7

Browse files
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 <mail@henrygressmann.de>
1 parent f46c32d commit 8951dc7

26 files changed

Lines changed: 530 additions & 281 deletions

‎CHANGELOG.md‎

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- Added support for the WebAssembly function-references proposal
13-
- Added support for the WebAssembly garbage-collection proposal
14-
- Added support for the WebAssembly exception-handling proposal, including tags, `try_table`, `throw`, and `throw_ref`
15-
- Added support for the WebAssembly compact-imports proposal
16-
- Added `WasmValue::ty` and `WasmValue::matches_type`. Non-null concrete reference types require Store-aware validation.
17-
- Added `ValueLane` for mapping WebAssembly value types to their physical 32-bit, 64-bit, or 128-bit storage lane.
18-
- Added a `validate` feature to `tinywasm` and `tinywasm-parser` (enabled by default) to optionally skip wasmparser validation for faster parsing of trusted modules.
19-
- Added optional parse-time operand deduplication to reduce precompiled module and `.twasm` archive size.
20-
- Added a `ResourceLimiter` trait, configurable through `engine::Config::with_resource_limiter`, to bound guest memory, table, and logical GC heap growth.
21-
- Added Store-aware owned references, explicit GC collection, and typed host access to GC objects and exceptions.
12+
- Support for the function-references, garbage-collection, exception-handling, and compact-imports proposals.
13+
- Store-aware owned references, explicit GC collection, and typed host access to GC objects and exceptions.
14+
- `WasmValue::ty`, `WasmValue::matches_type`, and `ValueLane` for inspecting value types and storage lanes.
15+
- `ResourceLimiter` for limiting guest memory, table, and logical GC heap growth.
16+
- A default `validate` Cargo feature that can be disabled when parsing trusted modules.
17+
- Optional parse-time operand deduplication for smaller `.twasm` archives.
2218

2319
### Changed
2420

25-
- `HostFunction` is now a reusable definition, and module instantiation borrows `Imports` so host imports can be shared across stores.
26-
- Host function callbacks now require `Send + Sync` so the same definition can be used safely with multiple stores.
27-
- Typed function tuples now support up to 20 parameters or results. Use untyped functions for larger signatures.
28-
- Module types now use one dense recursive type space, while function types are resolved through `Function::ty(&Store)`.
29-
- Globals are stored in separate 32-bit, 64-bit, and 128-bit value lanes, avoiding tagged value conversion during guest execution.
30-
- Linear memory now uses a single contiguous `Vec`-backed storage with const-generic fixed-width loads and stores.
31-
- Exceptions are stored as traced managed objects, allowing unreachable exceptions and their payload graphs to be collected.
32-
- Increased the minimum supported Rust version from 1.95 to 1.98.
21+
- Typed functions support tuples up to arity 20 and `[u8; 16]` values for `v128`.
22+
- Module types use one recursive type space, with runtime function types available through `Function::ty(&Store)`.
23+
- Linear memory uses contiguous `Vec`-backed storage.
24+
- The minimum supported Rust version is 1.98.
3325

3426
### Fixed
3527

@@ -41,23 +33,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4133

4234
### Breaking Changes
4335

44-
- `HostFunction::from` and `HostFunction::from_untyped` no longer take a `Store` and now return reusable `HostFunction` definitions
45-
- `ModuleInstance::instantiate` and `instantiate_no_start` now borrow `Imports`.
46-
- `Store::id` now returns `u32` instead of `usize`.
47-
- `Parser::new` now takes `ParserOptions`, use `Parser::default()` for default settings. `Parser::with_options` was removed.
48-
- Renamed `ModuleInstanceAddr` to `ModuleInstanceId`.
49-
- Removed `HostFunction::ty` and `WasmFunction::ty`. Use `Function::ty(&Store)` for runtime function types.
50-
- Changed `TableType::element_type` and `Element::ty` from `WasmType` to `RefType`, and replaced module `table_types` with `TableDefinition { ty, init }`.
51-
- 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`.
52-
- Removed the local-memory allocation analysis (`LocalMemoryAllocation` and `ParserOptions::optimize_local_memory_allocation`). Local memories are always allocated eagerly.
53-
- Removed `Config::with_trap_on_oom`. A `ResourceLimiter` can return a trap when rejecting a memory or table allocation or growth request.
54-
- `Table::grow` now returns `Result<Option<usize>>`, matching `Memory::grow`. Growth limits and allocation failures return `None`, while limiter-provided traps return an error.
55-
- 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.
56-
- `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.
57-
- Renamed the fallible `Memory`, `Table`, `Global`, and `Tag` constructors from `new` to `try_new`.
58-
- `WasmValue` is no longer `Copy` because it can contain owned references.
59-
- Nullable typed reference parameters and results use `Option<T>`. Bare typed reference values are non-null.
60-
- Removed `WasmTupleChain`. Use direct tuples up to arity 20 or untyped functions for larger signatures.
36+
- 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<()>`.
37+
- `HostFunction` definitions are reusable, require `Send + Sync`, and no longer take a `Store`.
38+
- Module instantiation borrows `Imports`, allowing imports to be shared across stores.
39+
- `HostFunction::ty` and `WasmFunction::ty` were removed. Use `Function::ty(&Store)` for runtime types.
40+
- Function and managed reference handles are Store-aware. Managed references keep their referents live, and `WasmValue` is no longer `Copy`.
41+
- Nullable typed reference parameters and results use `Option<T>`. Bare typed references are non-null.
42+
- 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.
43+
- Fallible `Memory`, `Table`, `Global`, and `Tag` constructors are named `try_new`.
44+
- `Store::id` returns `u32`, and `ModuleInstanceAddr` is renamed to `ModuleInstanceId`.
45+
- Table element types use `RefType`, and module table definitions use `TableDefinition { ty, init }`.
46+
- `Parser::new` takes `ParserOptions`. Use `Parser::default()` for default settings. `Parser::with_options` and local-memory allocation analysis were removed.
47+
- Pluggable memory backends and `Config::with_trap_on_oom` were removed. Linear memory is always `Vec`-backed, and allocation limits use `ResourceLimiter`.
48+
- `Table::grow` returns `Result<Option<usize>>`, matching `Memory::grow`.
49+
- `WasmTupleChain` was removed. Use direct tuples up to arity 20 or untyped functions for larger signatures.
6150

6251
## [0.10.0] - 2026-07-24
6352

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ TinyWasm targets non-JavaScript core proposals through [phase 3](https://github.
9595
| [**Typed Function References**](https://github.com/WebAssembly/function-references) | 🟢 | `next` |
9696
| [**Garbage Collection**](https://github.com/WebAssembly/gc) | 🟢 | `next` |
9797
| [**Exception Handling**](https://github.com/WebAssembly/exception-handling) | 🟢 | `next` |
98-
| [**Stack Switching**](https://github.com/WebAssembly/stack-switching) | 🌑 | - |
9998
| [**Compact Import Section**](https://github.com/WebAssembly/compact-import-section) | 🟢 | `next` |
99+
| [**Stack Switching**](https://github.com/WebAssembly/stack-switching) | 🌑 | - |
100100
| [**Threads**](https://github.com/WebAssembly/threads) | 🌑 | - |
101101

102102
**Legend**\

‎crates/cli/src/cmd/run.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ pub fn run(args: RunArgs) -> Result<()> {
2929
.ok_or_else(|| anyhow::anyhow!("export is not a function: {export}"))?;
3030
let func = instance.func_untyped(&store, export)?;
3131
let params = parse_invocation_args(func_ty, &args.args)?;
32-
let results = func.call(&mut store, &params)?;
32+
let mut results = vec![tinywasm::WasmValue::I32(0); func.ty(&store)?.results().len()];
33+
func.call(&mut store, &params, &mut results)?;
3334
print_results(&results);
3435
Ok(())
3536
}
@@ -44,7 +45,7 @@ pub fn run(args: RunArgs) -> Result<()> {
4445
"module has no start function or `_start` export. Use `tinywasm inspect {module_path}` or `tinywasm run --invoke <export> {module_path}`"
4546
)
4647
})?;
47-
start.call(&mut store, &[])?;
48+
start.call(&mut store, &[], &mut [])?;
4849
Ok(())
4950
}
5051
}

‎crates/cli/src/testsuite.rs‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ use std::io::{BufRead, BufReader, Seek, SeekFrom};
77
/// Result type used by the WAST test utilities.
88
pub type TestResult<T> = core::result::Result<T, Box<dyn core::error::Error>>;
99

10-
#[derive(Debug)]
1110
struct TestFailure(String);
1211

12+
impl core::fmt::Debug for TestFailure {
13+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
14+
Display::fmt(self, f)
15+
}
16+
}
17+
1318
impl Display for TestFailure {
1419
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1520
f.write_str(&self.0)
@@ -89,13 +94,9 @@ impl TestSuite {
8994
}
9095

9196
pub fn report_status(&self) -> TestResult<()> {
92-
if self.runner.failed() {
93-
println!();
94-
Err(TestFailure(format!("{}:\n{self}", "failed one or more tests".red().bold())).into())
95-
} else {
96-
println!("{self}");
97-
Ok(())
98-
}
97+
self.print_errors();
98+
println!("{self}");
99+
if self.runner.failed() { Err(TestFailure("failed one or more tests".into()).into()) } else { Ok(()) }
99100
}
100101

101102
pub fn save_csv(&self, path: &str, version: &str) -> TestResult<()> {

‎crates/cli/src/wast_runner.rs‎

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ impl WastRunner {
225225

226226
let mut store = Store::default();
227227
let mut module_registry = ModuleRegistry::default();
228+
let mut call_results = Vec::new();
228229

229230
println!("running {} tests for group: {}", directives.len(), file.name());
230231
for (i, directive) in directives.into_iter().enumerate() {
@@ -332,8 +333,9 @@ impl WastRunner {
332333
AssertExhaustion { call, message, span } => {
333334
let module = module_registry.get_idx(call.module);
334335
let args = convert_wastargs(&mut store, call.args)?;
335-
let res =
336-
catch_unwind_silent(|| exec_fn_instance(module, &mut store, call.name, &args).map(|_| ()));
336+
let res = catch_unwind_silent(|| {
337+
exec_fn_instance(module, &mut store, call.name, &args, &mut call_results)
338+
});
337339
let Ok(Err(tinywasm::Error::Trap(trap))) = res else {
338340
test_group.add_result(
339341
&format!("AssertExhaustion({i})"),
@@ -368,7 +370,7 @@ impl WastRunner {
368370
let module = module_registry.get_idx(invoke.module);
369371
let args = convert_wastargs(&mut store, invoke.args)
370372
.map_err(|err| tinywasm::Error::Other(err.to_string()))?;
371-
exec_fn_instance(module, &mut store, invoke.name, &args).map(|_| ())
373+
exec_fn_instance(module, &mut store, invoke.name, &args, &mut call_results)
372374
});
373375
match res {
374376
Err(err) => test_group.add_result(
@@ -415,7 +417,7 @@ impl WastRunner {
415417
let module = module_registry.get_idx(invoke.module);
416418
let args = convert_wastargs(&mut store, invoke.args)
417419
.map_err(|err| tinywasm::Error::Other(err.to_string()))?;
418-
exec_fn_instance(module, &mut store, invoke.name, &args).map(|_| ())
420+
exec_fn_instance(module, &mut store, invoke.name, &args, &mut call_results)
419421
});
420422
let result = match res {
421423
Err(err) => Err(anyhow!("test panicked: {}", try_downcast_panic(err))),
@@ -469,7 +471,7 @@ impl WastRunner {
469471
let res: Result<Result<()>, _> = catch_unwind_silent(|| {
470472
let args = convert_wastargs(&mut store, invoke.args)?;
471473
let module = module_registry.get_idx(invoke.module);
472-
exec_fn_instance(module, &mut store, invoke.name, &args).map_err(|e| {
474+
exec_fn_instance(module, &mut store, invoke.name, &args, &mut call_results).map_err(|e| {
473475
error!("failed to execute function: {e:?}");
474476
e
475477
})?;
@@ -554,21 +556,21 @@ impl WastRunner {
554556
let module = module_registry
555557
.get(invoke.module)
556558
.ok_or_else(|| anyhow!("module instance was not found"))?;
557-
let outcomes =
558-
exec_fn_instance(Some(module.id()), &mut store, invoke.name, &args).map_err(|e| {
559+
exec_fn_instance(Some(module.id()), &mut store, invoke.name, &args, &mut call_results)
560+
.map_err(|e| {
559561
error!("failed to execute function: {e:?}");
560562
e
561563
})?;
562-
if !expected_alternatives.iter().any(|expected| expected.len() == outcomes.len()) {
564+
if !expected_alternatives.iter().any(|expected| expected.len() == call_results.len()) {
563565
return Err(anyhow!(
564566
"expected {} results, got {}",
565567
expected_alternatives.first().map_or(0, |v| v.len()),
566-
outcomes.len()
568+
call_results.len()
567569
));
568570
}
569571
if expected_alternatives.iter().any(|expected| {
570-
expected.len() == outcomes.len()
571-
&& outcomes
572+
expected.len() == call_results.len()
573+
&& call_results
572574
.iter()
573575
.zip(expected.iter())
574576
.all(|(outcome, exp)| exp.matches(outcome, &store, &module))
@@ -716,12 +718,12 @@ fn exec_with_budget(
716718
func: &tinywasm::Function,
717719
store: &mut Store,
718720
args: &[WasmValue],
719-
) -> Result<Vec<WasmValue>, tinywasm::Error> {
720-
let mut exec = func.call_resumable(store, args)?;
721+
results: &mut [WasmValue],
722+
) -> Result<(), tinywasm::Error> {
723+
let mut exec = func.call_resumable(store, args, results)?;
721724
for _ in 0..TEST_MAX_SUSPENSIONS {
722-
match exec.resume_with_time_budget(TEST_TIME_SLICE)? {
723-
ExecProgress::Completed(values) => return Ok(values),
724-
ExecProgress::Suspended => {}
725+
if let ExecProgress::Completed(()) = exec.resume_with_time_budget(TEST_TIME_SLICE)? {
726+
return Ok(());
725727
}
726728
}
727729
Err(tinywasm::Error::Other(format!(
@@ -742,15 +744,18 @@ fn exec_fn_instance(
742744
store: &mut Store,
743745
name: &str,
744746
args: &[WasmValue],
745-
) -> Result<Vec<WasmValue>, tinywasm::Error> {
747+
results: &mut Vec<WasmValue>,
748+
) -> Result<(), tinywasm::Error> {
746749
let Some(instance) = instance else {
747750
return Err(tinywasm::Error::Other("no instance found".to_string()));
748751
};
749752
let Some(instance) = store.get_module_instance(instance) else {
750753
return Err(tinywasm::Error::Other("no instance found".to_string()));
751754
};
752755
let func = instance.func_untyped(store, name)?;
753-
exec_with_budget(&func, store, args)
756+
results.clear();
757+
results.resize(func.ty(store)?.results().len(), WasmValue::I32(0));
758+
exec_with_budget(&func, store, args, results)
754759
}
755760

756761
fn catch_unwind_silent<R>(f: impl FnOnce() -> R) -> std::thread::Result<R> {

0 commit comments

Comments
 (0)