Skip to content

Commit 1a17230

Browse files
alexcrichtonbongjunj
authored andcommitted
Make table growth a true async fn (bytecodealliance#11442)
* Make table growth a true `async fn` Upon further refactoring and thinking about bytecodealliance#11430 I've realized that we might be able to sidestep `T: Send` on the store entirely which would be quite the boon if it can be pulled off. The realization I had is that the main reason for this was `&mut dyn VMStore` on the stack, but that itself is actually a bug in Wasmtime (bytecodealliance#11178) and shouldn't be done. The functions which have this on the stack should actually ONLY have the resource limiter, if configured. This means that while the `ResourceLimiter{,Async}` traits need a `Send` supertrait that's relatively easy to add without much impact. My hunch is that plumbing this through to the end will enable all the benefits of bytecodealliance#11430 without requiring adding `T: Send` to the store. This commit starts out on this journey by making table growth a true `async fn`. A new internal type is added to represent a store's limiter which is plumbed to growth functions. This represents a hierarchy of borrows that look like: * `StoreInner<T>` * `StoreResourceLimiter<'_>` * `StoreOpaque` * `Pin<&mut Instance>` * `&mut vm::Table` This notably, safely, allows operating on `vm::Table` with a `StoreResourceLimiter` at the same time. This is exactly what's needed and prevents needing to have `&mut dyn VMStore`, the previous argument, on the stack. This refactoring cleans up `unsafe` blocks in table growth right now which manually uses raw pointers to work around the borrow checker. No more now! I'll note as well that this is just an incremental step. What I plan on doing next is handling other locations like memory growth, memory allocation, and table allocation. Each of those will require further refactorings to ensure that things like GC are correctly accounted for so they're going to be split into separate PRs. Functionally though this PR should have no impact other than a fiber is no longer required for `Table::grow_async`. * Remove #[cfg] gate
1 parent cf76fac commit 1a17230

File tree

9 files changed

+306
-106
lines changed

9 files changed

+306
-106
lines changed

crates/wasmtime/src/runtime/externals/table.rs

Lines changed: 71 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use crate::prelude::*;
22
use crate::runtime::RootedGcRefImpl;
3-
use crate::runtime::vm::{self as runtime, GcStore, TableElementType, VMFuncRef, VMGcRef};
3+
use crate::runtime::vm::{
4+
self, GcStore, SendSyncPtr, TableElementType, VMFuncRef, VMGcRef, VMStore,
5+
};
46
use crate::store::{AutoAssertNoGc, StoreInstanceId, StoreOpaque};
57
use crate::trampoline::generate_table_export;
68
use crate::{
7-
AnyRef, AsContext, AsContextMut, ExnRef, ExternRef, Func, HeapType, Ref, RefType, TableType,
8-
Trap,
9+
AnyRef, AsContext, AsContextMut, ExnRef, ExternRef, Func, HeapType, Ref, RefType,
10+
StoreContextMut, TableType, Trap,
911
};
1012
use core::iter;
1113
use core::ptr::NonNull;
@@ -139,7 +141,7 @@ impl Table {
139141
TableType::from_wasmtime_table(store.engine(), self.wasmtime_ty(store))
140142
}
141143

142-
/// Returns the `runtime::Table` within `store` as well as the optional
144+
/// Returns the `vm::Table` within `store` as well as the optional
143145
/// `GcStore` in use within `store`.
144146
///
145147
/// # Panics
@@ -149,7 +151,7 @@ impl Table {
149151
&self,
150152
store: &'a mut StoreOpaque,
151153
lazy_init_range: impl IntoIterator<Item = u64>,
152-
) -> (&'a mut runtime::Table, Option<&'a mut GcStore>) {
154+
) -> (&'a mut vm::Table, Option<&'a mut GcStore>) {
153155
self.instance.assert_belongs_to(store.id());
154156
let (store, instance) = store.optional_gc_store_and_instance_mut(self.instance.instance());
155157

@@ -281,42 +283,60 @@ impl Table {
281283
/// When using an async resource limiter, use [`Table::grow_async`]
282284
/// instead.
283285
pub fn grow(&self, mut store: impl AsContextMut, delta: u64, init: Ref) -> Result<u64> {
284-
let store = store.as_context_mut().0;
286+
vm::one_poll(self._grow(store.as_context_mut(), delta, init))
287+
.expect("must use `grow_async` when async resource limiters are in use")
288+
}
289+
290+
async fn _grow<T>(&self, store: StoreContextMut<'_, T>, delta: u64, init: Ref) -> Result<u64> {
291+
let store = store.0;
285292
let ty = self.ty(&store);
286-
let (table, _gc_store) = self.wasmtime_table(store, iter::empty());
287-
// FIXME(#11179) shouldn't need to subvert the borrow checker
288-
let table: *mut _ = table;
289-
unsafe {
290-
let result = match element_type(&ty) {
291-
TableElementType::Func => {
292-
let element = init.into_table_func(store, ty.element())?;
293-
(*table).grow_func(store, delta, element)?
294-
}
295-
TableElementType::GcRef => {
296-
// FIXME: `grow_gc_ref` shouldn't require the whole store
297-
// and should require `AutoAssertNoGc`. For now though we
298-
// know that table growth doesn't trigger GC so it should be
299-
// ok to create a copy of the GC reference even though it's
300-
// not tracked anywhere.
301-
let element = init
302-
.into_table_gc_ref(&mut AutoAssertNoGc::new(store), ty.element())?
303-
.map(|r| r.unchecked_copy());
304-
(*table).grow_gc_ref(store, delta, element.as_ref())?
305-
}
306-
// TODO(#10248) Required to support stack switching in the
307-
// embedder API.
308-
TableElementType::Cont => bail!("unimplemented table for cont"),
309-
};
310-
match result {
311-
Some(size) => {
312-
let vm = (*table).vmtable();
313-
store[self.instance].table_ptr(self.index).write(vm);
314-
// unwrap here should be ok because the runtime should always guarantee
315-
// that we can fit the table size in a 64-bit integer.
316-
Ok(u64::try_from(size).unwrap())
317-
}
318-
None => bail!("failed to grow table by `{}`", delta),
293+
let (mut limiter, store) = store.resource_limiter_and_store_opaque();
294+
let limiter = limiter.as_mut();
295+
let result = match element_type(&ty) {
296+
TableElementType::Func => {
297+
let element = init
298+
.into_table_func(store, ty.element())?
299+
.map(SendSyncPtr::new);
300+
self.instance
301+
.get_mut(store)
302+
.defined_table_grow(self.index, async |table| {
303+
// SAFETY: in the context of `defined_table_grow` this
304+
// is safe to call as it'll update the internal table
305+
// pointer in the instance.
306+
unsafe { table.grow_func(limiter, delta, element).await }
307+
})
308+
.await?
319309
}
310+
TableElementType::GcRef => {
311+
let mut store = AutoAssertNoGc::new(store);
312+
let element = init
313+
.into_table_gc_ref(&mut store, ty.element())?
314+
.map(|r| r.unchecked_copy());
315+
let (gc_store, instance) = self.instance.get_with_gc_store_mut(&mut store);
316+
instance
317+
.defined_table_grow(self.index, async |table| {
318+
// SAFETY: in the context of `defined_table_grow` this
319+
// is safe to call as it'll update the internal table
320+
// pointer in the instance.
321+
unsafe {
322+
table
323+
.grow_gc_ref(limiter, gc_store, delta, element.as_ref())
324+
.await
325+
}
326+
})
327+
.await?
328+
}
329+
// TODO(#10248) Required to support stack switching in the
330+
// embedder API.
331+
TableElementType::Cont => bail!("unimplemented table for cont"),
332+
};
333+
match result {
334+
Some(size) => {
335+
// unwrap here should be ok because the runtime should always
336+
// guarantee that we can fit the table size in a 64-bit integer.
337+
Ok(u64::try_from(size).unwrap())
338+
}
339+
None => bail!("failed to grow table by `{}`", delta),
320340
}
321341
}
322342

@@ -334,14 +354,7 @@ impl Table {
334354
delta: u64,
335355
init: Ref,
336356
) -> Result<u64> {
337-
let mut store = store.as_context_mut();
338-
assert!(
339-
store.0.async_support(),
340-
"cannot use `grow_async` without enabling async support on the config"
341-
);
342-
store
343-
.on_fiber(|store| self.grow(store, delta, init))
344-
.await?
357+
self._grow(store.as_context_mut(), delta, init).await
345358
}
346359

347360
/// Copy `len` elements from `src_table[src_index..]` into
@@ -516,11 +529,7 @@ impl Table {
516529
}
517530

518531
#[cfg(feature = "gc")]
519-
pub(crate) fn trace_roots(
520-
&self,
521-
store: &mut StoreOpaque,
522-
gc_roots_list: &mut crate::runtime::vm::GcRootsList,
523-
) {
532+
pub(crate) fn trace_roots(&self, store: &mut StoreOpaque, gc_roots_list: &mut vm::GcRootsList) {
524533
if !self
525534
._ty(store)
526535
.element()
@@ -549,9 +558,9 @@ impl Table {
549558
&module.tables[index]
550559
}
551560

552-
pub(crate) fn vmimport(&self, store: &StoreOpaque) -> crate::runtime::vm::VMTableImport {
561+
pub(crate) fn vmimport(&self, store: &StoreOpaque) -> vm::VMTableImport {
553562
let instance = &store[self.instance];
554-
crate::runtime::vm::VMTableImport {
563+
vm::VMTableImport {
555564
from: instance.table_ptr(self.index).into(),
556565
vmctx: instance.vmctx().into(),
557566
index: self.index,
@@ -691,4 +700,12 @@ mod tests {
691700

692701
Ok(())
693702
}
703+
704+
#[test]
705+
fn grow_is_send() {
706+
fn _assert_send<T: Send>(_: T) {}
707+
fn _grow(table: &Table, store: &mut Store<()>, init: Ref) {
708+
_assert_send(table.grow(store, 0, init))
709+
}
710+
}
694711
}

crates/wasmtime/src/runtime/fiber.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,6 @@ impl StoreOpaque {
393393
/// # Panics
394394
///
395395
/// Panics if this is invoked outside the context of a fiber.
396-
#[cfg(feature = "component-model-async")]
397396
pub(crate) fn with_blocking<R>(
398397
&mut self,
399398
f: impl FnOnce(&mut Self, &mut BlockingContext<'_, '_>) -> R,

crates/wasmtime/src/runtime/limits.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub const DEFAULT_MEMORY_LIMIT: usize = 10000;
2929
/// or not and you're otherwise working in an asynchronous context the
3030
/// [`ResourceLimiterAsync`] trait is also provided to avoid blocking an OS
3131
/// thread while a limit is determined.
32-
pub trait ResourceLimiter {
32+
pub trait ResourceLimiter: Send {
3333
/// Notifies the resource limiter that an instance's linear memory has been
3434
/// requested to grow.
3535
///
@@ -172,7 +172,7 @@ pub trait ResourceLimiter {
172172
/// answer the question whether growing a memory or table is allowed.
173173
#[cfg(feature = "async")]
174174
#[async_trait::async_trait]
175-
pub trait ResourceLimiterAsync {
175+
pub trait ResourceLimiterAsync: Send {
176176
/// Async version of [`ResourceLimiter::memory_growing`]
177177
async fn memory_growing(
178178
&mut self,

crates/wasmtime/src/runtime/store.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,43 @@ enum ResourceLimiterInner<T> {
247247
Async(Box<dyn (FnMut(&mut T) -> &mut dyn crate::ResourceLimiterAsync) + Send + Sync>),
248248
}
249249

250+
/// Representation of a configured resource limiter for a store.
251+
///
252+
/// This is acquired with `resource_limiter_and_store_opaque` for example and is
253+
/// threaded through to growth operations on tables/memories. Note that this is
254+
/// passed around as `Option<&mut StoreResourceLimiter<'_>>` to make it
255+
/// efficient to pass around (nullable pointer) and it's also notably passed
256+
/// around as an `Option` to represent how this is optionally specified within a
257+
/// store.
258+
pub enum StoreResourceLimiter<'a> {
259+
Sync(&'a mut dyn crate::ResourceLimiter),
260+
#[cfg(feature = "async")]
261+
Async(&'a mut dyn crate::ResourceLimiterAsync),
262+
}
263+
264+
impl StoreResourceLimiter<'_> {
265+
pub(crate) async fn table_growing(
266+
&mut self,
267+
current: usize,
268+
desired: usize,
269+
maximum: Option<usize>,
270+
) -> Result<bool, Error> {
271+
match self {
272+
Self::Sync(s) => s.table_growing(current, desired, maximum),
273+
#[cfg(feature = "async")]
274+
Self::Async(s) => s.table_growing(current, desired, maximum).await,
275+
}
276+
}
277+
278+
pub(crate) fn table_grow_failed(&mut self, error: anyhow::Error) -> Result<()> {
279+
match self {
280+
Self::Sync(s) => s.table_grow_failed(error),
281+
#[cfg(feature = "async")]
282+
Self::Async(s) => s.table_grow_failed(error),
283+
}
284+
}
285+
}
286+
250287
enum CallHookInner<T: 'static> {
251288
#[cfg(feature = "call-hook")]
252289
Sync(Box<dyn FnMut(StoreContextMut<'_, T>, CallHook) -> Result<()> + Send + Sync>),
@@ -2234,6 +2271,19 @@ unsafe impl<T> vm::VMStore for StoreInner<T> {
22342271
&mut self.inner
22352272
}
22362273

2274+
fn resource_limiter_and_store_opaque(
2275+
&mut self,
2276+
) -> (Option<StoreResourceLimiter<'_>>, &mut StoreOpaque) {
2277+
(
2278+
self.limiter.as_mut().map(|l| match l {
2279+
ResourceLimiterInner::Sync(s) => StoreResourceLimiter::Sync(s(&mut self.data)),
2280+
#[cfg(feature = "async")]
2281+
ResourceLimiterInner::Async(s) => StoreResourceLimiter::Async(s(&mut self.data)),
2282+
}),
2283+
&mut self.inner,
2284+
)
2285+
}
2286+
22372287
fn memory_growing(
22382288
&mut self,
22392289
current: usize,

crates/wasmtime/src/runtime/store/data.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::runtime::vm::{self, VMStore};
1+
use crate::runtime::vm::{self, GcStore, VMStore};
22
use crate::store::StoreOpaque;
33
use crate::{StoreContext, StoreContextMut};
44
use core::num::NonZeroU64;
@@ -252,6 +252,16 @@ impl StoreInstanceId {
252252
self.assert_belongs_to(store.id());
253253
store.instance_mut(self.instance)
254254
}
255+
256+
/// Same as [`Self::get_mut`], but also returns the `GcStore`.
257+
#[inline]
258+
pub(crate) fn get_with_gc_store_mut<'a>(
259+
&self,
260+
store: &'a mut StoreOpaque,
261+
) -> (Option<&'a mut GcStore>, Pin<&'a mut vm::Instance>) {
262+
self.assert_belongs_to(store.id());
263+
store.optional_gc_store_and_instance_mut(self.instance)
264+
}
255265
}
256266

257267
impl Index<StoreInstanceId> for StoreOpaque {

crates/wasmtime/src/runtime/vm.rs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ pub(crate) struct f64x2(crate::uninhabited::Uninhabited);
3535

3636
use crate::StoreContextMut;
3737
use crate::prelude::*;
38-
use crate::store::StoreInner;
39-
use crate::store::StoreOpaque;
38+
use crate::store::{StoreInner, StoreOpaque, StoreResourceLimiter};
4039
use crate::type_registry::RegisteredType;
4140
use alloc::sync::Arc;
4241
use core::fmt;
43-
use core::ops::Deref;
44-
use core::ops::DerefMut;
42+
use core::ops::{Deref, DerefMut};
43+
use core::pin::pin;
4544
use core::ptr::NonNull;
4645
use core::sync::atomic::{AtomicUsize, Ordering};
46+
use core::task::{Context, Poll, Waker};
4747
use wasmtime_environ::{
4848
DefinedFuncIndex, DefinedMemoryIndex, HostPtr, VMOffsets, VMSharedTypeIndex,
4949
};
@@ -201,6 +201,12 @@ pub unsafe trait VMStore: 'static {
201201
/// Get an exclusive borrow of this store's `StoreOpaque`.
202202
fn store_opaque_mut(&mut self) -> &mut StoreOpaque;
203203

204+
/// Returns a split borrow to the limiter plus `StoreOpaque` at the same
205+
/// time.
206+
fn resource_limiter_and_store_opaque(
207+
&mut self,
208+
) -> (Option<StoreResourceLimiter<'_>>, &mut StoreOpaque);
209+
204210
/// Callback invoked to allow the store's resource limiter to reject a
205211
/// memory grow operation.
206212
fn memory_growing(
@@ -509,3 +515,38 @@ impl fmt::Display for WasmFault {
509515
)
510516
}
511517
}
518+
519+
/// Asserts that the future `f` is ready and returns its output.
520+
///
521+
/// This function is intended to be used when `async_support` is verified as
522+
/// disabled. Internals of Wasmtime are generally `async` when they optionally
523+
/// can be, meaning that synchronous entrypoints will invoke this function
524+
/// after invoking the asynchronous internals. Due to `async_support` being
525+
/// disabled there should be no way to introduce a yield point meaning that all
526+
/// futures built from internal functions should always be ready.
527+
///
528+
/// # Panics
529+
///
530+
/// Panics if `f` is not yet ready.
531+
pub fn assert_ready<F: Future>(f: F) -> F::Output {
532+
one_poll(f).unwrap()
533+
}
534+
535+
/// Attempts one poll of `f` to see if its output is available.
536+
///
537+
/// This function is intended for a few minor entrypoints into the Wasmtime API
538+
/// where a synchronous function is documented to work even when `async_support`
539+
/// is enabled. For example growing a `Memory` can be done with a synchronous
540+
/// function, but it's documented to panic with an async resource limiter.
541+
///
542+
/// This function provides the opportunity to poll `f` once to see if its output
543+
/// is available. If it isn't then `None` is returned and an appropriate panic
544+
/// message should be generated recommending to use an async function (e.g.
545+
/// `grow_async` instead of `grow`).
546+
pub fn one_poll<F: Future>(f: F) -> Option<F::Output> {
547+
let mut context = Context::from_waker(&Waker::noop());
548+
match pin!(f).poll(&mut context) {
549+
Poll::Ready(output) => Some(output),
550+
Poll::Pending => None,
551+
}
552+
}

crates/wasmtime/src/runtime/vm/instance.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -766,13 +766,13 @@ impl Instance {
766766
/// Performs a grow operation on the `table_index` specified using `grow`.
767767
///
768768
/// This will handle updating the VMTableDefinition internally as necessary.
769-
pub(crate) fn defined_table_grow(
769+
pub(crate) async fn defined_table_grow(
770770
mut self: Pin<&mut Self>,
771771
table_index: DefinedTableIndex,
772-
grow: impl FnOnce(&mut Table) -> Result<Option<usize>>,
772+
grow: impl AsyncFnOnce(&mut Table) -> Result<Option<usize>>,
773773
) -> Result<Option<usize>> {
774774
let table = self.as_mut().get_defined_table(table_index);
775-
let result = grow(table);
775+
let result = grow(table).await;
776776
let element = table.vmtable();
777777
self.set_table(table_index, element);
778778
result

0 commit comments

Comments
 (0)