Skip to content

Commit 41b1072

Browse files
feat: add ResourceLimiter::gc_growing
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent 951c807 commit 41b1072

7 files changed

Lines changed: 61 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- Added `ValueLane` for mapping WebAssembly value types to their physical 32-bit, 64-bit, or 128-bit storage lane.
1818
- Added a `validate` feature to `tinywasm` and `tinywasm-parser` (enabled by default) to optionally skip wasmparser validation for faster parsing of trusted modules.
1919
- 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 and table allocation and growth.
20+
- Added a `ResourceLimiter` trait, configurable through `engine::Config::with_resource_limiter`, to bound guest memory, table, and logical GC heap growth.
2121

2222
### Changed
2323

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ TinyWasm modules can be compiled to the internal `twasm` bytecode format, which
6161

6262
With default features disabled, `tinywasm` depends only on `core`, `alloc`, and `libm`[^libm], making it usable in `no_std + alloc` environments.
6363

64-
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 and table allocation and growth and can trap rejected requests.
64+
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.
6565

6666
[^libm]: [rust-lang/rust#137578](https://github.com/rust-lang/rust/issues/137578) — tracking issue for floating-point math support in `no_std`.
6767

crates/tinywasm/src/store/gc/arena.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub(crate) struct Arena<T> {
6363
free_head: Option<u32>,
6464
worklist: Vec<u32>,
6565
len: usize,
66-
allocated_bytes: usize,
66+
pub(super) allocated_bytes: usize,
6767
collection_threshold: usize,
6868
next_collection: usize,
6969
}
@@ -86,7 +86,7 @@ impl<T> Arena<T> {
8686
///
8787
/// The byte count must remain valid while the value is in the arena.
8888
pub(crate) fn alloc(&mut self, value: T, out_of_line_bytes: usize) -> Result<Handle, AllocError> {
89-
let bytes = size_of::<Slot<T>>().checked_add(out_of_line_bytes).ok_or(AllocError)?;
89+
let bytes = Self::allocation_size(out_of_line_bytes).ok_or(AllocError)?;
9090
let allocated_bytes = self.allocated_bytes.checked_add(bytes).ok_or(AllocError)?;
9191

9292
let handle = if let Some(index) = self.free_head {
@@ -156,11 +156,14 @@ impl<T> Arena<T> {
156156

157157
/// Returns whether an allocation of this size should trigger collection.
158158
pub(crate) fn should_collect(&self, out_of_line_bytes: usize) -> bool {
159-
size_of::<Slot<T>>()
160-
.checked_add(out_of_line_bytes)
159+
Self::allocation_size(out_of_line_bytes)
161160
.and_then(|bytes| self.allocated_bytes.checked_add(bytes))
162161
.is_none_or(|bytes| bytes >= self.next_collection)
163162
}
163+
164+
pub(super) fn allocation_size(out_of_line_bytes: usize) -> Option<usize> {
165+
size_of::<Slot<T>>().checked_add(out_of_line_bytes)
166+
}
164167
}
165168

166169
impl<T: Trace> Arena<T> {

crates/tinywasm/src/store/gc/object.rs

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
use alloc::{boxed::Box, vec::Vec};
1+
use alloc::{boxed::Box, sync::Arc, vec::Vec};
22
use core::cell::RefCell;
33
use core::mem::size_of;
44
use core::sync::atomic::{AtomicU32, Ordering};
55

66
use tinywasm_types::TypeAddr;
77

8+
use crate::engine::Config;
89
use crate::interpreter::{TinyWasmValue, ValueRef};
10+
use crate::{ResourceLimiter, Trap};
911

1012
use super::{AllocError, Arena, Handle, Trace};
1113

@@ -29,18 +31,24 @@ pub(crate) struct GcHeap {
2931
objects: Arena<GcObject>,
3032
directory: Vec<(u32, Handle)>,
3133
pinned: RefCell<Vec<Handle>>,
34+
resource_limiter: Option<Arc<dyn ResourceLimiter>>,
3235
}
3336

3437
impl Default for GcHeap {
3538
fn default() -> Self {
36-
Self::new(1024 * 1024)
39+
Self::new(&Config::default())
3740
}
3841
}
3942

4043
impl GcHeap {
4144
/// Creates a heap with the configured allocation threshold.
42-
pub(crate) const fn new(collection_threshold: usize) -> Self {
43-
Self { objects: Arena::new(collection_threshold), directory: Vec::new(), pinned: RefCell::new(Vec::new()) }
45+
pub(crate) fn new(config: &Config) -> Self {
46+
Self {
47+
objects: Arena::new(config.gc_collection_threshold),
48+
directory: Vec::new(),
49+
pinned: RefCell::new(Vec::new()),
50+
resource_limiter: config.resource_limiter.clone(),
51+
}
4452
}
4553

4654
#[inline]
@@ -67,15 +75,25 @@ impl GcHeap {
6775
type_addr: TypeAddr,
6876
values: Vec<TinyWasmValue>,
6977
trace_references: bool,
70-
) -> Result<ValueRef, AllocError> {
78+
) -> Result<ValueRef, Trap> {
79+
let element_size = size_of::<TinyWasmValue>() + if trace_references { size_of::<Option<Handle>>() } else { 0 };
80+
let out_of_line_bytes = values.len().checked_mul(element_size).ok_or(Trap::OutOfMemory)?;
81+
let allocation_size = Arena::<GcObject>::allocation_size(out_of_line_bytes).ok_or(Trap::OutOfMemory)?;
82+
let desired = self.objects.allocated_bytes.checked_add(allocation_size).ok_or(Trap::OutOfMemory)?;
83+
if let Some(limiter) = &self.resource_limiter
84+
&& !limiter.gc_growing(self.objects.allocated_bytes, desired, None)?
85+
{
86+
return Err(Trap::OutOfMemory);
87+
}
88+
7189
let key =
7290
NEXT_GC_REF.try_update(Ordering::Relaxed, Ordering::Relaxed, |key| (key < (1 << 30)).then_some(key + 1));
7391
let Ok(key) = key else {
74-
return Err(AllocError);
92+
return Err(Trap::OutOfMemory);
7593
};
7694
let references = if trace_references {
7795
let mut references = Vec::new();
78-
references.try_reserve_exact(values.len()).map_err(|_| AllocError)?;
96+
references.try_reserve_exact(values.len()).map_err(|_| Trap::OutOfMemory)?;
7997
references.extend(values.iter().map(|value| match value {
8098
TinyWasmValue::ValueRef(value) => self.handle(*value),
8199
_ => None,
@@ -84,11 +102,9 @@ impl GcHeap {
84102
} else {
85103
None
86104
};
87-
let element_size = size_of::<TinyWasmValue>() + if trace_references { size_of::<Option<Handle>>() } else { 0 };
88-
let out_of_line_bytes = values.len().checked_mul(element_size).ok_or(AllocError)?;
89105
let object = GcObject { type_addr, values: values.into_boxed_slice(), references };
90-
self.directory.try_reserve(1).map_err(|_| AllocError)?;
91-
let handle = self.objects.alloc(object, out_of_line_bytes)?;
106+
self.directory.try_reserve(1).map_err(|_| Trap::OutOfMemory)?;
107+
let handle = self.objects.alloc(object, out_of_line_bytes).map_err(|_| Trap::OutOfMemory)?;
92108
self.directory.push((key, handle));
93109
Ok(ValueRef::from_category_addr(key))
94110
}
@@ -149,8 +165,7 @@ impl GcHeap {
149165

150166
pub(crate) fn should_collect(&self, value_count: usize, trace_references: bool) -> bool {
151167
let element_size = size_of::<TinyWasmValue>() + if trace_references { size_of::<Option<Handle>>() } else { 0 };
152-
let bytes = value_count.saturating_mul(element_size);
153-
self.objects.should_collect(bytes)
168+
self.objects.should_collect(value_count.saturating_mul(element_size))
154169
}
155170

156171
/// Reclaims objects unreachable from runtime and permanent host roots.

crates/tinywasm/src/store/mod.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ static STORE_ID: AtomicU32 = AtomicU32::new(0);
3535
///
3636
/// Configure a limiter with
3737
/// [`Config::with_resource_limiter`](crate::engine::Config::with_resource_limiter). It currently
38-
/// controls guest linear-memory and table allocation and growth. It does not account for stacks,
39-
/// GC storage, runtime metadata, or other host allocations.
38+
/// controls guest linear-memory, table, and GC heap growth. It does not account for stacks, runtime
39+
/// metadata, temporary buffers, backing-capacity overhead, or other host allocations.
4040
///
4141
/// # Example
4242
/// ```rust
@@ -94,6 +94,20 @@ pub trait ResourceLimiter: Send + Sync {
9494
) -> core::result::Result<bool, Trap> {
9595
Ok(true)
9696
}
97+
98+
/// Returns whether a GC object allocation is allowed.
99+
///
100+
/// Sizes are the logical bytes retained by live GC objects after collection. `maximum` is
101+
/// currently always `None`. `Ok(false)` rejects the allocation with [`Trap::OutOfMemory`], while
102+
/// `Err` returns the provided trap. The default implementation allows the request.
103+
fn gc_growing(
104+
&self,
105+
_current: usize,
106+
_desired: usize,
107+
_maximum: Option<usize>,
108+
) -> core::result::Result<bool, Trap> {
109+
Ok(true)
110+
}
97111
}
98112

99113
/// Runtime state used by WebAssembly instances and host functions.
@@ -150,7 +164,7 @@ impl Store {
150164
pub fn new(engine: Engine) -> Self {
151165
let id =
152166
STORE_ID.try_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1)).expect("too many stores");
153-
let state = State::new(engine.config().gc_collection_threshold);
167+
let state = State::new(engine.config());
154168
Self {
155169
id,
156170
module_instances: Vec::new(),

crates/tinywasm/src/store/state.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use alloc::vec::Vec;
22

33
use super::*;
4+
use crate::engine::Config;
45

56
/// Global state that can be manipulated by WebAssembly programs
67
///
@@ -18,11 +19,11 @@ pub(crate) struct State {
1819
pub(crate) exceptions: Vec<ExceptionInstance>,
1920
pub(crate) elements: Vec<ElementInstance>,
2021
pub(crate) data: Vec<DataInstance>,
21-
pub(crate) gc: Box<gc::GcHeap>,
22+
pub(crate) gc: gc::GcHeap,
2223
}
2324

2425
impl State {
25-
pub(crate) fn new(gc_collection_threshold: usize) -> Self {
26+
pub(crate) fn new(config: &Config) -> Self {
2627
Self {
2728
canonical_types: Vec::new(),
2829
canonical_rec_group_lengths: Vec::new(),
@@ -34,7 +35,7 @@ impl State {
3435
exceptions: Vec::new(),
3536
elements: Vec::new(),
3637
data: Vec::new(),
37-
gc: Box::new(gc::GcHeap::new(gc_collection_threshold)),
38+
gc: gc::GcHeap::new(config),
3839
}
3940
}
4041

@@ -120,7 +121,7 @@ impl State {
120121
}));
121122
cold_err!(self.gc.collect(roots)).map_err(|_| Trap::OutOfMemory)?;
122123
}
123-
cold_err!(self.gc.alloc(type_addr, values, trace_references)).map_err(|_| Trap::OutOfMemory)
124+
self.gc.alloc(type_addr, values, trace_references)
124125
}
125126

126127
/// Pins a host-visible reference when it resolves to a managed GC object.
@@ -441,6 +442,6 @@ impl State {
441442

442443
impl Default for State {
443444
fn default() -> Self {
444-
Self::new(1024 * 1024)
445+
Self::new(&Config::default())
445446
}
446447
}

crates/tinywasm/tests/gc_refs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
use tinywasm::types::{ExternRef, RefValue, WasmValue};
23
use tinywasm::{Engine, ExecProgress, ModuleInstance, Store, engine::Config};
34

0 commit comments

Comments
 (0)