Skip to content

Commit b39509f

Browse files
refactor: separate interpreter code and execution state
- borrow function code separately from mutable runtime state - forward the 32-bit lane height through tail handlers - add an optional preserve-none ABI. - add parser checkpoints and remove time-budgeted execution Adapts some of the ideas from #74 and #79 Signed-off-by: Henry <mail@henrygressmann.de>
1 parent aaaab6b commit b39509f

38 files changed

Lines changed: 2177 additions & 1412 deletions

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Added new `Memory::data` and `Memory::data_mut` methods for direct access to linear memory.
1414
- Added several internal methods to support a future c-api crate
1515

16+
### Removed
17+
18+
- Removed `resume_with_time_budget`. Use `resume_with_fuel` and check application deadlines between resumes.
19+
1620
### Fixed
1721

1822
- Restore store state after a panic in a function call, allowing subsequent calls and preventing invalid resumption.

‎README.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ See the [examples](./examples) directory and [documentation](https://docs.rs/tin
5959
- **`debug`:** Derives `Debug` for runtime types. Enabled by default.
6060
- **`guest-debug`:** Exposes module-internal by-index inspection APIs (`*_by_index`).
6161
- **`nightly-tail-calls`:** Uses Rust's unstable explicit tail calls for interpreter dispatch. Requires nightly Rust (recommended for maximum performance).
62+
- **`nightly-rust-preserve-none`:** Uses Rust's experimental preserve-none ABI for x86_64 and 64-bit AArch64 tail-call handlers. Requires nightly Rust (recommended for maximum performance).
6263
- **`simd-x86`:** Enables x86-specific SIMD intrinsics and uses `unsafe` internally.
6364

6465
With default features disabled, `tinywasm` supports `no_std + alloc` and depends only on `libm`.
@@ -75,7 +76,9 @@ Applications that only load `.twasm` can remove the parser and validator from th
7576

7677
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.
7778

78-
Validation does not limit parsing or execution resources. Hosts that run untrusted code should also configure `parser::ParseLimits`, stack and `ResourceLimiter` limits, and fuel- or time-budgeted execution as needed.
79+
Validation does not limit parsing or execution resources. Hosts that run untrusted code should also configure `parser::ParseLimits`, stack and `ResourceLimiter` limits, and fuel-budgeted execution as needed. The default fuel policy counts interpreter steps. Applications can check their own deadlines between calls to `resume_with_fuel`.
80+
81+
Use `parser::ParserOptions::new().with_checkpoint_interval(4096)` to tune straight-line budget-check spacing before parsing. The default is 1,024 emitted interpreter instructions, with up to three extra for pending fusion. Larger intervals trade responsiveness for lower checkpoint overhead. Taken loop backedges are checked independently, and synthetic checkpoints consume no fuel.
7982

8083
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.
8184

‎crates/cli/src/wast_runner.rs‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use std::fmt::{Display, Formatter};
33
use std::fs::canonicalize;
44
use std::panic::{self, AssertUnwindSafe};
55
use std::path::PathBuf;
6-
use std::time::Duration;
76

87
use anyhow::{Context, Result, anyhow, bail};
98
use log::{debug, error};
@@ -14,7 +13,7 @@ use tinywasm::{
1413
use wast::QuoteWat;
1514
use wast::core::{AbstractHeapType, NanPattern};
1615

17-
const TEST_TIME_SLICE: Duration = Duration::from_millis(20);
16+
const TEST_FUEL_SLICE: u32 = 64 * 1024;
1817
const TEST_MAX_SUSPENSIONS: u32 = 1000;
1918
// Older suites classify encodings that wasmparser accepts with the latest feature set as malformed.
2019
const ACCEPTED_MALFORMED_MESSAGES: &[&str] =
@@ -733,13 +732,13 @@ fn exec_with_budget(
733732
) -> Result<(), tinywasm::Error> {
734733
let mut exec = func.call_resumable(store, args, results)?;
735734
for _ in 0..TEST_MAX_SUSPENSIONS {
736-
if let ExecProgress::Completed(()) = exec.resume_with_time_budget(TEST_TIME_SLICE)? {
735+
if let ExecProgress::Completed(()) = exec.resume_with_fuel(TEST_FUEL_SLICE)? {
737736
return Ok(());
738737
}
739738
}
740739
Err(tinywasm::Error::Other(format!(
741-
"testsuite execution timed out after {} time slices of {:?}",
742-
TEST_MAX_SUSPENSIONS, TEST_TIME_SLICE
740+
"testsuite execution exhausted {} fuel slices of {} units",
741+
TEST_MAX_SUSPENSIONS, TEST_FUEL_SLICE
743742
)))
744743
}
745744

‎crates/parser/src/emitter.rs‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ enum PatchSite {
5757
pub(crate) struct Emitter {
5858
instructions: Vec<Instruction>,
5959
committed: usize,
60+
checkpoint_start: usize,
61+
checkpoint_interval: usize,
6062
select: bool,
6163
self_func_addr: u32,
6264
return_instruction: Instruction,
@@ -79,6 +81,8 @@ impl Emitter {
7981
Self {
8082
instructions: Vec::with_capacity(body_size.min(1024)),
8183
committed: 0,
84+
checkpoint_start: 0,
85+
checkpoint_interval: options.checkpoint_interval(),
8286
select: options.optimize(),
8387
self_func_addr: context.self_func_addr,
8488
return_instruction,
@@ -146,9 +150,19 @@ impl Emitter {
146150

147151
fn push(&mut self, instruction: Instruction) -> Result<()> {
148152
self.seal_jump();
149-
if self.instructions.len() >= u32::MAX as usize {
153+
let checkpoint = self.committed - self.checkpoint_start >= self.checkpoint_interval;
154+
let added = 1 + usize::from(checkpoint);
155+
if self.instructions.len().checked_add(added).is_none_or(|len| len > u32::MAX as usize) {
150156
return Err(ParseError::Other("function body is too large".into()));
151157
}
158+
if checkpoint {
159+
// Seal the pending fusion window instead of inserting into the committed prefix.
160+
// This permits at most LOOKBEHIND extra guest instructions per interval. Labels bound
161+
// at this boundary target the checkpoint, and exclusive exception ends exclude it.
162+
self.instructions.push(Instruction::Checkpoint);
163+
self.commit(self.instructions.len());
164+
self.checkpoint_start = self.instructions.len();
165+
}
152166
self.instructions.push(instruction);
153167
Ok(())
154168
}

‎crates/parser/src/lib.rs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ pub struct ParserOptions {
7171
/// Whether to deduplicate immutable function operands while parsing.
7272
pub deduplicate_operands: bool,
7373

74+
/// Target number of emitted guest instructions between synthetic execution checkpoints.
75+
///
76+
/// Defaults to 1,024. The pending fusion window can add up to three instructions per interval.
77+
/// Larger intervals reduce checkpoint overhead but allow longer straight-line execution before
78+
/// a resumable invocation checks its budget. Taken control-flow boundaries are checked separately.
79+
pub checkpoint_interval: core::num::NonZeroUsize,
80+
7481
/// Optional limits for parsing untrusted modules. Unlimited by default.
7582
pub limits: ParseLimits,
7683

@@ -91,6 +98,7 @@ impl Default for ParserOptions {
9198
validation: cfg!(feature = "validate"),
9299
optimize: true,
93100
deduplicate_operands: true,
101+
checkpoint_interval: core::num::NonZeroUsize::new(1024).unwrap(),
94102
limits: ParseLimits::default(),
95103
#[cfg(parallel_parser)]
96104
threads: None,
@@ -143,6 +151,23 @@ impl ParserOptions {
143151
self.deduplicate_operands
144152
}
145153

154+
/// Sets the target number of emitted guest instructions between execution checkpoints.
155+
///
156+
/// This also applies when optimizations are disabled. Synthetic checkpoints consume no guest
157+
/// fuel. This bounds interpreter work, not time spent in bulk operations or host callbacks.
158+
///
159+
/// # Panics
160+
/// Panics if `interval` is zero.
161+
pub const fn with_checkpoint_interval(mut self, interval: usize) -> Self {
162+
self.checkpoint_interval = core::num::NonZeroUsize::new(interval).expect("checkpoint interval must be nonzero");
163+
self
164+
}
165+
166+
/// Returns the target execution checkpoint interval in emitted guest instructions.
167+
pub const fn checkpoint_interval(&self) -> usize {
168+
self.checkpoint_interval.get()
169+
}
170+
146171
/// Set parse-time limits.
147172
pub const fn with_limits(mut self, limits: ParseLimits) -> Self {
148173
self.limits = limits;

‎crates/tinywasm/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ guest-debug = []
132132
# use explicit tail calls for interpreter dispatch (requires nightly Rust)
133133
nightly-tail-calls = []
134134

135+
# use the experimental preserve-none ABI for x86_64 and 64-bit AArch64 tail-call handlers
136+
nightly-rust-preserve-none = ["nightly-tail-calls"]
137+
135138
# enable x86-specific SIMD intrinsics in Value128 (uses unsafe code)
136139
# note: for x86 backend selection, compile with x86-64-v3 target features
137140
# (for example: `RUSTFLAGS="-C target-cpu=x86-64-v3"`)

‎crates/tinywasm/build.rs‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use std::env;
2+
3+
fn main() {
4+
println!("cargo::rerun-if-changed=build.rs");
5+
println!(
6+
"cargo::rustc-check-cfg=cfg(tail_abi, values(\"Rust\", \"C-unwind\", \"sysv64-unwind\", \"rust-preserve-none\"))"
7+
);
8+
println!("cargo::rustc-check-cfg=cfg(simd_backend, values(\"generic\", \"wasm\", \"x86\"))");
9+
10+
let arch = env::var("CARGO_CFG_TARGET_ARCH").expect("Cargo target architecture");
11+
let features = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or_default();
12+
let x86_v3 = ["sse4.2", "avx", "avx2", "bmi1", "bmi2", "fma", "lzcnt", "movbe", "popcnt"]
13+
.iter()
14+
.all(|required| features.split(',').any(|enabled| enabled == *required));
15+
let simd = match arch.as_str() {
16+
"wasm32" | "wasm64" => "wasm",
17+
"x86_64" if env::var_os("CARGO_FEATURE_SIMD_X86").is_some() && x86_v3 => "x86",
18+
_ => "generic",
19+
};
20+
println!("cargo::rustc-cfg=simd_backend={simd:?}");
21+
22+
if env::var_os("CARGO_FEATURE_NIGHTLY_TAIL_CALLS").is_none() {
23+
return;
24+
}
25+
26+
let width = env::var("CARGO_CFG_TARGET_POINTER_WIDTH").expect("Cargo target pointer width");
27+
let os = env::var("CARGO_CFG_TARGET_OS").expect("Cargo target OS");
28+
let preserve_none = env::var_os("CARGO_FEATURE_NIGHTLY_RUST_PRESERVE_NONE").is_some();
29+
let abi = match (arch.as_str(), width.as_str(), os.as_str(), preserve_none) {
30+
("aarch64", "32", _, _) => "C-unwind",
31+
("x86_64", _, _, true) | ("aarch64", "64", _, true) => "rust-preserve-none",
32+
("x86_64", _, "windows", _) => "sysv64-unwind",
33+
_ => "Rust",
34+
};
35+
println!("cargo::rustc-cfg=tail_abi={abi:?}");
36+
}
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Mark-and-sweep storage with stable handles for WebAssembly GC objects.
1+
//! Mark-and-sweep storage with stable generational handles.
22
33
use alloc::vec::Vec;
44
use core::cell::Cell;
@@ -63,7 +63,8 @@ pub(crate) struct Arena<T> {
6363
free_head: Option<u32>,
6464
worklist: Vec<u32>,
6565
len: usize,
66-
pub(super) allocated_bytes: usize,
66+
/// Bytes retained by live objects, including their slot storage.
67+
pub(crate) allocated_bytes: usize,
6768
collection_threshold: usize,
6869
next_collection: usize,
6970
}
@@ -161,7 +162,8 @@ impl<T> Arena<T> {
161162
.is_none_or(|bytes| bytes >= self.next_collection)
162163
}
163164

164-
pub(super) fn allocation_size(out_of_line_bytes: usize) -> Option<usize> {
165+
/// Returns the retained bytes for one slot and its separately allocated storage.
166+
pub(crate) fn allocation_size(out_of_line_bytes: usize) -> Option<usize> {
165167
size_of::<Slot<T>>().checked_add(out_of_line_bytes)
166168
}
167169
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//! Internal storage collections used by stacks and the managed heap.
2+
3+
mod arena;
4+
mod stack;
5+
6+
pub(crate) use arena::{AllocError, Arena, Handle, Trace};
7+
pub(crate) use stack::StackLane;

0 commit comments

Comments
 (0)