diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 7e8bacd1ef0d..f5acb0c41a2e 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -383,6 +383,9 @@ wasmtime_option_group! { /// Component model support for async lifting/lowering: this corresponds /// to the ๐ŸšŸ emoji in the component model specification. pub component_model_async_stackful: Option, + /// Component model support for threading: this corresponds + /// to the ๐Ÿงต emoji in the component model specification. + pub component_model_threading: Option, /// Component model support for `error-context`: this corresponds /// to the ๐Ÿ“ emoji in the component model specification. pub component_model_error_context: Option, @@ -1059,6 +1062,7 @@ impl CommonOptions { ("component-model-async", component_model_async, wasm_component_model_async) ("component-model-async", component_model_async_builtins, wasm_component_model_async_builtins) ("component-model-async", component_model_async_stackful, wasm_component_model_async_stackful) + ("component-model-async", component_model_threading, wasm_component_model_threading) ("component-model", component_model_error_context, wasm_component_model_error_context) ("threads", threads, wasm_threads) ("gc", gc, wasm_gc) diff --git a/crates/cranelift/src/compiler/component.rs b/crates/cranelift/src/compiler/component.rs index 533c150b11e2..7bbf8a3590b6 100644 --- a/crates/cranelift/src/compiler/component.rs +++ b/crates/cranelift/src/compiler/component.rs @@ -871,6 +871,94 @@ impl<'a> TrampolineCompiler<'a> { }, ); } + Trampoline::ThreadIndex => { + self.translate_libcall( + host::thread_index, + TrapSentinel::NegativeOne, + WasmArgs::InRegisters, + |_, _| {}, + ); + } + Trampoline::ThreadNewIndirect { + instance, + start_func_table_idx, + start_func_ty_idx, + } => { + self.translate_libcall( + host::thread_new_indirect, + TrapSentinel::NegativeOne, + WasmArgs::InRegisters, + |me, params| { + params.push(me.index_value(*instance)); + params.push(me.index_value(*start_func_table_idx)); + params.push(me.index_value(*start_func_ty_idx)); + }, + ); + } + Trampoline::ThreadSwitchTo { + instance, + cancellable, + } => { + self.translate_libcall( + host::thread_switch_to, + TrapSentinel::NegativeOne, + WasmArgs::InRegisters, + |me, params| { + params.push(me.index_value(*instance)); + params.push( + me.builder + .ins() + .iconst(ir::types::I8, i64::from(*cancellable)), + ); + }, + ); + } + Trampoline::ThreadSuspend { + instance, + cancellable, + } => { + self.translate_libcall( + host::thread_suspend, + TrapSentinel::NegativeOne, + WasmArgs::InRegisters, + |me, params| { + params.push(me.index_value(*instance)); + params.push( + me.builder + .ins() + .iconst(ir::types::I8, i64::from(*cancellable)), + ); + }, + ); + } + Trampoline::ThreadResumeLater { instance } => { + self.translate_libcall( + host::thread_resume_later, + TrapSentinel::Falsy, + WasmArgs::InRegisters, + |me, params| { + params.push(me.index_value(*instance)); + }, + ); + } + Trampoline::ThreadYieldTo { + instance, + cancellable, + } => { + self.translate_libcall( + host::thread_yield_to, + TrapSentinel::NegativeOne, + WasmArgs::InRegisters, + |me, params| { + params.push(me.index_value(*instance)); + params.push( + me.builder + .ins() + .iconst(ir::types::I8, i64::from(*cancellable)), + ); + }, + ); + } } } diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index 414c1a3ec53e..e1c7962d383b 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -189,6 +189,18 @@ macro_rules! foreach_builtin_component_function { context_get(vmctx: vmctx, caller_instance: u32, slot: u32) -> u64; #[cfg(feature = "component-model-async")] context_set(vmctx: vmctx, caller_instance: u32, slot: u32, val: u32) -> bool; + #[cfg(feature = "component-model-async")] + thread_index(vmctx: vmctx) -> u64; + #[cfg(feature = "component-model-async")] + thread_new_indirect(vmctx: vmctx, caller_instance: u32, func_ty_id: u32, func_table_idx: u32, func_idx: u32, context: u32) -> u64; + #[cfg(feature = "component-model-async")] + thread_switch_to(vmctx: vmctx, caller_instance: u32, cancellable: u8, thread_idx: u32) -> u32; + #[cfg(feature = "component-model-async")] + thread_suspend(vmctx: vmctx, caller_instance: u32, cancellable: u8) -> u32; + #[cfg(feature = "component-model-async")] + thread_resume_later(vmctx: vmctx, caller_instance: u32, thread_idx: u32) -> bool; + #[cfg(feature = "component-model-async")] + thread_yield_to(vmctx: vmctx, caller_instance: u32, cancellable: u8, thread_idx: u32) -> u32; trap(vmctx: vmctx, code: u8) -> bool; diff --git a/crates/environ/src/component/dfg.rs b/crates/environ/src/component/dfg.rs index 68e234e338b6..9df5116e2b04 100644 --- a/crates/environ/src/component/dfg.rs +++ b/crates/environ/src/component/dfg.rs @@ -69,9 +69,12 @@ pub struct ComponentDfg { /// Same as `reallocs`, but for post-return. pub post_returns: Intern, - /// Same as `reallocs`, but for post-return. + /// Same as `reallocs`, but for memories. pub memories: Intern>, + /// Same as `reallocs`, but for tables. + pub tables: Intern>, + /// Metadata about identified fused adapters. /// /// Note that this list is required to be populated in-order where the @@ -483,6 +486,27 @@ pub enum Trampoline { instance: RuntimeComponentInstanceIndex, slot: u32, }, + ThreadIndex, + ThreadNewIndirect { + instance: RuntimeComponentInstanceIndex, + start_func_ty_idx: ComponentTypeIndex, + start_func_table_id: TableId, + }, + ThreadSwitchTo { + instance: RuntimeComponentInstanceIndex, + cancellable: bool, + }, + ThreadSuspend { + instance: RuntimeComponentInstanceIndex, + cancellable: bool, + }, + ThreadResumeLater { + instance: RuntimeComponentInstanceIndex, + }, + ThreadYieldTo { + instance: RuntimeComponentInstanceIndex, + cancellable: bool, + }, } #[derive(Copy, Clone, Hash, Eq, PartialEq)] @@ -833,6 +857,15 @@ impl LinearizeDfg<'_> { ) } + fn runtime_table(&mut self, table: TableId) -> RuntimeTableIndex { + self.intern( + table, + |me| &mut me.runtime_tables, + |me, table| me.core_export(&me.dfg.tables[table]), + |index, export| GlobalInitializer::ExtractTable(ExtractTable { index, export }), + ) + } + fn runtime_realloc(&mut self, realloc: ReallocId) -> RuntimeReallocIndex { self.intern( realloc, @@ -1135,6 +1168,40 @@ impl LinearizeDfg<'_> { instance: *instance, slot: *slot, }, + Trampoline::ThreadIndex => info::Trampoline::ThreadIndex, + Trampoline::ThreadNewIndirect { + instance, + start_func_ty_idx, + start_func_table_id, + } => info::Trampoline::ThreadNewIndirect { + instance: *instance, + start_func_ty_idx: *start_func_ty_idx, + start_func_table_idx: self.runtime_table(*start_func_table_id), + }, + Trampoline::ThreadSwitchTo { + instance, + cancellable, + } => info::Trampoline::ThreadSwitchTo { + instance: *instance, + cancellable: *cancellable, + }, + Trampoline::ThreadSuspend { + instance, + cancellable, + } => info::Trampoline::ThreadSuspend { + instance: *instance, + cancellable: *cancellable, + }, + Trampoline::ThreadResumeLater { instance } => info::Trampoline::ThreadResumeLater { + instance: *instance, + }, + Trampoline::ThreadYieldTo { + instance, + cancellable, + } => info::Trampoline::ThreadYieldTo { + instance: *instance, + cancellable: *cancellable, + }, }; let i1 = self.trampolines.push(*signature); let i2 = self.trampoline_defs.push(trampoline); diff --git a/crates/environ/src/component/info.rs b/crates/environ/src/component/info.rs index 251d6ef181d0..5e7e47438104 100644 --- a/crates/environ/src/component/info.rs +++ b/crates/environ/src/component/info.rs @@ -831,7 +831,7 @@ pub enum Trampoline { ThreadYield { /// The specific component instance which is calling the intrinsic. instance: RuntimeComponentInstanceIndex, - /// If `true`, indicates the caller instance maybe receive notification + /// If `true`, indicates the caller instance may receive notification /// of task cancellation. cancellable: bool, }, @@ -1123,6 +1123,52 @@ pub enum Trampoline { /// Which slot to update. slot: u32, }, + + /// Intrinsic used to implement the `thread.index` component model builtin. + ThreadIndex, + + /// Intrinsic used to implement the `thread.new_indirect` component model builtin. + ThreadNewIndirect { + /// The specific component instance which is calling the intrinsic. + instance: RuntimeComponentInstanceIndex, + /// The type index for the start function of the thread. + start_func_ty_idx: ComponentTypeIndex, + /// The index of the table that stores the start function. + start_func_table_idx: RuntimeTableIndex, + }, + + /// Intrinsic used to implement the `thread.switch-to` component model builtin. + ThreadSwitchTo { + /// The specific component instance which is calling the intrinsic. + instance: RuntimeComponentInstanceIndex, + /// If `true`, indicates the caller instance may receive notification + /// of task cancellation. + cancellable: bool, + }, + + /// Intrinsic used to implement the `thread.suspend` component model builtin. + ThreadSuspend { + /// The specific component instance which is calling the intrinsic. + instance: RuntimeComponentInstanceIndex, + /// If `true`, indicates the caller instance may receive notification + /// of task cancellation. + cancellable: bool, + }, + + /// Intrinsic used to implement the `thread.resume-later` component model builtin. + ThreadResumeLater { + /// The specific component instance which is calling the intrinsic. + instance: RuntimeComponentInstanceIndex, + }, + + /// Intrinsic used to implement the `thread.yield-to` component model builtin. + ThreadYieldTo { + /// The specific component instance which is calling the intrinsic. + instance: RuntimeComponentInstanceIndex, + /// If `true`, indicates the caller instance may receive notification + /// of task cancellation. + cancellable: bool, + }, } impl Trampoline { @@ -1188,6 +1234,12 @@ impl Trampoline { ErrorContextTransfer => format!("error-context-transfer"), ContextGet { .. } => format!("context-get"), ContextSet { .. } => format!("context-set"), + ThreadIndex => format!("thread-index"), + ThreadNewIndirect { .. } => format!("thread-new-indirect"), + ThreadSwitchTo { .. } => format!("thread-switch-to"), + ThreadSuspend { .. } => format!("thread-suspend"), + ThreadResumeLater { .. } => format!("thread-resume-later"), + ThreadYieldTo { .. } => format!("thread-yield-to"), } } } diff --git a/crates/environ/src/component/translate.rs b/crates/environ/src/component/translate.rs index 580dc5c82613..7be1728700fb 100644 --- a/crates/environ/src/component/translate.rs +++ b/crates/environ/src/component/translate.rs @@ -318,6 +318,29 @@ enum LocalInitializer<'data> { func: ModuleInternedTypeIndex, i: u32, }, + ThreadIndex { + func: ModuleInternedTypeIndex, + }, + ThreadNewIndirect { + func: ModuleInternedTypeIndex, + start_func_ty: ComponentTypeIndex, + start_func_table_index: TableIndex, + }, + ThreadSwitchTo { + func: ModuleInternedTypeIndex, + cancellable: bool, + }, + ThreadSuspend { + func: ModuleInternedTypeIndex, + cancellable: bool, + }, + ThreadResumeLater { + func: ModuleInternedTypeIndex, + }, + ThreadYieldTo { + func: ModuleInternedTypeIndex, + cancellable: bool, + }, // core wasm modules ModuleStatic(StaticModuleIndex, ComponentCoreModuleTypeId), @@ -1121,24 +1144,42 @@ impl<'a, 'data> Translator<'a, 'data> { core_func_index += 1; LocalInitializer::ContextSet { i, func } } - wasmparser::CanonicalFunction::ThreadIndex => { - bail!("unimplemented `thread.index`"); + let func = self.core_func_signature(core_func_index)?; + core_func_index += 1; + LocalInitializer::ThreadIndex { func } } - wasmparser::CanonicalFunction::ThreadNewIndirect { .. } => { - bail!("unimplemented `thread.new-indirect`"); + wasmparser::CanonicalFunction::ThreadNewIndirect { + func_ty_index, + table_index, + } => { + let func = self.core_func_signature(core_func_index)?; + core_func_index += 1; + LocalInitializer::ThreadNewIndirect { + func, + start_func_ty: ComponentTypeIndex::from_u32(func_ty_index), + start_func_table_index: TableIndex::from_u32(table_index), + } } - wasmparser::CanonicalFunction::ThreadSwitchTo { .. } => { - bail!("unimplemented `thread.switch-to`"); + wasmparser::CanonicalFunction::ThreadSwitchTo { cancellable } => { + let func = self.core_func_signature(core_func_index)?; + core_func_index += 1; + LocalInitializer::ThreadSwitchTo { func, cancellable } } - wasmparser::CanonicalFunction::ThreadSuspend { .. } => { - bail!("unimplemented `thread.suspend`"); + wasmparser::CanonicalFunction::ThreadSuspend { cancellable } => { + let func = self.core_func_signature(core_func_index)?; + core_func_index += 1; + LocalInitializer::ThreadSuspend { func, cancellable } } wasmparser::CanonicalFunction::ThreadResumeLater => { - bail!("unimplemented `thread.resume-later`"); + let func = self.core_func_signature(core_func_index)?; + core_func_index += 1; + LocalInitializer::ThreadResumeLater { func } } - wasmparser::CanonicalFunction::ThreadYieldTo { .. } => { - bail!("unimplemented `thread.yield-to`"); + wasmparser::CanonicalFunction::ThreadYieldTo { cancellable } => { + let func = self.core_func_signature(core_func_index)?; + core_func_index += 1; + LocalInitializer::ThreadYieldTo { func, cancellable } } }; self.result.initializers.push(init); diff --git a/crates/environ/src/component/translate/inline.rs b/crates/environ/src/component/translate/inline.rs index 360f7313f823..5b78fe7e3ee1 100644 --- a/crates/environ/src/component/translate/inline.rs +++ b/crates/environ/src/component/translate/inline.rs @@ -1127,7 +1127,75 @@ impl<'a> Inliner<'a> { )); frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); } + ThreadIndex { func } => { + let index = self + .result + .trampolines + .push((*func, dfg::Trampoline::ThreadIndex)); + frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); + } + ThreadNewIndirect { + func, + start_func_table_index, + start_func_ty, + } => { + let table_export = frame.tables[*start_func_table_index] + .clone() + .map_index(|i| match i { + EntityIndex::Table(i) => i, + _ => unreachable!(), + }); + let table_id = self.result.tables.push(table_export); + let index = self.result.trampolines.push(( + *func, + dfg::Trampoline::ThreadNewIndirect { + instance: frame.instance, + start_func_ty_idx: *start_func_ty, + start_func_table_id: table_id, + }, + )); + frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); + } + ThreadSwitchTo { func, cancellable } => { + let index = self.result.trampolines.push(( + *func, + dfg::Trampoline::ThreadSwitchTo { + instance: frame.instance, + cancellable: *cancellable, + }, + )); + frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); + } + ThreadSuspend { func, cancellable } => { + let index = self.result.trampolines.push(( + *func, + dfg::Trampoline::ThreadSuspend { + instance: frame.instance, + cancellable: *cancellable, + }, + )); + frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); + } + ThreadResumeLater { func } => { + let index = self.result.trampolines.push(( + *func, + dfg::Trampoline::ThreadResumeLater { + instance: frame.instance, + }, + )); + frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); + } + ThreadYieldTo { func, cancellable } => { + let index = self.result.trampolines.push(( + *func, + dfg::Trampoline::ThreadYieldTo { + instance: frame.instance, + cancellable: *cancellable, + }, + )); + frame.funcs.push((*func, dfg::CoreDef::Trampoline(index))); + } ModuleStatic(idx, ty) => { frame.modules.push(ModuleDef::Static(*idx, *ty)); } diff --git a/crates/fuzzing/src/generators/config.rs b/crates/fuzzing/src/generators/config.rs index 6f70915721b9..aacfbb465b99 100644 --- a/crates/fuzzing/src/generators/config.rs +++ b/crates/fuzzing/src/generators/config.rs @@ -137,6 +137,7 @@ impl Config { component_model_async, component_model_async_builtins, component_model_async_stackful, + component_model_threading, component_model_error_context, component_model_gc, simd, @@ -159,6 +160,7 @@ impl Config { component_model_async_builtins.unwrap_or(false); self.module_config.component_model_async_stackful = component_model_async_stackful.unwrap_or(false); + self.module_config.component_model_threading = component_model_threading.unwrap_or(false); self.module_config.component_model_error_context = component_model_error_context.unwrap_or(false); self.module_config.component_model_gc = component_model_gc.unwrap_or(false); @@ -283,6 +285,7 @@ impl Config { Some(self.module_config.component_model_async_builtins); cfg.wasm.component_model_async_stackful = Some(self.module_config.component_model_async_stackful); + cfg.wasm.component_model_threading = Some(self.module_config.component_model_threading); cfg.wasm.component_model_error_context = Some(self.module_config.component_model_error_context); cfg.wasm.component_model_gc = Some(self.module_config.component_model_gc); diff --git a/crates/fuzzing/src/generators/module.rs b/crates/fuzzing/src/generators/module.rs index 7b65c92c4c31..942ff9fad882 100644 --- a/crates/fuzzing/src/generators/module.rs +++ b/crates/fuzzing/src/generators/module.rs @@ -19,6 +19,7 @@ pub struct ModuleConfig { pub component_model_async: bool, pub component_model_async_builtins: bool, pub component_model_async_stackful: bool, + pub component_model_threading: bool, pub component_model_error_context: bool, pub component_model_gc: bool, pub legacy_exceptions: bool, @@ -70,6 +71,7 @@ impl<'a> Arbitrary<'a> for ModuleConfig { component_model_async: false, component_model_async_builtins: false, component_model_async_stackful: false, + component_model_threading: false, component_model_error_context: false, component_model_gc: false, legacy_exceptions: false, diff --git a/crates/misc/component-async-tests/tests/scenario/util.rs b/crates/misc/component-async-tests/tests/scenario/util.rs index 10aeae2f4a51..0d204e2ad7da 100644 --- a/crates/misc/component-async-tests/tests/scenario/util.rs +++ b/crates/misc/component-async-tests/tests/scenario/util.rs @@ -37,6 +37,7 @@ pub fn config() -> Config { config.wasm_component_model_async(true); config.wasm_component_model_async_builtins(true); config.wasm_component_model_async_stackful(true); + config.wasm_component_model_threading(true); config.wasm_component_model_error_context(true); config.async_support(true); config diff --git a/crates/test-util/src/wasmtime_wast.rs b/crates/test-util/src/wasmtime_wast.rs index feec31e2146e..e4f9a0497103 100644 --- a/crates/test-util/src/wasmtime_wast.rs +++ b/crates/test-util/src/wasmtime_wast.rs @@ -40,6 +40,7 @@ pub fn apply_test_config(config: &mut Config, test_config: &wast::TestConfig) { component_model_async, component_model_async_builtins, component_model_async_stackful, + component_model_threading, component_model_error_context, component_model_gc, nan_canonicalization, @@ -67,6 +68,7 @@ pub fn apply_test_config(config: &mut Config, test_config: &wast::TestConfig) { let component_model_async = component_model_async.unwrap_or(false); let component_model_async_builtins = component_model_async_builtins.unwrap_or(false); let component_model_async_stackful = component_model_async_stackful.unwrap_or(false); + let component_model_threading = component_model_threading.unwrap_or(false); let component_model_error_context = component_model_error_context.unwrap_or(false); let component_model_gc = component_model_gc.unwrap_or(false); let nan_canonicalization = nan_canonicalization.unwrap_or(false); @@ -102,6 +104,7 @@ pub fn apply_test_config(config: &mut Config, test_config: &wast::TestConfig) { .wasm_component_model_async(component_model_async) .wasm_component_model_async_builtins(component_model_async_builtins) .wasm_component_model_async_stackful(component_model_async_stackful) + .wasm_component_model_threading(component_model_threading) .wasm_component_model_error_context(component_model_error_context) .wasm_component_model_gc(component_model_gc) .wasm_exceptions(exceptions) diff --git a/crates/test-util/src/wast.rs b/crates/test-util/src/wast.rs index f98ddfee33fc..ad72d11de3d6 100644 --- a/crates/test-util/src/wast.rs +++ b/crates/test-util/src/wast.rs @@ -233,6 +233,7 @@ macro_rules! foreach_config_option { component_model_async component_model_async_builtins component_model_async_stackful + component_model_threading component_model_error_context component_model_gc simd @@ -668,6 +669,16 @@ impl WastTest { let failing_component_model_tests = [ // FIXME(#11683) "component-model/test/values/trap-in-post-return.wast", + // Awaiting https://github.com/WebAssembly/component-model/pull/570 + "component-model/test/resources/multiple-resources.wast", + "component-model/test/async/empty-wait.wast", + "component-model/test/async/drop-stream.wast", + "component-model/test/async/passing-resources.wast", + "component-model/test/async/async-calls-sync.wast", + "component-model/test/async/partial-stream-copies.wast", + "component-model/test/async/futures-must-write.wast", + "component-model/test/async/cancel-stream.wast", + "component-model/test/async/drop-waitable-set.wast", ]; if failing_component_model_tests .iter() diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 9890feb8b87b..6be6a5ab0c6b 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -1208,6 +1208,19 @@ impl Config { self } + /// This corresponds to the ๐Ÿงต emoji in the component model specification. + /// + /// Please note that Wasmtime's support for this feature is _very_ + /// incomplete. + /// + /// [proposal]: + /// https://github.com/WebAssembly/component-model/pull/557 + #[cfg(feature = "component-model-async")] + pub fn wasm_component_model_threading(&mut self, enable: bool) -> &mut Self { + self.wasm_features(WasmFeatures::CM_THREADING, enable); + self + } + /// This corresponds to the ๐Ÿ“ emoji in the component model specification. /// /// Please note that Wasmtime's support for this feature is _very_ diff --git a/crates/wasmtime/src/runtime/component/concurrent.rs b/crates/wasmtime/src/runtime/component/concurrent.rs index 9aca9de4bc64..4030459479e2 100644 --- a/crates/wasmtime/src/runtime/component/concurrent.rs +++ b/crates/wasmtime/src/runtime/component/concurrent.rs @@ -58,7 +58,7 @@ use crate::vm::component::{ CallContext, ComponentInstance, InstanceFlags, ResourceTables, TransmitLocalState, }; use crate::vm::{AlwaysMut, SendSyncPtr, VMFuncRef, VMMemoryDefinition, VMStore}; -use crate::{AsContext, AsContextMut, StoreContext, StoreContextMut, ValRaw}; +use crate::{AsContext, AsContextMut, FuncType, StoreContext, StoreContextMut, ValRaw, ValType}; use anyhow::{Context as _, Result, anyhow, bail}; use error_contexts::GlobalErrorContextRefCount; use futures::channel::oneshot; @@ -82,12 +82,13 @@ use std::sync::Arc; use std::task::{Context, Poll, Waker}; use std::vec::Vec; use table::{TableDebug, TableId}; +use wasmtime_environ::Trap; use wasmtime_environ::component::{ CanonicalOptions, CanonicalOptionsDataModel, ExportIndex, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, OptionsIndex, PREPARE_ASYNC_NO_RESULT, PREPARE_ASYNC_WITH_RESULT, - RuntimeComponentInstanceIndex, StringEncoding, TypeComponentGlobalErrorContextTableIndex, - TypeComponentLocalErrorContextTableIndex, TypeFutureTableIndex, TypeStreamTableIndex, - TypeTupleIndex, + RuntimeComponentInstanceIndex, RuntimeTableIndex, StringEncoding, + TypeComponentGlobalErrorContextTableIndex, TypeComponentLocalErrorContextTableIndex, + TypeFuncIndex, TypeFutureTableIndex, TypeStreamTableIndex, TypeTupleIndex, }; pub use abort::JoinHandle; @@ -103,7 +104,7 @@ pub(crate) use futures_and_streams::{ mod abort; mod error_contexts; mod futures_and_streams; -mod table; +pub(crate) mod table; pub(crate) mod tls; /// Constant defined in the Component Model spec to indicate that the async @@ -568,14 +569,16 @@ enum SuspendReason { /// waitable set or task. Waiting { set: TableId, - task: TableId, + thread: QualifiedThreadId, }, /// The fiber has finished handling its most recent work item and is waiting /// for another (or to be dropped if it is no longer needed). NeedWork, /// The fiber is yielding and should be resumed once other tasks have had a /// chance to run. - Yielding { task: TableId }, + Yielding { thread: QualifiedThreadId }, + /// The fiber was explicitly suspended with a call to `thread.suspend` or `thread.switch-to`. + ExplicitlySuspending { thread: QualifiedThreadId }, } /// Represents a pending call into guest code for a given guest task. @@ -593,7 +596,8 @@ enum GuestCallKind { }, /// Indicates that a new guest task call is pending and may be executed /// using the specified closure. - Start(Box Result<()> + Send + Sync>), + StartImplicit(Box Result<()> + Send + Sync>), + StartExplicit(Box Result<()> + Send + Sync>), } impl fmt::Debug for GuestCallKind { @@ -604,15 +608,16 @@ impl fmt::Debug for GuestCallKind { .field("instance", instance) .field("set", set) .finish(), - Self::Start(_) => f.debug_tuple("Start").finish(), + Self::StartImplicit(_) => f.debug_tuple("StartImplicit").finish(), + Self::StartExplicit(_) => f.debug_tuple("StartExplicit").finish(), } } } -/// Represents a pending call into guest code for a given guest task. +/// Represents a pending call into guest code for a given guest thread. #[derive(Debug)] struct GuestCall { - task: TableId, + thread: QualifiedThreadId, kind: GuestCallKind, } @@ -627,11 +632,13 @@ impl GuestCall { /// - the call is for a not-yet started task and the (sub-)component /// instance to be called has backpressure enabled fn is_ready(&self, state: &mut ConcurrentState) -> Result { - let task_instance = state.get_mut(self.task)?.instance; + let task_instance = state.get_mut(self.thread.task)?.instance; let state = state.instance_state(task_instance); + let ready = match &self.kind { GuestCallKind::DeliverEvent { .. } => !state.do_not_enter, - GuestCallKind::Start(_) => !(state.do_not_enter || state.backpressure > 0), + GuestCallKind::StartImplicit(_) => !(state.do_not_enter || state.backpressure > 0), + GuestCallKind::StartExplicit(_) => true, }; log::trace!( "call {self:?} ready? {ready} (do_not_enter: {}; backpressure: {})", @@ -652,10 +659,10 @@ enum WorkerItem { /// or `CallbackCode.POLL`). #[derive(Debug)] struct PollParams { - /// The instance to which the polling task belongs. + /// The instance to which the polling thread belongs. instance: Instance, - /// The polling task. - task: TableId, + /// The polling thread. + thread: QualifiedThreadId, /// The waitable set being polled. set: TableId, } @@ -687,6 +694,13 @@ impl fmt::Debug for WorkItem { } } +/// Whether a suspension intrinsic was cancelled or completed +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub(crate) enum WaitResult { + Cancelled, + Completed, +} + /// Poll the specified future until it completes on behalf of a guest->host call /// using a sync-lowered import. /// @@ -701,10 +715,10 @@ pub(crate) fn poll_and_block( ) -> Result { let state = store.concurrent_state_mut(); - // If there is no current guest task set, that means the host function was + // If there is no current guest thread set, that means the host function was // registered using e.g. `LinkerInstance::func_wrap`, in which case it // should complete immediately. - let Some(caller) = state.guest_task else { + let Some(caller) = state.guest_thread else { return match pin!(future).poll(&mut Context::from_waker(&Waker::noop())) { Poll::Ready(result) => result, Poll::Pending => { @@ -716,7 +730,7 @@ pub(crate) fn poll_and_block( // Save any existing result stashed in `GuestTask::result` so we can replace // it with the new result. let old_result = state - .get_mut(caller) + .get_mut(caller.task) .with_context(|| format!("bad handle: {caller:?}"))? .result .take(); @@ -735,7 +749,7 @@ pub(crate) fn poll_and_block( let result = future.await?; tls::get(move |store| { let state = store.concurrent_state_mut(); - state.get_mut(caller)?.result = Some(Box::new(result) as _); + state.get_mut(caller.task)?.result = Some(Box::new(result) as _); Waitable::Host(task).set_event( state, @@ -772,16 +786,19 @@ pub(crate) fn poll_and_block( let state = store.concurrent_state_mut(); state.push_future(future); - let set = state.get_mut(caller)?.sync_call_set; + let set = state.get_mut(caller.task)?.sync_call_set; Waitable::Host(task).join(state, Some(set))?; - store.suspend(SuspendReason::Waiting { set, task: caller })?; + store.suspend(SuspendReason::Waiting { + set, + thread: caller, + })?; } } // Retrieve and return the result. Ok(*mem::replace( - &mut store.concurrent_state_mut().get_mut(caller)?.result, + &mut store.concurrent_state_mut().get_mut(caller.task)?.result, old_result, ) .unwrap() @@ -793,46 +810,50 @@ pub(crate) fn poll_and_block( fn handle_guest_call(store: &mut dyn VMStore, call: GuestCall) -> Result<()> { match call.kind { GuestCallKind::DeliverEvent { instance, set } => { - let (event, waitable) = instance.get_event(store, call.task, set, true)?.unwrap(); + let (event, waitable) = instance + .get_event(store, call.thread.task, set, true)? + .unwrap(); let state = store.concurrent_state_mut(); - let task = state.get_mut(call.task)?; + let task = state.get_mut(call.thread.task)?; let runtime_instance = task.instance; let handle = waitable.map(|(_, v)| v).unwrap_or(0); log::trace!( "use callback to deliver event {event:?} to {:?} for {waitable:?}", - call.task, + call.thread, ); - let old_task = state.guest_task.replace(call.task); + let old_thread = state.guest_thread.replace(call.thread); log::trace!( - "GuestCallKind::DeliverEvent: replaced {old_task:?} with {:?} as current task", - call.task + "GuestCallKind::DeliverEvent: replaced {old_thread:?} with {:?} as current thread", + call.thread ); - store.maybe_push_call_context(call.task)?; + store.maybe_push_call_context(call.thread.task)?; let state = store.concurrent_state_mut(); state.enter_instance(runtime_instance); - let callback = state.get_mut(call.task)?.callback.take().unwrap(); + let callback = state.get_mut(call.thread.task)?.callback.take().unwrap(); let code = callback(store, runtime_instance, event, handle)?; let state = store.concurrent_state_mut(); - state.get_mut(call.task)?.callback = Some(callback); - + state.get_mut(call.thread.task)?.callback = Some(callback); state.exit_instance(runtime_instance)?; - store.maybe_pop_call_context(call.task)?; + store.maybe_pop_call_context(call.thread.task)?; - instance.handle_callback_code(store, call.task, runtime_instance, code, false)?; + instance.handle_callback_code(store, call.thread, runtime_instance, code, false)?; - store.concurrent_state_mut().guest_task = old_task; - log::trace!("GuestCallKind::DeliverEvent: restored {old_task:?} as current task"); + store.concurrent_state_mut().guest_thread = old_thread; + log::trace!("GuestCallKind::DeliverEvent: restored {old_thread:?} as current thread"); } - GuestCallKind::Start(fun) => { + GuestCallKind::StartImplicit(fun) => { + fun(store)?; + } + GuestCallKind::StartExplicit(fun) => { fun(store)?; } } @@ -887,7 +908,7 @@ impl StoreContextMut<'_, T> { ); assert!(state.high_priority.is_empty()); assert!(state.low_priority.is_empty()); - assert!(state.guest_task.is_none()); + assert!(state.guest_thread.is_none()); assert!(state.futures.get_mut().as_ref().unwrap().is_empty()); assert!( state @@ -1268,11 +1289,11 @@ impl StoreContextMut<'_, T> { if call.is_ready(state)? { self.run_on_worker(WorkerItem::GuestCall(call)).await?; } else { - let task = state.get_mut(call.task)?; + let task = state.get_mut(call.thread.task)?; if !task.starting_sent { task.starting_sent = true; - if let GuestCallKind::Start(_) = &call.kind { - Waitable::Guest(call.task).set_event( + if let GuestCallKind::StartImplicit(_) = &call.kind { + Waitable::Guest(call.thread.task).set_event( state, Some(Event::Subtask { status: Status::Starting, @@ -1281,22 +1302,22 @@ impl StoreContextMut<'_, T> { } } - let runtime_instance = state.get_mut(call.task)?.instance; + let runtime_instance = state.get_mut(call.thread.task)?.instance; state .instance_state(runtime_instance) .pending - .insert(call.task, call.kind); + .insert(call.thread, call.kind); } } WorkItem::Poll(params) => { let state = self.0.concurrent_state_mut(); - if state.get_mut(params.task)?.event.is_some() + if state.get_mut(params.thread.task)?.event.is_some() || !state.get_mut(params.set)?.ready.is_empty() { // There's at least one event immediately available; deliver // it to the guest ASAP. state.push_high_priority(WorkItem::GuestCall(GuestCall { - task: params.task, + thread: params.thread, kind: GuestCallKind::DeliverEvent { instance: params.instance, set: Some(params.set), @@ -1305,9 +1326,9 @@ impl StoreContextMut<'_, T> { } else { // There are no events immediately available; deliver // `Event::None` to the guest. - state.get_mut(params.task)?.event = Some(Event::None); + state.get_mut(params.thread.task)?.event = Some(Event::None); state.push_high_priority(WorkItem::GuestCall(GuestCall { - task: params.task, + thread: params.thread, kind: GuestCallKind::DeliverEvent { instance: params.instance, set: Some(params.set), @@ -1375,17 +1396,21 @@ impl StoreOpaque { /// Resume the specified fiber, giving it exclusive access to the specified /// store. async fn resume_fiber(&mut self, fiber: StoreFiber<'static>) -> Result<()> { - let old_task = self.concurrent_state_mut().guest_task; - log::trace!("resume_fiber: save current task {old_task:?}"); + let old_thread = self.concurrent_state_mut().guest_thread; + log::trace!("resume_fiber: save current thread {old_thread:?}"); let fiber = fiber::resolve_or_release(self, fiber).await?; let state = self.concurrent_state_mut(); - state.guest_task = old_task; - log::trace!("resume_fiber: restore current task {old_task:?}"); + state.guest_thread = old_thread; + if let Some(ref ot) = old_thread { + state.get_mut(ot.thread)?.state = GuestThreadState::Running; + } + log::trace!("resume_fiber: restore current thread {old_thread:?}"); if let Some(mut fiber) = fiber { + log::trace!("resume_fiber: suspend reason {:?}", &state.suspend_reason); // See the `SuspendReason` documentation for what each case means. match state.suspend_reason.take().unwrap() { SuspendReason::NeedWork => { @@ -1395,17 +1420,23 @@ impl StoreOpaque { fiber.dispose(self); } } - SuspendReason::Yielding { .. } => { + SuspendReason::Yielding { thread, .. } => { + state.get_mut(thread.thread)?.state = GuestThreadState::Pending; state.push_low_priority(WorkItem::ResumeFiber(fiber)); } - SuspendReason::Waiting { set, task } => { + SuspendReason::ExplicitlySuspending { thread, .. } => { + state.get_mut(thread.thread)?.state = GuestThreadState::Suspended(fiber); + } + SuspendReason::Waiting { set, thread } => { let old = state .get_mut(set)? .waiting - .insert(task, WaitMode::Fiber(fiber)); + .insert(thread, WaitMode::Fiber(fiber)); assert!(old.is_none()); } - } + }; + } else { + log::trace!("resume_fiber: fiber has exited"); } Ok(()) @@ -1419,17 +1450,19 @@ impl StoreOpaque { fn suspend(&mut self, reason: SuspendReason) -> Result<()> { log::trace!("suspend fiber: {reason:?}"); - // If we're yielding or waiting on behalf of a guest task, we'll need to + // If we're yielding or waiting on behalf of a guest thread, we'll need to // pop the call context which manages resource borrows before suspending // and then push it again once we've resumed. let task = match &reason { - SuspendReason::Yielding { task } | SuspendReason::Waiting { task, .. } => Some(*task), + SuspendReason::Yielding { thread, .. } + | SuspendReason::Waiting { thread, .. } + | SuspendReason::ExplicitlySuspending { thread, .. } => Some(thread.task), SuspendReason::NeedWork => None, }; - let old_guest_task = if let Some(task) = task { + let old_guest_thread = if let Some(task) = task { self.maybe_pop_call_context(task)?; - self.concurrent_state_mut().guest_task + self.concurrent_state_mut().guest_thread } else { None }; @@ -1441,7 +1474,7 @@ impl StoreOpaque { self.with_blocking(|_, cx| cx.suspend(StoreFiberYield::ReleaseStore))?; if let Some(task) = task { - self.concurrent_state_mut().guest_task = old_guest_task; + self.concurrent_state_mut().guest_thread = old_guest_thread; self.maybe_push_call_context(task)?; } @@ -1453,7 +1486,8 @@ impl StoreOpaque { /// itself. fn maybe_push_call_context(&mut self, guest_task: TableId) -> Result<()> { let task = self.concurrent_state_mut().get_mut(guest_task)?; - if task.lift_result.is_some() { + + if !task.returned_or_cancelled() { log::trace!("push call context for {guest_task:?}"); let call_context = task.call_context.take().unwrap(); self.component_resource_state().0.push(call_context); @@ -1465,11 +1499,10 @@ impl StoreOpaque { /// guest task if it has not yet either returned a result or cancelled /// itself. fn maybe_pop_call_context(&mut self, guest_task: TableId) -> Result<()> { - if self + if !self .concurrent_state_mut() .get_mut(guest_task)? - .lift_result - .is_some() + .returned_or_cancelled() { log::trace!("pop call context for {guest_task:?}"); let call_context = Some(self.component_resource_state().0.pop().unwrap()); @@ -1482,11 +1515,14 @@ impl StoreOpaque { fn wait_for_event(&mut self, waitable: Waitable) -> Result<()> { let state = self.concurrent_state_mut(); - let caller = state.guest_task.unwrap(); + let caller = state.guest_thread.unwrap(); let old_set = waitable.common(state)?.set; - let set = state.get_mut(caller)?.sync_call_set; + let set = state.get_mut(caller.task)?.sync_call_set; waitable.join(state, Some(set))?; - self.suspend(SuspendReason::Waiting { set, task: caller })?; + self.suspend(SuspendReason::Waiting { + set, + thread: caller, + })?; let state = self.concurrent_state_mut(); waitable.join(state, old_set) } @@ -1549,26 +1585,21 @@ impl Instance { fn handle_callback_code( self, store: &mut StoreOpaque, - guest_task: TableId, + guest_thread: QualifiedThreadId, runtime_instance: RuntimeComponentInstanceIndex, code: u32, initial_call: bool, ) -> Result<()> { let (code, set) = unpack_callback_code(code); - log::trace!("received callback code from {guest_task:?}: {code} (set: {set})"); + log::trace!("received callback code from {guest_thread:?}: {code} (set: {set})"); let state = store.concurrent_state_mut(); - let task = state.get_mut(guest_task)?; - - if task.lift_result.is_some() { - if code == callback_code::EXIT { - return Err(anyhow!(crate::Trap::NoAsyncResult)); - } + if !state.get_mut(guest_thread.task)?.returned_or_cancelled() { if initial_call { // Notify any current or future waiters that this subtask has // started. - Waitable::Guest(guest_task).set_event( + Waitable::Guest(guest_thread.task).set_event( state, Some(Event::Subtask { status: Status::Started, @@ -1590,15 +1621,17 @@ impl Instance { match code { callback_code::EXIT => { - let task = state.get_mut(guest_task)?; + log::trace!("implicit thread {guest_thread:?} completed"); + self.cleanup_thread(store, guest_thread, runtime_instance)?; + let task = store.concurrent_state_mut().get_mut(guest_thread.task)?; + if task.threads.is_empty() && !task.returned_or_cancelled() { + bail!(Trap::NoAsyncResult); + } match &task.caller { - Caller::Host { - remove_task_automatically, - .. - } => { - if *remove_task_automatically { - log::trace!("handle_callback_code will delete task {guest_task:?}"); - Waitable::Guest(guest_task).delete_from(state)?; + Caller::Host { .. } => { + if task.ready_to_delete() { + Waitable::Guest(guest_thread.task) + .delete_from(store.concurrent_state_mut())?; } } Caller::Guest { .. } => { @@ -1608,13 +1641,13 @@ impl Instance { } } callback_code::YIELD => { - // Push this task onto the "low priority" queue so it runs after - // any other tasks have had a chance to run. - let task = state.get_mut(guest_task)?; + // Push this thread onto the "low priority" queue so it runs after + // any other threads have had a chance to run. + let task = state.get_mut(guest_thread.task)?; assert!(task.event.is_none()); task.event = Some(Event::None); state.push_low_priority(WorkItem::GuestCall(GuestCall { - task: guest_task, + thread: guest_thread, kind: GuestCallKind::DeliverEvent { instance: self, set: None, @@ -1625,12 +1658,12 @@ impl Instance { let set = get_set(store, set)?; let state = store.concurrent_state_mut(); - if state.get_mut(guest_task)?.event.is_some() + if state.get_mut(guest_thread.task)?.event.is_some() || !state.get_mut(set)?.ready.is_empty() { // An event is immediately available; deliver it ASAP. state.push_high_priority(WorkItem::GuestCall(GuestCall { - task: guest_task, + thread: guest_thread, kind: GuestCallKind::DeliverEvent { instance: self, set: Some(set), @@ -1644,7 +1677,7 @@ impl Instance { // event has arrived after that. state.push_low_priority(WorkItem::Poll(PollParams { instance: self, - task: guest_task, + thread: guest_thread, set, })); } @@ -1655,12 +1688,15 @@ impl Instance { // Here we also set `GuestTask::wake_on_cancel` // which allows `subtask.cancel` to interrupt the // wait. - let old = state.get_mut(guest_task)?.wake_on_cancel.replace(set); + let old = state + .get_mut(guest_thread.thread)? + .wake_on_cancel + .replace(set); assert!(old.is_none()); let old = state .get_mut(set)? .waiting - .insert(guest_task, WaitMode::Callback(self)); + .insert(guest_thread, WaitMode::Callback(self)); assert!(old.is_none()); } _ => unreachable!(), @@ -1673,6 +1709,25 @@ impl Instance { Ok(()) } + fn cleanup_thread( + self, + store: &mut StoreOpaque, + guest_thread: QualifiedThreadId, + runtime_instance: RuntimeComponentInstanceIndex, + ) -> Result<()> { + let guest_id = store + .concurrent_state_mut() + .get_mut(guest_thread.thread)? + .instance_rep; + self.id().get_mut(store).guest_tables().0[runtime_instance] + .guest_thread_remove(guest_id.unwrap())?; + + store.concurrent_state_mut().delete(guest_thread.thread)?; + let task = store.concurrent_state_mut().get_mut(guest_thread.task)?; + task.threads.remove(&guest_thread.thread); + Ok(()) + } + /// Add the specified guest call to the "high priority" work item queue, to /// be started as soon as backpressure and/or reentrance rules allow. /// @@ -1682,7 +1737,7 @@ impl Instance { unsafe fn queue_call( self, mut store: StoreContextMut, - guest_task: TableId, + guest_thread: QualifiedThreadId, callee: SendSyncPtr, param_count: usize, result_count: usize, @@ -1707,7 +1762,7 @@ impl Instance { /// the returned closure is called. unsafe fn make_call( store: StoreContextMut, - guest_task: TableId, + guest_thread: QualifiedThreadId, callee: SendSyncPtr, param_count: usize, result_count: usize, @@ -1720,7 +1775,12 @@ impl Instance { let token = StoreToken::new(store); move |store: &mut dyn VMStore| { let mut storage = [MaybeUninit::uninit(); MAX_FLAT_PARAMS]; - let task = store.concurrent_state_mut().get_mut(guest_task)?; + + store + .concurrent_state_mut() + .get_mut(guest_thread.thread)? + .state = GuestThreadState::Running; + let task = store.concurrent_state_mut().get_mut(guest_thread.task)?; let may_enter_after_call = task.call_post_return_automatically(); let lower = task.lower_params.take().unwrap(); @@ -1758,7 +1818,7 @@ impl Instance { let call = unsafe { make_call( store.as_context_mut(), - guest_task, + guest_thread, callee, param_count, result_count, @@ -1766,17 +1826,29 @@ impl Instance { ) }; - let callee_instance = store.0.concurrent_state_mut().get_mut(guest_task)?.instance; + let callee_instance = store + .0 + .concurrent_state_mut() + .get_mut(guest_thread.task)? + .instance; let fun = if callback.is_some() { assert!(async_); Box::new(move |store: &mut dyn VMStore| { - let old_task = store.concurrent_state_mut().guest_task.replace(guest_task); + self.add_guest_thread_to_instance_table( + guest_thread.thread, + store, + callee_instance, + )?; + let old_thread = store + .concurrent_state_mut() + .guest_thread + .replace(guest_thread); log::trace!( - "stackless call: replaced {old_task:?} with {guest_task:?} as current task" + "stackless call: replaced {old_thread:?} with {guest_thread:?} as current thread" ); - store.maybe_push_call_context(guest_task)?; + store.maybe_push_call_context(guest_thread.task)?; store.concurrent_state_mut().enter_instance(callee_instance); @@ -1792,31 +1864,40 @@ impl Instance { .concurrent_state_mut() .exit_instance(callee_instance)?; - store.maybe_pop_call_context(guest_task)?; + store.maybe_pop_call_context(guest_thread.task)?; let state = store.concurrent_state_mut(); - state.guest_task = old_task; - log::trace!("stackless call: restored {old_task:?} as current task"); + state.guest_thread = old_thread; + old_thread + .map(|t| state.get_mut(t.thread).unwrap().state = GuestThreadState::Running); + log::trace!("stackless call: restored {old_thread:?} as current thread"); // SAFETY: `wasmparser` will have validated that the callback // function returns a `i32` result. let code = unsafe { storage[0].assume_init() }.get_i32() as u32; - self.handle_callback_code(store, guest_task, callee_instance, code, true)?; + self.handle_callback_code(store, guest_thread, callee_instance, code, true)?; Ok(()) }) as Box Result<()> + Send + Sync> } else { let token = StoreToken::new(store.as_context_mut()); Box::new(move |store: &mut dyn VMStore| { - let old_task = store.concurrent_state_mut().guest_task.replace(guest_task); + self.add_guest_thread_to_instance_table( + guest_thread.thread, + store, + callee_instance, + )?; + let old_thread = store + .concurrent_state_mut() + .guest_thread + .replace(guest_thread); log::trace!( - "stackful call: replaced {old_task:?} with {guest_task:?} as current task", + "sync/async-stackful call: replaced {old_thread:?} with {guest_thread:?} as current thread", ); - let mut flags = self.id().get(store).instance_flags(callee_instance); - store.maybe_push_call_context(guest_task)?; + store.maybe_push_call_context(guest_thread.task)?; // Unless this is a callback-less (i.e. stackful) // async-lifted export, we need to record that the instance @@ -1833,18 +1914,13 @@ impl Instance { // over must be valid. let storage = call(store)?; + // This is a callback-less call, so the implicit thread has now completed + self.cleanup_thread(store, guest_thread, callee_instance)?; + if async_ { - // This is a callback-less (i.e. stackful) async-lifted - // export, so there is no post-return function, and - // either `task.return` or `task.cancel` should have - // been called. - if store - .concurrent_state_mut() - .get_mut(guest_task)? - .lift_result - .is_some() - { - return Err(anyhow!(crate::Trap::NoAsyncResult)); + let task = store.concurrent_state_mut().get_mut(guest_thread.task)?; + if task.threads.is_empty() && !task.returned_or_cancelled() { + bail!(Trap::NoAsyncResult); } } else { // This is a sync-lifted export, so now is when we lift the @@ -1856,9 +1932,13 @@ impl Instance { let state = store.concurrent_state_mut(); state.exit_instance(callee_instance)?; - assert!(state.get_mut(guest_task)?.result.is_none()); + assert!(state.get_mut(guest_thread.task)?.result.is_none()); - state.get_mut(guest_task)?.lift_result.take().unwrap() + state + .get_mut(guest_thread.task)? + .lift_result + .take() + .unwrap() }; // SAFETY: `result_count` represents the number of core Wasm @@ -1879,7 +1959,7 @@ impl Instance { if store .concurrent_state_mut() - .get_mut(guest_task)? + .get_mut(guest_thread.task)? .call_post_return_automatically() { unsafe { @@ -1912,25 +1992,22 @@ impl Instance { self.task_complete( store, - guest_task, + guest_thread.task, result, Status::Returned, post_return_arg, )?; } - store.maybe_pop_call_context(guest_task)?; + store.maybe_pop_call_context(guest_thread.task)?; - let task = store.concurrent_state_mut().get_mut(guest_task)?; + let state = store.concurrent_state_mut(); + let task = state.get_mut(guest_thread.task)?; match &task.caller { - Caller::Host { - remove_task_automatically, - .. - } => { - if *remove_task_automatically { - Waitable::Guest(guest_task) - .delete_from(store.concurrent_state_mut())?; + Caller::Host { .. } => { + if task.ready_to_delete() { + Waitable::Guest(guest_thread.task).delete_from(state)?; } } Caller::Guest { .. } => { @@ -1946,8 +2023,8 @@ impl Instance { .0 .concurrent_state_mut() .push_high_priority(WorkItem::GuestCall(GuestCall { - task: guest_task, - kind: GuestCallKind::Start(fun), + thread: guest_thread, + kind: GuestCallKind::StartImplicit(fun), })); Ok(()) @@ -2014,7 +2091,7 @@ impl Instance { let return_ = SendSyncPtr::new(NonNull::new(return_).unwrap()); let token = StoreToken::new(store.as_context_mut()); let state = store.0.concurrent_state_mut(); - let old_task = state.guest_task.take(); + let old_thread = state.guest_thread.take(); let new_task = GuestTask::new( state, Box::new(move |store, dst| { @@ -2059,8 +2136,7 @@ impl Instance { } dst.copy_from_slice(&src[..dst.len()]); let state = store.0.concurrent_state_mut(); - let task = state.guest_task.unwrap(); - Waitable::Guest(task).set_event( + Waitable::Guest(state.guest_thread.unwrap().task).set_event( state, Some(Event::Subtask { status: Status::Started, @@ -2091,10 +2167,10 @@ impl Instance { )?; } let state = store.0.concurrent_state_mut(); - let task = state.guest_task.unwrap(); + let thread = state.guest_thread.unwrap(); if sync_caller { - state.get_mut(task)?.sync_result = - Some(if let ResultInfo::Stack { result_count } = &result_info { + state.get_mut(thread.task)?.sync_result = SyncResult::Produced( + if let ResultInfo::Stack { result_count } = &result_info { match result_count { 0 => None, 1 => Some(my_src[0]), @@ -2102,7 +2178,8 @@ impl Instance { } } else { None - }); + }, + ); } Ok(Box::new(DummyResult) as Box) }), @@ -2111,7 +2188,7 @@ impl Instance { string_encoding: StringEncoding::from_u8(string_encoding).unwrap(), }, Caller::Guest { - task: old_task.unwrap(), + thread: old_thread.unwrap(), instance: caller_instance, }, None, @@ -2119,19 +2196,28 @@ impl Instance { )?; let guest_task = state.push(new_task)?; + let new_thread = GuestThread::new_implicit(guest_task); + let guest_thread = state.push(new_thread)?; + state.get_mut(guest_task)?.threads.insert(guest_thread); - if let Some(old_task) = old_task { + let state = store.0.concurrent_state_mut(); + if let Some(old_thread) = old_thread { if !state.may_enter(guest_task) { bail!(crate::Trap::CannotEnterComponent); } - state.get_mut(old_task)?.subtasks.insert(guest_task); + state.get_mut(old_thread.task)?.subtasks.insert(guest_task); }; - // Make the new task the current one so that `Self::start_call` knows + // Make the new thread the current one so that `Self::start_call` knows // which one to start. - state.guest_task = Some(guest_task); - log::trace!("pushed {guest_task:?} as current task; old task was {old_task:?}"); + state.guest_thread = Some(QualifiedThreadId { + task: guest_task, + thread: guest_thread, + }); + log::trace!( + "pushed {guest_task:?}:{guest_thread:?} as current thread; old thread was {old_thread:?}" + ); Ok(()) } @@ -2199,15 +2285,17 @@ impl Instance { let token = StoreToken::new(store.as_context_mut()); let async_caller = storage.is_none(); let state = store.0.concurrent_state_mut(); - let guest_task = state.guest_task.unwrap(); - let may_enter_after_call = state.get_mut(guest_task)?.call_post_return_automatically(); + let guest_thread = state.guest_thread.unwrap(); + let may_enter_after_call = state + .get_mut(guest_thread.task)? + .call_post_return_automatically(); let callee = SendSyncPtr::new(NonNull::new(callee).unwrap()); let param_count = usize::try_from(param_count).unwrap(); assert!(param_count <= MAX_FLAT_PARAMS); let result_count = usize::try_from(result_count).unwrap(); assert!(result_count <= MAX_FLAT_RESULTS); - let task = state.get_mut(guest_task)?; + let task = state.get_mut(guest_thread.task)?; if !callback.is_null() { // We're calling an async-lifted export with a callback, so store // the callback and related context as part of the task so we can @@ -2229,7 +2317,7 @@ impl Instance { } let Caller::Guest { - task: caller, + thread: caller, instance: runtime_instance, } = &task.caller else { @@ -2252,7 +2340,7 @@ impl Instance { unsafe { self.queue_call( store.as_context_mut(), - guest_task, + guest_thread, callee, param_count, result_count, @@ -2267,9 +2355,9 @@ impl Instance { // Use the caller's `GuestTask::sync_call_set` to register interest in // the subtask... - let guest_waitable = Waitable::Guest(guest_task); + let guest_waitable = Waitable::Guest(guest_thread.task); let old_set = guest_waitable.common(state)?.set; - let set = state.get_mut(caller)?.sync_call_set; + let set = state.get_mut(caller.task)?.sync_call_set; guest_waitable.join(state, Some(set))?; // ... and suspend this fiber temporarily while we wait for it to start. @@ -2288,18 +2376,20 @@ impl Instance { // before committing to such an optimization. And again, we'd need to // update the spec to allow that. let (status, waitable) = loop { - store - .0 - .suspend(SuspendReason::Waiting { set, task: caller })?; + store.0.suspend(SuspendReason::Waiting { + set, + thread: caller, + })?; let state = store.0.concurrent_state_mut(); + log::trace!("taking event for {:?}", guest_thread.task); let event = guest_waitable.take_event(state)?; let Some(Event::Subtask { status }) = event else { unreachable!(); }; - log::trace!("status {status:?} for {guest_task:?}"); + log::trace!("status {status:?} for {:?}", guest_thread.task); if status == Status::Returned { // It returned, so we can stop waiting. @@ -2309,11 +2399,11 @@ impl Instance { // async-lowered import, so we generate a handle for the task // waitable and return the status. let handle = self.id().get_mut(store.0).guest_tables().0[caller_instance] - .subtask_insert_guest(guest_task.rep())?; + .subtask_insert_guest(guest_thread.task.rep())?; store .0 .concurrent_state_mut() - .get_mut(guest_task)? + .get_mut(guest_thread.task)? .common .handle = Some(handle); break (status, Some(handle)); @@ -2332,25 +2422,24 @@ impl Instance { // The caller used a sync-lowered import to call an async-lifted // export, in which case the result, if any, has been stashed in // `GuestTask::sync_result`. - let task = state.get_mut(guest_task)?; + let task = state.get_mut(guest_thread.task)?; if let Some(result) = task.sync_result.take() { if let Some(result) = result { storage[0] = MaybeUninit::new(result); } if task.exited { - Waitable::Guest(guest_task).delete_from(state)?; + if task.ready_to_delete() { + Waitable::Guest(guest_thread.task).delete_from(state)?; + } } - } else { - // This means the callee failed to call either `task.return` or - // `task.cancel` before exiting. - return Err(anyhow!(crate::Trap::NoAsyncResult)); } } - // Reset the current task to point to the caller as it resumes control. - state.guest_task = Some(caller); - log::trace!("popped current task {guest_task:?}; new task is {caller:?}"); + // Reset the current thread to point to the caller as it resumes control. + state.guest_thread = Some(caller); + state.get_mut(caller.thread)?.state = GuestThreadState::Running; + log::trace!("popped current thread {guest_thread:?}; new thread is {caller:?}"); Ok(status.pack(waitable)) } @@ -2375,7 +2464,7 @@ impl Instance { ) -> Result> { let token = StoreToken::new(store.as_context_mut()); let state = store.0.concurrent_state_mut(); - let caller = state.guest_task.unwrap(); + let caller = state.guest_thread.unwrap(); // Create an abortable future which hooks calls to poll and manages call // context state for the future. @@ -2509,15 +2598,15 @@ impl Instance { ) -> Result<()> { self.id().get(store).check_may_leave(caller)?; let state = store.concurrent_state_mut(); - let guest_task = state.guest_task.unwrap(); + let guest_thread = state.guest_thread.unwrap(); let lift = state - .get_mut(guest_task)? + .get_mut(guest_thread.task)? .lift_result .take() .ok_or_else(|| { anyhow!("`task.return` or `task.cancel` called more than once for current task") })?; - assert!(state.get_mut(guest_task)?.result.is_none()); + assert!(state.get_mut(guest_thread.task)?.result.is_none()); let CanonicalOptions { string_encoding, @@ -2546,11 +2635,16 @@ impl Instance { bail!("invalid `task.return` signature and/or options for current task"); } - log::trace!("task.return for {guest_task:?}"); + log::trace!("task.return for {guest_thread:?}"); let result = (lift.lift)(store, storage)?; - - self.task_complete(store, guest_task, result, Status::Returned, ValRaw::i32(0)) + self.task_complete( + store, + guest_thread.task, + result, + Status::Returned, + ValRaw::i32(0), + ) } /// Implements the `task.cancel` intrinsic. @@ -2561,8 +2655,8 @@ impl Instance { ) -> Result<()> { self.id().get(store).check_may_leave(caller)?; let state = store.concurrent_state_mut(); - let guest_task = state.guest_task.unwrap(); - let task = state.get_mut(guest_task)?; + let guest_thread = state.guest_thread.unwrap(); + let task = state.get_mut(guest_thread.task)?; if !task.cancel_sent { bail!("`task.cancel` called by task which has not been cancelled") } @@ -2572,11 +2666,11 @@ impl Instance { assert!(task.result.is_none()); - log::trace!("task.cancel for {guest_task:?}"); + log::trace!("task.cancel for {guest_thread:?}"); self.task_complete( store, - guest_task, + guest_thread.task, Box::new(DummyResult), Status::ReturnCancelled, ValRaw::i32(0), @@ -2619,7 +2713,6 @@ impl Instance { .get_mut(guest_task)? .function_index .unwrap(); - self.id() .get_mut(store) .post_return_arg_set(function_index, post_return_arg); @@ -2816,54 +2909,221 @@ impl Instance { ) } - /// Implements the `thread.yield` intrinsic. - pub(crate) fn thread_yield( + /// Implements the `thread.index` intrinsic. + pub(crate) fn thread_index(&self, store: &mut dyn VMStore) -> Result { + let thread_id = store + .concurrent_state_mut() + .guest_thread + .ok_or_else(|| anyhow!("no current thread"))? + .thread; + // The unwrap is safe because `instance_rep` must be `Some` by this point + Ok(store + .concurrent_state_mut() + .get_mut(thread_id)? + .instance_rep + .unwrap()) + } + + /// Implements the `thread.new_indirect` intrinsic. + pub(crate) fn thread_new_indirect( + self, + mut store: StoreContextMut, + runtime_instance: RuntimeComponentInstanceIndex, + _func_ty_idx: TypeFuncIndex, // currently unused + start_func_table_idx: RuntimeTableIndex, + start_func_idx: u32, + context: i32, + ) -> Result { + self.id().get(store.0).check_may_leave(runtime_instance)?; + + log::trace!("creating new thread"); + + let start_func_ty = FuncType::new(store.engine(), [ValType::I32], []); + let instance = self.id().get_mut(store.0); + let callee = instance + .index_runtime_func_table(start_func_table_idx, start_func_idx as u64)? + .ok_or_else(|| { + anyhow!("the start function index points to an uninitialized function") + })?; + if callee.type_index(store.0) != start_func_ty.type_index() { + bail!( + "start function does not match expected type (currently only `(i32) -> ()` is supported)" + ); + } + + let token = StoreToken::new(store.as_context_mut()); + let start_func = Box::new( + move |store: &mut dyn VMStore, guest_thread: QualifiedThreadId| -> Result<()> { + let old_thread = store + .concurrent_state_mut() + .guest_thread + .replace(guest_thread); + log::trace!( + "thread start: replaced {old_thread:?} with {guest_thread:?} as current thread" + ); + + store.maybe_push_call_context(guest_thread.task)?; + + let mut store = token.as_context_mut(store); + let mut params = [ValRaw::i32(context)]; + // Use call_unchecked rather than call or call_async, as we don't want to run the function + // on a separate fiber if we're running in an async store. + unsafe { callee.call_unchecked(store.as_context_mut(), &mut params)? }; + + store.0.maybe_pop_call_context(guest_thread.task)?; + + self.cleanup_thread(store.0, guest_thread, runtime_instance)?; + log::trace!("explicit thread {guest_thread:?} completed"); + let state = store.0.concurrent_state_mut(); + let task = state.get_mut(guest_thread.task)?; + if task.threads.is_empty() && !task.returned_or_cancelled() { + bail!(Trap::NoAsyncResult); + } + state.guest_thread = old_thread; + old_thread + .map(|t| state.get_mut(t.thread).unwrap().state = GuestThreadState::Running); + if state.get_mut(guest_thread.task)?.ready_to_delete() { + Waitable::Guest(guest_thread.task).delete_from(state)?; + } + log::trace!("thread start: restored {old_thread:?} as current thread"); + + Ok(()) + }, + ); + + let state = store.0.concurrent_state_mut(); + let current_thread = state.guest_thread.unwrap(); + let parent_task = current_thread.task; + + let new_thread = GuestThread::new_explicit(parent_task, start_func); + let thread_id = state.push(new_thread)?; + state.get_mut(parent_task)?.threads.insert(thread_id); + + log::trace!("new thread with id {thread_id:?} created"); + + self.add_guest_thread_to_instance_table(thread_id, store.0, runtime_instance) + } + + pub(crate) fn resume_suspended_thread( + self, + store: &mut StoreOpaque, + runtime_instance: RuntimeComponentInstanceIndex, + thread_idx: u32, + high_priority: bool, + ) -> Result<()> { + let thread_id = + GuestThread::from_instance(self.id().get_mut(store), runtime_instance, thread_idx)?; + let state = store.concurrent_state_mut(); + let guest_thread = QualifiedThreadId::qualify(state, thread_id)?; + let thread = state.get_mut(guest_thread.thread)?; + + match mem::replace(&mut thread.state, GuestThreadState::Running) { + GuestThreadState::NotStartedExplicit(start_func) => { + log::trace!("starting thread {guest_thread:?}"); + let guest_call = WorkItem::GuestCall(GuestCall { + thread: guest_thread, + kind: GuestCallKind::StartExplicit(Box::new(move |store| { + start_func(store, guest_thread) + })), + }); + store + .concurrent_state_mut() + .push_work_item(guest_call, high_priority); + } + GuestThreadState::Suspended(fiber) => { + log::trace!("resuming thread {thread_id:?} that was suspended"); + store + .concurrent_state_mut() + .push_work_item(WorkItem::ResumeFiber(fiber), high_priority); + } + _ => { + bail!("cannot resume thread which is not suspended"); + } + } + Ok(()) + } + + fn add_guest_thread_to_instance_table( + self, + thread_id: TableId, + store: &mut StoreOpaque, + runtime_instance: RuntimeComponentInstanceIndex, + ) -> Result { + let guest_id = self.id().get_mut(store).guest_tables().0[runtime_instance] + .guest_thread_insert(thread_id.rep())?; + store + .concurrent_state_mut() + .get_mut(thread_id)? + .instance_rep = Some(guest_id); + Ok(guest_id) + } + + /// Helper function for the `thread.yield`, `thread.yield-to`, `thread.suspend`, + /// and `thread.switch-to` intrinsics. + pub(crate) fn suspension_intrinsic( self, store: &mut StoreOpaque, caller: RuntimeComponentInstanceIndex, cancellable: bool, - ) -> Result { + yielding: bool, + to_thread: Option, + ) -> Result { + // There could be a pending cancellation from a previous uncancellable wait + if cancellable && store.concurrent_state_mut().take_pending_cancellation() { + return Ok(WaitResult::Cancelled); + } + self.id().get(store).check_may_leave(caller)?; - self.waitable_check(store, cancellable, WaitableCheck::Yield) - .map(|_| { - if cancellable { - let state = store.concurrent_state_mut(); - let task = state.guest_task.unwrap(); - if let Some(event) = state.get_mut(task).unwrap().event.take() { - assert!(matches!(event, Event::Cancelled)); - true - } else { - false - } - } else { - false - } - }) + + if let Some(thread) = to_thread { + self.resume_suspended_thread(store, caller, thread, true)?; + } + + let state = store.concurrent_state_mut(); + let guest_thread = state.guest_thread.unwrap(); + let reason = if yielding { + SuspendReason::Yielding { + thread: guest_thread, + } + } else { + SuspendReason::ExplicitlySuspending { + thread: guest_thread, + } + }; + + store.suspend(reason)?; + + if cancellable && store.concurrent_state_mut().take_pending_cancellation() { + Ok(WaitResult::Cancelled) + } else { + Ok(WaitResult::Completed) + } } - /// Helper function for the `waitable-set.wait`, `waitable-set.poll`, and - /// `yield` intrinsics. + /// Helper function for the `waitable-set.wait` and `waitable-set.poll` intrinsics. fn waitable_check( self, store: &mut StoreOpaque, cancellable: bool, check: WaitableCheck, ) -> Result { - let guest_task = store.concurrent_state_mut().guest_task.unwrap(); + let guest_thread = store.concurrent_state_mut().guest_thread.unwrap(); let (wait, set) = match &check { WaitableCheck::Wait(params) => (true, Some(params.set)), WaitableCheck::Poll(params) => (false, Some(params.set)), - WaitableCheck::Yield => (false, None), }; - // First, suspend this fiber, allowing any other tasks to run. - store.suspend(SuspendReason::Yielding { task: guest_task })?; + log::trace!("waitable check for {guest_thread:?}; set {set:?}"); + // First, suspend this fiber, allowing any other threads to run. + store.suspend(SuspendReason::Yielding { + thread: guest_thread, + })?; - log::trace!("waitable check for {guest_task:?}; set {set:?}"); + log::trace!("waitable check for {guest_thread:?}; set {set:?}"); let state = store.concurrent_state_mut(); - let task = state.get_mut(guest_task)?; + let task = state.get_mut(guest_thread.task)?; // If we're waiting, and there are no events immediately available, // suspend the fiber until that changes. @@ -2875,23 +3135,27 @@ impl Instance { && state.get_mut(set)?.ready.is_empty() { if cancellable { - let old = state.get_mut(guest_task)?.wake_on_cancel.replace(set); + let old = state + .get_mut(guest_thread.thread)? + .wake_on_cancel + .replace(set); assert!(old.is_none()); } store.suspend(SuspendReason::Waiting { set, - task: guest_task, + thread: guest_thread, })?; } } - log::trace!("waitable check for {guest_task:?}; set {set:?}, part two"); + log::trace!("waitable check for {guest_thread:?}; set {set:?}, part two"); let result = match check { // Deliver any pending events to the guest and return. WaitableCheck::Wait(params) | WaitableCheck::Poll(params) => { - let event = self.get_event(store, guest_task, Some(params.set), cancellable)?; + let event = + self.get_event(store, guest_thread.task, Some(params.set), cancellable)?; let (ordinal, handle, result) = if wait { let (event, waitable) = event.unwrap(); @@ -2905,7 +3169,8 @@ impl Instance { (ordinal, handle, result) } else { log::trace!( - "no events ready to deliver via waitable-set.poll to {guest_task:?}; set {:?}", + "no events ready to deliver via waitable-set.poll to {:?}; set {:?}", + guest_thread.task, params.set ); let (ordinal, result) = Event::None.parts(); @@ -2921,7 +3186,6 @@ impl Instance { options.memory_mut(store)[ptr + 4..][..4].copy_from_slice(&result.to_le_bytes()); Ok(ordinal) } - WaitableCheck::Yield => Ok(0), }; result @@ -2968,27 +3232,25 @@ impl Instance { return Ok(Status::ReturnCancelled as u32); } } else { - let caller = concurrent_state.guest_task.unwrap(); + let caller = concurrent_state.guest_thread.unwrap(); let guest_task = TableId::::new(rep); let task = concurrent_state.get_mut(guest_task)?; - if task.lower_params.is_some() { + if !task.already_lowered_parameters() { task.lower_params = None; task.lift_result = None; // Not yet started; cancel and remove from pending let callee_instance = task.instance; - let kind = concurrent_state - .instance_state(callee_instance) - .pending - .remove(&guest_task); - - if kind.is_none() { + let pending = &mut concurrent_state.instance_state(callee_instance).pending; + let pending_count = pending.len(); + pending.retain(|thread, _| thread.task != guest_task); + // If there were no pending threads for this task, we're in an error state + if pending.len() == pending_count { bail!("`subtask.cancel` called after terminal status delivered"); } - return Ok(Status::StartCancelled as u32); - } else if task.lift_result.is_some() { + } else if !task.returned_or_cancelled() { // Started, but not yet returned or cancelled; send the // `CANCELLED` event task.cancel_sent = true; @@ -2997,32 +3259,42 @@ impl Instance { // `Event::Cancelled` if it was already cancelled), but that's // okay -- this should supersede the previous state. task.event = Some(Event::Cancelled); - if let Some(set) = task.wake_on_cancel.take() { - let item = match concurrent_state - .get_mut(set)? - .waiting - .remove(&guest_task) + for thread in task.threads.clone() { + let thread = QualifiedThreadId { + task: guest_task, + thread, + }; + if let Some(set) = concurrent_state + .get_mut(thread.thread) .unwrap() + .wake_on_cancel + .take() { - WaitMode::Fiber(fiber) => WorkItem::ResumeFiber(fiber), - WaitMode::Callback(instance) => WorkItem::GuestCall(GuestCall { - task: guest_task, - kind: GuestCallKind::DeliverEvent { - instance, - set: None, - }, - }), - }; - concurrent_state.push_high_priority(item); + let item = match concurrent_state + .get_mut(set)? + .waiting + .remove(&thread) + .unwrap() + { + WaitMode::Fiber(fiber) => WorkItem::ResumeFiber(fiber), + WaitMode::Callback(instance) => WorkItem::GuestCall(GuestCall { + thread, + kind: GuestCallKind::DeliverEvent { + instance, + set: None, + }, + }), + }; + concurrent_state.push_high_priority(item); - store.suspend(SuspendReason::Yielding { task: caller })?; + store.suspend(SuspendReason::Yielding { thread: caller })?; + break; + } } let concurrent_state = store.concurrent_state_mut(); let task = concurrent_state.get_mut(guest_task)?; - if task.lift_result.is_some() { - // Still not yet returned or cancelled; if `async_`, return - // `BLOCKED`; otherwise wait + if !task.returned_or_cancelled() { if async_ { return Ok(BLOCKED); } else { @@ -3222,6 +3494,17 @@ pub trait VMComponentAsyncStore { err_ctx_handle: u32, debug_msg_address: u32, ) -> Result<()>; + + /// The `thread.new_indirect` intrinsic + fn thread_new_indirect( + &mut self, + instance: Instance, + caller: RuntimeComponentInstanceIndex, + func_ty_idx: TypeFuncIndex, + start_func_table_idx: RuntimeTableIndex, + start_func_idx: u32, + context: i32, + ) -> Result; } /// SAFETY: See trait docs. @@ -3517,6 +3800,25 @@ impl VMComponentAsyncStore for StoreInner { debug_msg_address, ) } + + fn thread_new_indirect( + &mut self, + instance: Instance, + caller: RuntimeComponentInstanceIndex, + func_ty_idx: TypeFuncIndex, + start_func_table_idx: RuntimeTableIndex, + start_func_idx: u32, + context: i32, + ) -> Result { + instance.thread_new_indirect( + StoreContextMut(self), + caller, + func_ty_idx, + start_func_table_idx, + start_func_idx, + context, + ) + } } type HostTaskFuture = Pin> + Send + 'static>>; @@ -3567,16 +3869,16 @@ enum Caller { /// dropping it when the refcount goes to zero is sufficient to notify /// the receiver. exit_tx: Arc>, - /// If true, remove the task from the concurrent state that owns it - /// automatically after it completes. - remove_task_automatically: bool, + /// If true, there's a host future that must be dropped before the task + /// can be deleted. + host_future_present: bool, /// If true, call `post-return` function (if any) automatically. call_post_return_automatically: bool, }, - /// Another guest task called the guest task + /// Another guest thread called the guest task Guest { /// The id of the caller - task: TableId, + thread: QualifiedThreadId, /// The instance to use to enforce reentrance rules. /// /// Note that this might not be the same as the instance the caller task @@ -3595,8 +3897,134 @@ struct LiftResult { string_encoding: StringEncoding, } +/// The table ID for a guest thread, qualified by the task to which it belongs. +/// +/// This exists to minimize table lookups and the necessity to pass stores around mutably +/// for the common case of identifying the task to which a thread belongs. +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +struct QualifiedThreadId { + task: TableId, + thread: TableId, +} + +impl QualifiedThreadId { + fn qualify( + state: &mut ConcurrentState, + thread: TableId, + ) -> Result { + Ok(QualifiedThreadId { + task: state.get_mut(thread)?.parent_task, + thread, + }) + } +} + +impl fmt::Debug for QualifiedThreadId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("QualifiedThreadId") + .field(&self.task.rep()) + .field(&self.thread.rep()) + .finish() + } +} + +enum GuestThreadState { + NotStartedImplicit, + NotStartedExplicit( + Box Result<()> + Send + Sync>, + ), + Running, + Suspended(StoreFiber<'static>), + Pending, + Completed, +} +pub struct GuestThread { + /// Context-local state used to implement the `context.{get,set}` + /// intrinsics. + context: [u32; 2], + /// The owning guest task. + parent_task: TableId, + /// If present, indicates that the thread is currently waiting on the + /// specified set but may be cancelled and woken immediately. + wake_on_cancel: Option>, + /// The execution state of this guest thread + state: GuestThreadState, + /// The index of this thread in the component instance's handle table. + /// This must always be `Some` after initialization. + instance_rep: Option, +} + +impl GuestThread { + /// Retrieve the `GuestThread` corresponding to the specified guest-visible + /// handle. + fn from_instance( + state: Pin<&mut ComponentInstance>, + caller_instance: RuntimeComponentInstanceIndex, + guest_thread: u32, + ) -> Result> { + let rep = state.guest_tables().0[caller_instance].guest_thread_rep(guest_thread)?; + Ok(TableId::new(rep)) + } + + fn new_implicit(parent_task: TableId) -> Self { + Self { + context: [0; 2], + parent_task, + wake_on_cancel: None, + state: GuestThreadState::NotStartedImplicit, + instance_rep: None, + } + } + + fn new_explicit( + parent_task: TableId, + start_func: Box< + dyn FnOnce(&mut dyn VMStore, QualifiedThreadId) -> Result<()> + Send + Sync, + >, + ) -> Self { + Self { + context: [0; 2], + parent_task, + wake_on_cancel: None, + state: GuestThreadState::NotStartedExplicit(start_func), + instance_rep: None, + } + } +} + +impl TableDebug for GuestThread { + fn type_name() -> &'static str { + "GuestThread" + } +} + +enum SyncResult { + NotProduced, + Produced(Option), + Taken, +} + +impl SyncResult { + fn take(&mut self) -> Option> { + match mem::replace(self, SyncResult::Taken) { + SyncResult::NotProduced => None, + SyncResult::Produced(val) => Some(val), + SyncResult::Taken => { + panic!("attempted to take a synchronous result that was already taken") + } + } + } +} + +#[derive(Debug)] +enum HostFutureState { + NotApplicable, + Live, + Dropped, +} + /// Represents a pending guest task. -struct GuestTask { +pub(crate) struct GuestTask { /// See `WaitableCommon` common: WaitableCommon, /// Closure to lower the parameters passed to this task. @@ -3616,16 +4044,13 @@ struct GuestTask { call_context: Option, /// A place to stash the lowered result for a sync-to-async call until it /// can be returned to the caller. - sync_result: Option>, + sync_result: SyncResult, /// Whether or not the task has been cancelled (i.e. whether the task is /// permitted to call `task.cancel`). cancel_sent: bool, /// Whether or not we've sent a `Status::Starting` event to any current or /// future waiters for this waitable. starting_sent: bool, - /// Context-local state used to implement the `context.{get,set}` - /// intrinsics. - context: [u32; 2], /// Pending guest subtasks created by this task (directly or indirectly). /// /// This is used to re-parent subtasks which are still running when their @@ -3642,16 +4067,48 @@ struct GuestTask { /// If present, a pending `Event::None` or `Event::Cancelled` to be /// delivered to this task. event: Option, - /// If present, indicates that the task is currently waiting on the - /// specified set but may be cancelled and woken immediately. - wake_on_cancel: Option>, /// The `ExportIndex` of the guest function being called, if known. function_index: Option, /// Whether or not the task has exited. exited: bool, + /// Threads belonging to this task + threads: HashSet>, + /// The state of the host future that represents an async task, which must + /// be dropped before we can delete the task. + host_future_state: HostFutureState, } impl GuestTask { + fn already_lowered_parameters(&self) -> bool { + // We reset `lower_params` after we lower the parameters + self.lower_params.is_none() + } + fn returned_or_cancelled(&self) -> bool { + // We reset `lift_result` after we return or exit + self.lift_result.is_none() + } + fn ready_to_delete(&self) -> bool { + let threads_completed = self.threads.is_empty(); + let has_sync_result = matches!(self.sync_result, SyncResult::Produced(_)); + let pending_completion_event = matches!( + self.common.event, + Some(Event::Subtask { + status: Status::Returned | Status::ReturnCancelled + }) + ); + let ready = threads_completed + && !has_sync_result + && !pending_completion_event + && !matches!(self.host_future_state, HostFutureState::Live); + log::trace!( + "ready to delete? {ready} (threads_completed: {}, has_sync_result: {}, pending_completion_event: {}, host_future_state: {:?})", + threads_completed, + has_sync_result, + pending_completion_event, + self.host_future_state + ); + ready + } fn new( state: &mut ConcurrentState, lower_params: RawLower, @@ -3661,7 +4118,19 @@ impl GuestTask { component_instance: RuntimeComponentInstanceIndex, ) -> Result { let sync_call_set = state.push(WaitableSet::default())?; - + let host_future_state = match &caller { + Caller::Guest { .. } => HostFutureState::NotApplicable, + Caller::Host { + host_future_present, + .. + } => { + if *host_future_present { + HostFutureState::Live + } else { + HostFutureState::NotApplicable + } + } + }; Ok(Self { common: WaitableCommon::default(), lower_params: Some(lower_params), @@ -3670,17 +4139,17 @@ impl GuestTask { callback, caller, call_context: Some(CallContext::default()), - sync_result: None, + sync_result: SyncResult::NotProduced, cancel_sent: false, starting_sent: false, - context: [0u32; 2], subtasks: HashSet::new(), sync_call_set, instance: component_instance, event: None, - wake_on_cancel: None, function_index: None, exited: false, + threads: HashSet::new(), + host_future_state, }) } @@ -3698,15 +4167,17 @@ impl GuestTask { } } + assert!(self.threads.is_empty()); + state.delete(self.sync_call_set)?; // Reparent any pending subtasks to the caller. match &self.caller { Caller::Guest { - task, + thread, instance: runtime_instance, } => { - let task_mut = state.get_mut(*task)?; + let task_mut = state.get_mut(thread.task)?; let present = task_mut.subtasks.remove(&me); assert!(present); @@ -3716,7 +4187,7 @@ impl GuestTask { for subtask in &self.subtasks { state.get_mut(*subtask)?.caller = Caller::Guest { - task: *task, + thread: *thread, instance: *runtime_instance, }; } @@ -3729,7 +4200,7 @@ impl GuestTask { // once all transitive subtasks of the host call have // exited: exit_tx: exit_tx.clone(), - remove_task_automatically: true, + host_future_present: false, call_post_return_automatically: true, }; } @@ -3871,6 +4342,7 @@ impl Waitable { if let Some(set) = self.common(state)?.set { state.get_mut(set)?.ready.remove(self); } + Ok(event) } @@ -3880,14 +4352,14 @@ impl Waitable { fn mark_ready(&self, state: &mut ConcurrentState) -> Result<()> { if let Some(set) = self.common(state)?.set { state.get_mut(set)?.ready.insert(*self); - if let Some((task, mode)) = state.get_mut(set)?.waiting.pop_first() { - let wake_on_cancel = state.get_mut(task)?.wake_on_cancel.take(); + if let Some((thread, mode)) = state.get_mut(set)?.waiting.pop_first() { + let wake_on_cancel = state.get_mut(thread.thread)?.wake_on_cancel.take(); assert!(wake_on_cancel.is_none() || wake_on_cancel == Some(set)); let item = match mode { WaitMode::Fiber(fiber) => WorkItem::ResumeFiber(fiber), WaitMode::Callback(instance) => WorkItem::GuestCall(GuestCall { - task, + thread, kind: GuestCallKind::DeliverEvent { instance, set: Some(set), @@ -3984,8 +4456,8 @@ impl fmt::Debug for Waitable { struct WaitableSet { /// Which waitables in this set have pending events, if any. ready: BTreeSet, - /// Which guest tasks are currently waiting on this set, if any. - waiting: BTreeMap, WaitMode>, + /// Which guest threads are currently waiting on this set, if any. + waiting: BTreeMap, } impl TableDebug for WaitableSet { @@ -4021,13 +4493,14 @@ struct InstanceState { do_not_enter: bool, /// Pending calls for this instance which require `Self::backpressure` to be /// `true` and/or `Self::do_not_enter` to be false before they can proceed. - pending: BTreeMap, GuestCallKind>, + pending: BTreeMap, } /// Represents the Component Model Async state of a store. pub struct ConcurrentState { - /// The currently running guest task, if any. - guest_task: Option>, + /// The currently running guest thread, if any. + guest_thread: Option, + /// The set of pending host and background tasks, if any. /// /// See `ComponentInstance::poll_until` for where we temporarily take this @@ -4041,9 +4514,9 @@ pub struct ConcurrentState { /// populated as needed. // TODO: this can and should be a `PrimaryMap` instance_states: HashMap, - /// The "high priority" work queue for this instance's event loop. + /// The "high priority" work queue for this store's event loop. high_priority: Vec, - /// The "high priority" work queue for this instance's event loop. + /// The "low priority" work queue for this store's event loop. low_priority: Vec, /// A place to stash the reason a fiber is suspending so that the code which /// resumed it will know under what conditions the fiber should be resumed @@ -4075,7 +4548,7 @@ pub struct ConcurrentState { impl Default for ConcurrentState { fn default() -> Self { Self { - guest_task: None, + guest_thread: None, table: AlwaysMut::new(ResourceTable::new()), futures: AlwaysMut::new(Some(FuturesUnordered::new())), instance_states: HashMap::new(), @@ -4118,6 +4591,12 @@ impl ConcurrentState { fibers.push(fiber); } } + } else if let Some(thread) = entry.downcast_mut::() { + if let GuestThreadState::Suspended(fiber) = + mem::replace(&mut thread.state, GuestThreadState::Completed) + { + fibers.push(fiber); + } } } @@ -4210,6 +4689,14 @@ impl ConcurrentState { self.low_priority.push(item); } + fn push_work_item(&mut self, item: WorkItem, high_priority: bool) { + if high_priority { + self.push_high_priority(item); + } else { + self.push_low_priority(item); + } + } + /// Determine whether the instance associated with the specified guest task /// may be entered (i.e. is not already on the async call stack). /// @@ -4230,16 +4717,17 @@ impl ConcurrentState { // that task or an ancestor of that task, in which case this would be a // constant time check. loop { - match &self.get_mut(guest_task).unwrap().caller { + let next_thread = match &self.get_mut(guest_task).unwrap().caller { Caller::Host { .. } => break true, - Caller::Guest { task, instance } => { + Caller::Guest { thread, instance } => { if *instance == guest_instance { break false; } else { - guest_task = *task; + *thread } } - } + }; + guest_task = next_thread.task; } } @@ -4263,14 +4751,14 @@ impl ConcurrentState { /// /// See `GuestCall::is_ready` for details. fn partition_pending(&mut self, instance: RuntimeComponentInstanceIndex) -> Result<()> { - for (task, kind) in mem::take(&mut self.instance_state(instance).pending).into_iter() { - let call = GuestCall { task, kind }; + for (thread, kind) in mem::take(&mut self.instance_state(instance).pending).into_iter() { + let call = GuestCall { thread, kind }; if call.is_ready(self)? { self.push_high_priority(WorkItem::GuestCall(call)); } else { self.instance_state(instance) .pending - .insert(call.task, call.kind); + .insert(call.thread, call.kind); } } @@ -4299,19 +4787,31 @@ impl ConcurrentState { /// Implements the `context.get` intrinsic. pub(crate) fn context_get(&mut self, slot: u32) -> Result { - let task = self.guest_task.unwrap(); - let val = self.get_mut(task)?.context[usize::try_from(slot).unwrap()]; - log::trace!("context_get {task:?} slot {slot} val {val:#x}"); + let thread = self.guest_thread.unwrap(); + let val = self.get_mut(thread.thread)?.context[usize::try_from(slot).unwrap()]; + log::trace!("context_get {thread:?} slot {slot} val {val:#x}"); Ok(val) } /// Implements the `context.set` intrinsic. pub(crate) fn context_set(&mut self, slot: u32, val: u32) -> Result<()> { - let task = self.guest_task.unwrap(); - log::trace!("context_set {task:?} slot {slot} val {val:#x}"); - self.get_mut(task)?.context[usize::try_from(slot).unwrap()] = val; + let thread = self.guest_thread.unwrap(); + log::trace!("context_set {thread:?} slot {slot} val {val:#x}"); + self.get_mut(thread.thread)?.context[usize::try_from(slot).unwrap()] = val; Ok(()) } + + /// Returns whether there's a pending cancellation on the current guest thread, + /// consuming the event if so. + fn take_pending_cancellation(&mut self) -> bool { + let thread = self.guest_thread.unwrap(); + if let Some(event) = self.get_mut(thread.task).unwrap().event.take() { + assert!(matches!(event, Event::Cancelled)); + true + } else { + false + } + } } /// Provide a type hint to compiler about the shape of a parameter lower @@ -4393,15 +4893,14 @@ struct WaitableCheckParams { enum WaitableCheck { Wait(WaitableCheckParams), Poll(WaitableCheckParams), - Yield, } /// Represents a guest task called from the host, prepared using `prepare_call`. pub(crate) struct PreparedCall { /// The guest export to be called handle: Func, - /// The guest task created by `prepare_call` - task: TableId, + /// The guest thread created by `prepare_call` + thread: QualifiedThreadId, /// The number of lowered core Wasm parameters to pass to the call. param_count: usize, /// The `oneshot::Receiver` to which the result of the call will be @@ -4416,7 +4915,9 @@ pub(crate) struct PreparedCall { impl PreparedCall { /// Get a copy of the `TaskId` for this `PreparedCall`. pub(crate) fn task_id(&self) -> TaskId { - TaskId { task: self.task } + TaskId { + task: self.thread.task, + } } } @@ -4426,17 +4927,22 @@ pub(crate) struct TaskId { } impl TaskId { - /// Remove the specified task from the concurrent state to which it belongs. - /// - /// This must be used with care to avoid use-after-delete or double-delete - /// bugs. Specifically, it should only be called on tasks created with the - /// `remove_task_automatically` parameter to `prepare_call` set to `false`, - /// which tells the runtime that the caller is responsible for removing the - /// task from the state; otherwise, it will be removed automatically. Also, - /// it should only be called once for a given task, and only after either - /// the task has completed or the instance has trapped. - pub(crate) fn remove(&self, store: StoreContextMut) -> Result<()> { - Waitable::Guest(self.task).delete_from(store.0.concurrent_state_mut()) + /// The host future for an async task was dropped. If the parameters have not been lowered yet, + /// it is no longer valid to do so, as the lowering closure would see a dangling pointer. In this case, + /// we delete the task eagerly. Otherwise, there may be running threads, or ones that are suspended + /// and can be resumed by other tasks for this component, so we mark the future as dropped + /// and delete the task when all threads are done. + pub(crate) fn host_future_dropped(&self, store: StoreContextMut) -> Result<()> { + let task = store.0.concurrent_state_mut().get_mut(self.task)?; + if !task.already_lowered_parameters() { + Waitable::Guest(self.task).delete_from(store.0.concurrent_state_mut())? + } else { + task.host_future_state = HostFutureState::Dropped; + if task.ready_to_delete() { + Waitable::Guest(self.task).delete_from(store.0.concurrent_state_mut())? + } + } + Ok(()) } } @@ -4449,7 +4955,7 @@ pub(crate) fn prepare_call( mut store: StoreContextMut, handle: Func, param_count: usize, - remove_task_automatically: bool, + host_future_present: bool, call_post_return_automatically: bool, lower_params: impl FnOnce(Func, StoreContextMut, &mut [MaybeUninit]) -> Result<()> + Send @@ -4471,7 +4977,7 @@ pub(crate) fn prepare_call( let token = StoreToken::new(store.as_context_mut()); let state = store.0.concurrent_state_mut(); - assert!(state.guest_task.is_none()); + assert!(state.guest_thread.is_none()); let (tx, rx) = oneshot::channel(); let (exit_tx, exit_rx) = oneshot::channel(); @@ -4492,7 +4998,7 @@ pub(crate) fn prepare_call( Caller::Host { tx: Some(tx), exit_tx: Arc::new(exit_tx), - remove_task_automatically, + host_future_present, call_post_return_automatically, }, callback.map(|callback| { @@ -4521,10 +5027,12 @@ pub(crate) fn prepare_call( task.function_index = Some(handle.index()); let task = state.push(task)?; + let thread = state.push(GuestThread::new_implicit(task))?; + state.get_mut(task)?.threads.insert(thread); Ok(PreparedCall { handle, - task, + thread: QualifiedThreadId { task, thread }, param_count, rx, exit_rx, @@ -4544,14 +5052,14 @@ pub(crate) fn queue_call( ) -> Result)>> + Send + 'static + use> { let PreparedCall { handle, - task, + thread, param_count, rx, exit_rx, .. } = prepared; - queue_call0(store.as_context_mut(), handle, task, param_count)?; + queue_call0(store.as_context_mut(), handle, thread, param_count)?; Ok(checked( store.0.id(), @@ -4568,7 +5076,7 @@ pub(crate) fn queue_call( fn queue_call0( store: StoreContextMut, handle: Func, - guest_task: TableId, + guest_thread: QualifiedThreadId, param_count: usize, ) -> Result<()> { let (options, flags, _ty, raw_options) = handle.abi_info(store.0); @@ -4578,7 +5086,7 @@ fn queue_call0( let callback = options.callback(); let post_return = handle.post_return_core_func(store.0); - log::trace!("queueing call {guest_task:?}"); + log::trace!("queueing call {guest_thread:?}"); let instance_flags = if callback.is_none() { None @@ -4592,7 +5100,7 @@ fn queue_call0( unsafe { instance.queue_call( store, - guest_task, + guest_thread, SendSyncPtr::new(callee), param_count, 1, diff --git a/crates/wasmtime/src/runtime/component/func.rs b/crates/wasmtime/src/runtime/component/func.rs index 32823dc28903..28a602c75efd 100644 --- a/crates/wasmtime/src/runtime/component/func.rs +++ b/crates/wasmtime/src/runtime/component/func.rs @@ -444,7 +444,7 @@ impl Func { store, self, MAX_FLAT_PARAMS, - true, + false, call_post_return_automatically, move |func, store, params_out| { func.with_lower_context(store, call_post_return_automatically, |cx, ty| { diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs index 590491b503f3..8970c0c3d4c7 100644 --- a/crates/wasmtime/src/runtime/component/func/typed.rs +++ b/crates/wasmtime/src/runtime/component/func/typed.rs @@ -193,7 +193,7 @@ where let ptr = SendSyncPtr::from(NonNull::from(¶ms).cast::()); let prepared = - self.prepare_call(store.as_context_mut(), false, false, move |cx, ty, dst| { + self.prepare_call(store.as_context_mut(), true, false, move |cx, ty, dst| { // SAFETY: The goal here is to get `Params`, a non-`'static` // value, to live long enough to the lowering of the // parameters. We're guaranteed that `Params` lives in the @@ -205,28 +205,31 @@ where // through a `SendSyncPtr` to thwart the `'static` check // of rustc and the signature of `prepare_call`. // - // Note the use of `RemoveOnDrop` in the code that follows + // Note the use of `SignalOnDrop` in the code that follows // this closure, which ensures that the task will be removed // from the concurrent state to which it belongs when the - // containing `Future` is dropped, thereby ensuring that - // this closure will never be called if it hasn't already, - // meaning it will never see a dangling pointer. + // containing `Future` is dropped, so long as the parameters + // have not yet been lowered. Since this closure is removed from + // the task after the parameters are lowered, it will never be called + // after the containing `Future` is dropped. let params = unsafe { ptr.cast::().as_ref() }; Self::lower_args(cx, ty, dst, params) })?; - struct RemoveOnDrop<'a, T: 'static> { + struct SignalOnDrop<'a, T: 'static> { store: StoreContextMut<'a, T>, task: TaskId, } - impl<'a, T> Drop for RemoveOnDrop<'a, T> { + impl<'a, T> Drop for SignalOnDrop<'a, T> { fn drop(&mut self) { - self.task.remove(self.store.as_context_mut()).unwrap(); + self.task + .host_future_dropped(self.store.as_context_mut()) + .unwrap(); } } - let mut wrapper = RemoveOnDrop { + let mut wrapper = SignalOnDrop { store, task: prepared.task_id(), }; @@ -289,7 +292,7 @@ where ); let prepared = - self.prepare_call(store.as_context_mut(), true, true, move |cx, ty, dst| { + self.prepare_call(store.as_context_mut(), false, true, move |cx, ty, dst| { Self::lower_args(cx, ty, dst, ¶ms) })?; concurrent::queue_call(store, prepared) @@ -326,7 +329,7 @@ where fn prepare_call( self, store: StoreContextMut<'_, T>, - remove_task_automatically: bool, + host_future_present: bool, call_post_return_automatically: bool, lower: impl FnOnce( &mut LowerContext, @@ -356,7 +359,7 @@ where store, self.func, param_count, - remove_task_automatically, + host_future_present, call_post_return_automatically, move |func, store, params_out| { func.with_lower_context(store, call_post_return_automatically, |cx, ty| { diff --git a/crates/wasmtime/src/runtime/vm/component.rs b/crates/wasmtime/src/runtime/vm/component.rs index 079db57bd963..70a8eee279bd 100644 --- a/crates/wasmtime/src/runtime/vm/component.rs +++ b/crates/wasmtime/src/runtime/vm/component.rs @@ -16,8 +16,10 @@ use crate::runtime::vm::{ ValRaw, VmPtr, VmSafe, catch_unwind_and_record_trap, }; use crate::store::InstanceId; +use crate::{Func, vm}; use alloc::alloc::Layout; use alloc::sync::Arc; +use anyhow::Result; use core::mem; use core::mem::offset_of; use core::pin::Pin; @@ -357,6 +359,30 @@ impl ComponentInstance { } } + /// Returns the `Func` at index `func_idx` in the funcref table at `table_idx`. + pub fn index_runtime_func_table( + &self, + table_idx: RuntimeTableIndex, + func_idx: u64, + ) -> Result> { + unsafe { + let store = self.store.0.as_ref(); + let table = self.runtime_table(table_idx); + let vmctx = table.vmctx.as_non_null(); + // SAFETY: it's a contract of this function that `vmctx` is a valid + // allocation which can go backwards to a `ComponentInstance`. + let mut instance_ptr = vm::Instance::from_vmctx(vmctx); + // SAFETY: We just constructed `instance_ptr` from a valid pointer. This pointer won't leave + // this call, so we don't need a lifetime to bind it to. + let instance = Pin::new_unchecked(instance_ptr.as_mut()); + let table = instance.get_defined_table_with_lazy_init(table.index, [func_idx]); + let func = table + .get_func(func_idx)? + .map(|funcref| Func::from_vm_func_ref(store.id(), funcref)); + Ok(func) + } + } + /// Returns the realloc pointer corresponding to the index provided. /// /// This can only be called after `idx` has been initialized at runtime @@ -725,8 +751,14 @@ impl ComponentInstance { let i = RuntimeTableIndex::from_u32(i); let offset = self.offsets.runtime_table(i); // SAFETY: see above + #[allow(clippy::cast_possible_truncation, reason = "known to not overflow")] unsafe { - *self.as_mut().vmctx_plus_offset_mut(offset) = INVALID_PTR; + *self.as_mut().vmctx_plus_offset_mut::( + offset + offset_of!(VMTableImport, from) as u32, + ) = INVALID_PTR; + *self.as_mut().vmctx_plus_offset_mut::( + offset + offset_of!(VMTableImport, vmctx) as u32, + ) = INVALID_PTR; } } } diff --git a/crates/wasmtime/src/runtime/vm/component/handle_table.rs b/crates/wasmtime/src/runtime/vm/component/handle_table.rs index c550e2fde871..60ce64793c30 100644 --- a/crates/wasmtime/src/runtime/vm/component/handle_table.rs +++ b/crates/wasmtime/src/runtime/vm/component/handle_table.rs @@ -46,6 +46,7 @@ pub enum Waitable { Stream, } +#[derive(Debug)] enum Slot { Free { next: u32, @@ -80,6 +81,11 @@ enum Slot { rep: u32, }, + /// Represents a guest thread handle. + GuestThread { + rep: u32, + }, + /// Represents a stream handle. Stream { ty: TypeStreamTableIndex, @@ -565,4 +571,30 @@ impl HandleTable { _ => bail!("handle is not a waitable"), } } + + /// Inserts the guest thread `rep` into this table, returning the index it + /// now resides at. + pub fn guest_thread_insert(&mut self, rep: u32) -> Result { + self.insert(Slot::GuestThread { rep }) + } + + /// Returns the `rep` of a guest thread pointed to by `idx`. + pub fn guest_thread_rep(&mut self, idx: u32) -> Result { + match self.get_mut(idx)? { + Slot::GuestThread { rep } => Ok(*rep), + _ => bail!("handle is not a guest thread"), + } + } + + /// Removes the guest thread pointed to by `idx`. + /// + /// Returns the internal `rep`. + pub fn guest_thread_remove(&mut self, idx: u32) -> Result { + let rep = match self.get_mut(idx)? { + Slot::GuestThread { rep } => *rep, + _ => bail!("handle is not a guest thread"), + }; + self.remove(idx)?; + Ok(rep) + } } diff --git a/crates/wasmtime/src/runtime/vm/component/libcalls.rs b/crates/wasmtime/src/runtime/vm/component/libcalls.rs index 08563dea82e2..5df911dfb6f8 100644 --- a/crates/wasmtime/src/runtime/vm/component/libcalls.rs +++ b/crates/wasmtime/src/runtime/vm/component/libcalls.rs @@ -1,6 +1,8 @@ //! Implementation of string transcoding required by the component model. use crate::component::Instance; +#[cfg(feature = "component-model-async")] +use crate::component::concurrent::WaitResult; use crate::prelude::*; #[cfg(feature = "component-model-async")] use crate::runtime::component::concurrent::ResourcePair; @@ -811,11 +813,15 @@ fn thread_yield( caller_instance: u32, cancellable: u8, ) -> Result { - instance.thread_yield( - store, - RuntimeComponentInstanceIndex::from_u32(caller_instance), - cancellable != 0, - ) + instance + .suspension_intrinsic( + store, + RuntimeComponentInstanceIndex::from_u32(caller_instance), + cancellable != 0, + true, + None, + ) + .map(|r| r == WaitResult::Cancelled) } #[cfg(feature = "component-model-async")] @@ -1367,3 +1373,99 @@ fn context_set( val, ) } + +#[cfg(feature = "component-model-async")] +fn thread_index(store: &mut dyn VMStore, instance: Instance) -> Result { + instance.thread_index(store) +} + +#[cfg(feature = "component-model-async")] +fn thread_new_indirect( + store: &mut dyn VMStore, + instance: Instance, + caller: u32, + func_ty_id: u32, + func_table_idx: u32, + func_idx: u32, + context: u32, +) -> Result { + store.component_async_store().thread_new_indirect( + instance, + RuntimeComponentInstanceIndex::from_u32(caller), + TypeFuncIndex::from_u32(func_ty_id), + RuntimeTableIndex::from_u32(func_table_idx), + func_idx, + context as i32, + ) +} + +#[cfg(feature = "component-model-async")] +fn thread_switch_to( + store: &mut dyn VMStore, + instance: Instance, + caller: u32, + cancellable: u8, + thread_idx: u32, +) -> Result { + instance + .suspension_intrinsic( + store, + RuntimeComponentInstanceIndex::from_u32(caller), + cancellable != 0, + false, + Some(thread_idx), + ) + .map(|r| r == WaitResult::Cancelled) +} + +#[cfg(feature = "component-model-async")] +fn thread_suspend( + store: &mut dyn VMStore, + instance: Instance, + caller: u32, + cancellable: u8, +) -> Result { + instance + .suspension_intrinsic( + store, + RuntimeComponentInstanceIndex::from_u32(caller), + cancellable != 0, + false, + None, + ) + .map(|r| r == WaitResult::Cancelled) +} + +#[cfg(feature = "component-model-async")] +fn thread_resume_later( + store: &mut dyn VMStore, + instance: Instance, + caller_instance: u32, + thread_idx: u32, +) -> Result<()> { + instance.resume_suspended_thread( + store, + RuntimeComponentInstanceIndex::from_u32(caller_instance), + thread_idx, + false, + ) +} + +#[cfg(feature = "component-model-async")] +fn thread_yield_to( + store: &mut dyn VMStore, + instance: Instance, + caller_instance: u32, + cancellable: u8, + thread_idx: u32, +) -> Result { + instance + .suspension_intrinsic( + store, + RuntimeComponentInstanceIndex::from_u32(caller_instance), + cancellable != 0, + true, + Some(thread_idx), + ) + .map(|r| r == WaitResult::Cancelled) +} diff --git a/crates/wasmtime/src/runtime/vm/table.rs b/crates/wasmtime/src/runtime/vm/table.rs index 8b3f84f4692c..e94029f475dc 100644 --- a/crates/wasmtime/src/runtime/vm/table.rs +++ b/crates/wasmtime/src/runtime/vm/table.rs @@ -750,7 +750,8 @@ impl Table { /// Get reference to the specified element. /// - /// Returns `None` if the index is out of bounds. + /// Returns `Ok(None)` if the element is null or uninitialized. + /// Returns `Err` if the index is out of bounds. /// /// Panics if this is a table of GC references and `gc_store` is `None`. pub fn get_func(&self, index: u64) -> Result>, Trap> { diff --git a/tests/all/component_model/async.rs b/tests/all/component_model/async.rs index 18b509b000cb..ec7e6b606703 100644 --- a/tests/all/component_model/async.rs +++ b/tests/all/component_model/async.rs @@ -1,6 +1,6 @@ use crate::async_functions::{PollOnce, execute_across_threads}; use anyhow::Result; -use wasmtime::component::*; +use wasmtime::{AsContextMut, Config, component::*}; use wasmtime::{Engine, Store, StoreContextMut, Trap}; use wasmtime_component_util::REALLOC_AND_FREE; @@ -314,3 +314,360 @@ async fn drop_resource_async() -> Result<()> { Ok(()) } + +/// Test task deletion in three situations, for every combination of lift/lower/(guest/host): +/// 1. An explicit thread calls task.return +/// 2. An explicit thread suspends indefinitely +/// 3. An explicit thread yield loops indefinitely +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn task_deletion() -> Result<()> { + let mut config = Config::new(); + config.async_support(true); + config.wasm_component_model_async(true); + config.wasm_component_model_threading(true); + config.wasm_component_model_async_stackful(true); + config.wasm_component_model_async_builtins(true); + let engine = Engine::new(&config)?; + let component = Component::new( + &engine, + r#"(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + ;; Defines the table for the thread start functions + (core module $libc + (table (export "__indirect_function_table") 3 funcref)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task-return (param i32))) + (import "" "task.cancel" (func $task-cancel)) + (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.suspend" (func $thread-suspend (result i32))) + (import "" "thread.suspend-cancellable" (func $thread-suspend-cancellable (result i32))) + (import "" "thread.yield-to" (func $thread-yield-to (param i32) (result i32))) + (import "" "thread.yield-to-cancellable" (func $thread-yield-to-cancellable (param i32) (result i32))) + (import "" "thread.switch-to" (func $thread-switch-to (param i32) (result i32))) + (import "" "thread.switch-to-cancellable" (func $thread-switch-to-cancellable (param i32) (result i32))) + (import "" "thread.yield" (func $thread-yield (result i32))) + (import "" "thread.yield-cancellable" (func $thread-yield-cancellable (result i32))) + (import "" "thread.index" (func $thread-index (result i32))) + (import "" "thread.resume-later" (func $thread-resume-later (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "libc" "__indirect_function_table" (table $indirect-function-table 3 funcref)) + + ;; Indices into the function table for the thread start functions + (global $call-return-ftbl-idx i32 (i32.const 0)) + (global $suspend-ftbl-idx i32 (i32.const 1)) + (global $yield-loop-ftbl-idx i32 (i32.const 2)) + + (func $call-return (param i32) + (call $task-return (local.get 0))) + + (func $suspend (param i32) + (drop (call $thread-suspend))) + + (func $yield-loop (param i32) + (loop $top + (drop (call $thread-yield)) + (br $top))) + + (func (export "explicit-thread-calls-return-stackful") + (call $thread-resume-later + (call $thread-new-indirect (global.get $call-return-ftbl-idx) (i32.const 42)))) + + (func (export "explicit-thread-calls-return-stackless") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $call-return-ftbl-idx) (i32.const 42))) + (i32.const 0 (; EXIT ;))) + + (func (export "cb") (param i32 i32 i32) (result i32) + (unreachable)) + + (func (export "explicit-thread-suspends-sync") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $suspend-ftbl-idx) (i32.const 42))) + (i32.const 42)) + + (func (export "explicit-thread-suspends-stackful") + (call $thread-resume-later + (call $thread-new-indirect (global.get $suspend-ftbl-idx) (i32.const 42))) + (call $task-return (i32.const 42))) + + (func (export "explicit-thread-suspends-stackless") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $suspend-ftbl-idx) (i32.const 42))) + (call $task-return (i32.const 42)) + (i32.const 0)) + + (func (export "explicit-thread-yield-loops-sync") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $yield-loop-ftbl-idx) (i32.const 42))) + (i32.const 42)) + + (func (export "explicit-thread-yield-loops-stackful") + (call $thread-resume-later + (call $thread-new-indirect (global.get $yield-loop-ftbl-idx) (i32.const 42))) + (call $task-return (i32.const 42))) + + (func (export "explicit-thread-yield-loops-stackless") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (global.get $suspend-ftbl-idx) (i32.const 42))) + (call $task-return (i32.const 42)) + (i32.const 0 (; EXIT ;))) + + + ;; Initialize the function table that will be used by thread.new_indirect + (elem (table $indirect-function-table) (i32.const 0 (; call-return-ftbl-idx ;)) func $call-return) + (elem (table $indirect-function-table) (i32.const 1 (; suspend-ftbl-idx ;)) func $suspend) + (elem (table $indirect-function-table) (i32.const 2 (; yield-loop-ftbl-idx ;)) func $yield-loop) + ) + + ;; Instantiate the libc module to get the table + (core instance $libc (instantiate $libc)) + ;; Get access to `thread.new_indirect` that uses the table from libc + (core type $start-func-ty (func (param i32))) + (alias core export $libc "__indirect_function_table" (core table $indirect-function-table)) + + (core func $task-return (canon task.return (result u32))) + (core func $task-cancel (canon task.cancel)) + (core func $thread-new-indirect + (canon thread.new_indirect $start-func-ty (table $indirect-function-table))) + (core func $thread-yield (canon thread.yield)) + (core func $thread-yield-cancellable (canon thread.yield cancellable)) + (core func $thread-index (canon thread.index)) + (core func $thread-yield-to (canon thread.yield-to)) + (core func $thread-yield-to-cancellable (canon thread.yield-to cancellable)) + (core func $thread-resume-later (canon thread.resume-later)) + (core func $thread-switch-to (canon thread.switch-to)) + (core func $thread-switch-to-cancellable (canon thread.switch-to cancellable)) + (core func $thread-suspend (canon thread.suspend)) + (core func $thread-suspend-cancellable (canon thread.suspend cancellable)) + (core func $waitable-set.new (canon waitable-set.new)) + (core func $waitable.join (canon waitable.join)) + (core func $waitable-set.wait (canon waitable-set.wait (memory $memory "mem"))) + + ;; Instantiate the main module + (core instance $cm ( + instantiate $CM + (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return" (func $task-return)) + (export "task.cancel" (func $task-cancel)) + (export "thread.new_indirect" (func $thread-new-indirect)) + (export "thread.index" (func $thread-index)) + (export "thread.yield-to" (func $thread-yield-to)) + (export "thread.yield-to-cancellable" (func $thread-yield-to-cancellable)) + (export "thread.yield" (func $thread-yield)) + (export "thread.yield-cancellable" (func $thread-yield-cancellable)) + (export "thread.switch-to" (func $thread-switch-to)) + (export "thread.switch-to-cancellable" (func $thread-switch-to-cancellable)) + (export "thread.suspend" (func $thread-suspend)) + (export "thread.suspend-cancellable" (func $thread-suspend-cancellable)) + (export "thread.resume-later" (func $thread-resume-later)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "waitable-set.new" (func $waitable-set.new)))) + (with "libc" (instance $libc)))) + + (func (export "explicit-thread-calls-return-stackful") (result u32) + (canon lift (core func $cm "explicit-thread-calls-return-stackful") async)) + (func (export "explicit-thread-calls-return-stackless") (result u32) + (canon lift (core func $cm "explicit-thread-calls-return-stackless") async (callback (func $cm "cb")))) + (func (export "explicit-thread-suspends-sync") (result u32) + (canon lift (core func $cm "explicit-thread-suspends-sync"))) + (func (export "explicit-thread-suspends-stackful") (result u32) + (canon lift (core func $cm "explicit-thread-suspends-stackful") async)) + (func (export "explicit-thread-suspends-stackless") (result u32) + (canon lift (core func $cm "explicit-thread-suspends-stackless") async (callback (func $cm "cb")))) + (func (export "explicit-thread-yield-loops-sync") (result u32) + (canon lift (core func $cm "explicit-thread-yield-loops-sync"))) + (func (export "explicit-thread-yield-loops-stackful") (result u32) + (canon lift (core func $cm "explicit-thread-yield-loops-stackful") async)) + (func (export "explicit-thread-yield-loops-stackless") (result u32) + (canon lift (core func $cm "explicit-thread-yield-loops-stackless") async (callback (func $cm "cb")))) + ) + + (component $D + (import "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful (result u32))) + (import "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless (result u32))) + (import "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync (result u32))) + (import "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful (result u32))) + (import "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless (result u32))) + (import "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync (result u32))) + (import "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful (result u32))) + (import "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless (result u32))) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $DM + (import "" "mem" (memory 1)) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + ;; sync lowered + (import "" "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful (result i32))) + (import "" "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless (result i32))) + (import "" "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync (result i32))) + (import "" "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful (result i32))) + (import "" "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless (result i32))) + (import "" "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync (result i32))) + (import "" "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful (result i32))) + (import "" "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless (result i32))) + ;; async lowered + (import "" "explicit-thread-calls-return-stackful-async" (func $explicit-thread-calls-return-stackful-async (param i32) (result i32))) + (import "" "explicit-thread-calls-return-stackless-async" (func $explicit-thread-calls-return-stackless-async (param i32) (result i32))) + (import "" "explicit-thread-suspends-sync-async" (func $explicit-thread-suspends-sync-async (param i32) (result i32))) + (import "" "explicit-thread-suspends-stackful-async" (func $explicit-thread-suspends-stackful-async (param i32) (result i32))) + (import "" "explicit-thread-suspends-stackless-async" (func $explicit-thread-suspends-stackless-async (param i32) (result i32))) + (import "" "explicit-thread-yield-loops-sync-async" (func $explicit-thread-yield-loops-sync-async (param i32) (result i32))) + (import "" "explicit-thread-yield-loops-stackful-async" (func $explicit-thread-yield-loops-stackful-async (param i32) (result i32))) + (import "" "explicit-thread-yield-loops-stackless-async" (func $explicit-thread-yield-loops-stackless-async (param i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "thread.yield" (func $thread-yield (result i32))) + + (func $check (param i32) + (if (i32.ne (local.get 0) (i32.const 42)) + (then unreachable)) + ) + + (func $check-async (param i32) + (local $retp i32) (local $ws i32) (local $ws-retp i32) + (local.set $retp (i32.const 8)) + (local.set $ws-retp (i32.const 16)) + (local.set $ws (call $waitable-set.new)) + + (if (i32.eq (i32.and (local.get 0) (i32.const 0xF)) (i32.const 2 (; RETURNED ;))) + (then (call $check (i32.load (local.get $retp)))) + (else + (call $waitable.join (i32.shr_u (local.get 0) (i32.const 4)) (local.get $ws)) + (drop (call $waitable-set.wait (local.get $ws) (local.get $ws-retp))) + (call $check (i32.load (local.get $retp))))) + ) + + (func $run (export "run") (result i32) + (local $retp i32) + (local.set $retp (i32.const 8)) + (call $check (call $explicit-thread-calls-return-stackless)) + (call $check (call $explicit-thread-calls-return-stackful)) + (call $check (call $explicit-thread-suspends-sync)) + (call $check (call $explicit-thread-suspends-stackful)) + (call $check (call $explicit-thread-suspends-stackless)) + (call $check (call $explicit-thread-yield-loops-sync)) + (call $check (call $explicit-thread-yield-loops-stackful)) + (call $check (call $explicit-thread-yield-loops-stackless)) + + (call $check-async (call $explicit-thread-calls-return-stackless-async (local.get $retp))) + (call $check-async (call $explicit-thread-calls-return-stackful-async (local.get $retp))) + (call $check-async (call $explicit-thread-suspends-sync-async (local.get $retp))) + (call $check-async (call $explicit-thread-suspends-stackful-async (local.get $retp))) + (call $check-async (call $explicit-thread-suspends-stackless-async (local.get $retp))) + (call $check-async (call $explicit-thread-yield-loops-sync-async (local.get $retp))) + (call $check-async (call $explicit-thread-yield-loops-stackful-async (local.get $retp))) + (call $check-async (call $explicit-thread-yield-loops-stackless-async (local.get $retp))) + + (i32.const 42) + ) + ) + + (core func $waitable-set.new (canon waitable-set.new)) + (core func $waitable-set.wait (canon waitable-set.wait (memory $memory "mem"))) + (core func $waitable.join (canon waitable.join)) + (core func $subtask.cancel (canon subtask.cancel async)) + (core func $thread.yield (canon thread.yield)) + ;; sync lowered + (canon lower (func $explicit-thread-calls-return-stackful) (memory $memory "mem") (core func $explicit-thread-calls-return-stackful')) + (canon lower (func $explicit-thread-calls-return-stackless) (memory $memory "mem") (core func $explicit-thread-calls-return-stackless')) + (canon lower (func $explicit-thread-suspends-sync) (memory $memory "mem") (core func $explicit-thread-suspends-sync')) + (canon lower (func $explicit-thread-suspends-stackful) (memory $memory "mem") (core func $explicit-thread-suspends-stackful')) + (canon lower (func $explicit-thread-suspends-stackless) (memory $memory "mem") (core func $explicit-thread-suspends-stackless')) + (canon lower (func $explicit-thread-yield-loops-sync) (memory $memory "mem") (core func $explicit-thread-yield-loops-sync')) + (canon lower (func $explicit-thread-yield-loops-stackful) (memory $memory "mem") (core func $explicit-thread-yield-loops-stackful')) + (canon lower (func $explicit-thread-yield-loops-stackless) (memory $memory "mem") (core func $explicit-thread-yield-loops-stackless')) + ;; async lowered + (canon lower (func $explicit-thread-calls-return-stackful) async (memory $memory "mem") (core func $explicit-thread-calls-return-stackful-async')) + (canon lower (func $explicit-thread-calls-return-stackless) async (memory $memory "mem") (core func $explicit-thread-calls-return-stackless-async')) + (canon lower (func $explicit-thread-suspends-sync) async (memory $memory "mem") (core func $explicit-thread-suspends-sync-async')) + (canon lower (func $explicit-thread-suspends-stackful) async (memory $memory "mem") (core func $explicit-thread-suspends-stackful-async')) + (canon lower (func $explicit-thread-suspends-stackless) async (memory $memory "mem") (core func $explicit-thread-suspends-stackless-async')) + (canon lower (func $explicit-thread-yield-loops-sync) async (memory $memory "mem") (core func $explicit-thread-yield-loops-sync-async')) + (canon lower (func $explicit-thread-yield-loops-stackful) async (memory $memory "mem") (core func $explicit-thread-yield-loops-stackful-async')) + (canon lower (func $explicit-thread-yield-loops-stackless) async (memory $memory "mem") (core func $explicit-thread-yield-loops-stackless-async')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "explicit-thread-calls-return-stackful" (func $explicit-thread-calls-return-stackful')) + (export "explicit-thread-calls-return-stackless" (func $explicit-thread-calls-return-stackless')) + (export "explicit-thread-suspends-sync" (func $explicit-thread-suspends-sync')) + (export "explicit-thread-suspends-stackful" (func $explicit-thread-suspends-stackful')) + (export "explicit-thread-suspends-stackless" (func $explicit-thread-suspends-stackless')) + (export "explicit-thread-yield-loops-sync" (func $explicit-thread-yield-loops-sync')) + (export "explicit-thread-yield-loops-stackful" (func $explicit-thread-yield-loops-stackful')) + (export "explicit-thread-yield-loops-stackless" (func $explicit-thread-yield-loops-stackless')) + (export "explicit-thread-calls-return-stackful-async" (func $explicit-thread-calls-return-stackful-async')) + (export "explicit-thread-calls-return-stackless-async" (func $explicit-thread-calls-return-stackless-async')) + (export "explicit-thread-suspends-sync-async" (func $explicit-thread-suspends-sync-async')) + (export "explicit-thread-suspends-stackful-async" (func $explicit-thread-suspends-stackful-async')) + (export "explicit-thread-suspends-stackless-async" (func $explicit-thread-suspends-stackless-async')) + (export "explicit-thread-yield-loops-sync-async" (func $explicit-thread-yield-loops-sync-async')) + (export "explicit-thread-yield-loops-stackful-async" (func $explicit-thread-yield-loops-stackful-async')) + (export "explicit-thread-yield-loops-stackless-async" (func $explicit-thread-yield-loops-stackless-async')) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "subtask.cancel" (func $subtask.cancel)) + (export "thread.yield" (func $thread.yield)) + )))) + (func (export "run") (result u32) (canon lift (core func $dm "run"))) + ) + + (instance $c (instantiate $C)) + (instance $d (instantiate $D + (with "explicit-thread-calls-return-stackful" (func $c "explicit-thread-calls-return-stackful")) + (with "explicit-thread-calls-return-stackless" (func $c "explicit-thread-calls-return-stackless")) + (with "explicit-thread-suspends-sync" (func $c "explicit-thread-suspends-sync")) + (with "explicit-thread-suspends-stackful" (func $c "explicit-thread-suspends-stackful")) + (with "explicit-thread-suspends-stackless" (func $c "explicit-thread-suspends-stackless")) + (with "explicit-thread-yield-loops-sync" (func $c "explicit-thread-yield-loops-sync")) + (with "explicit-thread-yield-loops-stackful" (func $c "explicit-thread-yield-loops-stackful")) + (with "explicit-thread-yield-loops-stackless" (func $c "explicit-thread-yield-loops-stackless")) + )) + (func (export "run") (alias export $d "run")) + (func (export "explicit-thread-calls-return-stackful") (alias export $c "explicit-thread-calls-return-stackful")) + (func (export "explicit-thread-calls-return-stackless") (alias export $c "explicit-thread-calls-return-stackless")) + (func (export "explicit-thread-suspends-sync") (alias export $c "explicit-thread-suspends-sync")) + (func (export "explicit-thread-suspends-stackful") (alias export $c "explicit-thread-suspends-stackful")) + (func (export "explicit-thread-suspends-stackless") (alias export $c "explicit-thread-suspends-stackless")) + (func (export "explicit-thread-yield-loops-sync") (alias export $c "explicit-thread-yield-loops-sync")) + (func (export "explicit-thread-yield-loops-stackful") (alias export $c "explicit-thread-yield-loops-stackful")) + (func (export "explicit-thread-yield-loops-stackless") (alias export $c "explicit-thread-yield-loops-stackless")) +) + "#, + )? + .serialize()?; + + let component = unsafe { Component::deserialize(&engine, &component)? }; + let mut store = Store::new(&engine, ()); + let instance = Linker::new(&engine) + .instantiate_async(&mut store, &component) + .await?; + let funcs = vec![ + "run", + "explicit-thread-calls-return-stackful", + "explicit-thread-calls-return-stackless", + "explicit-thread-suspends-sync", + "explicit-thread-suspends-stackful", + "explicit-thread-suspends-stackless", + "explicit-thread-yield-loops-sync", + "explicit-thread-yield-loops-stackful", + "explicit-thread-yield-loops-stackless", + ]; + for func in funcs { + let func = instance.get_typed_func::<(), (u32,)>(&mut store, func)?; + assert_eq!(func.call_async(&mut store, ()).await?, (42,)); + func.post_return_async(store.as_context_mut()).await?; + } + + Ok(()) +} diff --git a/tests/all/component_model/func.rs b/tests/all/component_model/func.rs index 22e280a61785..08194b9c4688 100644 --- a/tests/all/component_model/func.rs +++ b/tests/all/component_model/func.rs @@ -910,6 +910,8 @@ async fn async_reentrance() -> Result<()> { let mut config = Config::new(); config.wasm_component_model_async(true); + config.wasm_component_model_async_stackful(true); + config.wasm_component_model_threading(true); config.async_support(true); let engine = &Engine::new(&config)?; let component = Component::new(&engine, component)?; @@ -958,6 +960,88 @@ async fn missing_task_return_call_stackless() -> Result<()> { .await } +#[tokio::test] +async fn missing_task_return_call_stackless_explicit_thread() -> Result<()> { + task_return_trap( + r#"(component + (core module $libc + (table (export "__indirect_function_table") 1 funcref)) + (core module $m + (import "" "task.return" (func $task-return)) + (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.resume-later" (func $thread-resume-later (param i32))) + (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) + (func $thread-start (param i32) (; empty ;)) + (elem (table $indirect-function-table) (i32.const 0) func $thread-start) + (func (export "foo") (result i32) + (call $thread-resume-later + (call $thread-new-indirect (i32.const 0) (i32.const 0))) + i32.const 0 + ) + (func (export "callback") (param i32 i32 i32) (result i32) unreachable) + ) + (core instance $libc (instantiate $libc)) + (core type $start-func-ty (func (param i32))) + (alias core export $libc "__indirect_function_table" (core table $indirect-function-table)) + (core func $thread-new-indirect + (canon thread.new_indirect $start-func-ty (table $indirect-function-table))) + (core func $thread-resume-later (canon thread.resume-later)) + (core func $task-return (canon task.return)) + (core instance $i (instantiate $m + (with "" (instance + (export "thread.new_indirect" (func $thread-new-indirect)) + (export "thread.resume-later" (func $thread-resume-later)) + (export "task.return" (func $task-return)) + )) + (with "libc" (instance $libc)) + )) + (func (export "foo") (canon lift (core func $i "foo") async (callback (func $i "callback")))) + )"#, + "wasm trap: async-lifted export failed to produce a result", + ) + .await +} + +#[tokio::test] +async fn missing_task_return_call_stackful_explicit_thread() -> Result<()> { + task_return_trap( + r#"(component + (core module $libc + (table (export "__indirect_function_table") 1 funcref)) + (core module $m + (import "" "task.return" (func $task-return)) + (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.resume-later" (func $thread-resume-later (param i32))) + (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) + (func $thread-start (param i32) (; empty ;)) + (elem (table $indirect-function-table) (i32.const 0) func $thread-start) + (func (export "foo") + (call $thread-resume-later + (call $thread-new-indirect (i32.const 0) (i32.const 0))) + ) + ) + (core instance $libc (instantiate $libc)) + (core type $start-func-ty (func (param i32))) + (alias core export $libc "__indirect_function_table" (core table $indirect-function-table)) + (core func $thread-new-indirect + (canon thread.new_indirect $start-func-ty (table $indirect-function-table))) + (core func $thread-resume-later (canon thread.resume-later)) + (core func $task-return (canon task.return)) + (core instance $i (instantiate $m + (with "" (instance + (export "thread.new_indirect" (func $thread-new-indirect)) + (export "thread.resume-later" (func $thread-resume-later)) + (export "task.return" (func $task-return)) + )) + (with "libc" (instance $libc)) + )) + (func (export "foo") (canon lift (core func $i "foo") async)) + )"#, + "wasm trap: async-lifted export failed to produce a result", + ) + .await +} + #[tokio::test] async fn missing_task_return_call_stackful() -> Result<()> { task_return_trap( @@ -1040,6 +1124,7 @@ async fn task_return_trap(component: &str, substring: &str) -> Result<()> { let mut config = Config::new(); config.wasm_component_model_async(true); config.wasm_component_model_async_stackful(true); + config.wasm_component_model_threading(true); config.async_support(true); let engine = &Engine::new(&config)?; let component = Component::new(&engine, component)?; diff --git a/tests/all/pulley.rs b/tests/all/pulley.rs index 870d621c34b0..633d502e8474 100644 --- a/tests/all/pulley.rs +++ b/tests/all/pulley.rs @@ -85,8 +85,9 @@ fn provenance_test_config() -> Config { config.memory_guard_size(0); config.signals_based_traps(false); config.wasm_component_model_async(true); - config.wasm_component_model_async_stackful(true); config.wasm_component_model_async_builtins(true); + config.wasm_component_model_async_stackful(true); + config.wasm_component_model_threading(true); config.wasm_component_model_error_context(true); config } diff --git a/tests/misc_testsuite/component-model-threading/many-threads-indexed.wast b/tests/misc_testsuite/component-model-threading/many-threads-indexed.wast new file mode 100644 index 000000000000..852b7f00c982 --- /dev/null +++ b/tests/misc_testsuite/component-model-threading/many-threads-indexed.wast @@ -0,0 +1,126 @@ +;;! component_model_async = true +;;! component_model_threading = true + +;; Spawns 5 threads, makes them write their indices into a buffer out-of-order, then yields to them in-order, +;; ensuring that the yield order is as-expected. + +;; More concretely: +;; Thread Index | Assigned Number +;; 1 | 16 +;; 2 | 12 +;; 3 | 04 +;; 4 | 00 +;; 5 | 08 + +;; After all threads have spawned and written their indices to the byte position given by their assigned number, +;; the buffer state will be: +;; 4 3 5 2 1 + +;; The main thread will then yield to these threads in the order that they are stored in the buffer, +;; and they will write their assigned number into the buffer, after the indices. +;; After all threads have been yielded to, the buffer contents will be: +;; 4 3 5 2 1 0 4 8 12 16 + +;; The main thread then ensures that the assigned numbers have been written into the correct locations. + +(component + ;; Defines the table for the thread start function + (core module $libc + (table (export "__indirect_function_table") 1 funcref)) + ;; Defines the thread start function and a function that calls thread.new_indirect + (core module $m + ;; Import the threading builtins and the table from libc + (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.suspend" (func $thread-suspend (result i32))) + (import "" "thread.yield-to" (func $thread-yield-to (param i32) (result i32))) + (import "" "thread.switch-to" (func $thread-switch-to (param i32) (result i32))) + (import "" "thread.yield" (func $thread-yield (result i32))) + (import "" "thread.index" (func $thread-index (result i32))) + (import "" "thread.resume-later" (func $thread-resume-later (param i32))) + (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) + + ;; A memory block that threads will write their thread indexes and assigned values into + (memory 1) + + ;; A global that points to the next memory index to write into + ;; We initialize this to 20 (threads * 4 bytes of storage per thread) + (global $g (mut i32) (i32.const 20)) + + ;; The thread entry point, which writes the thread's index into memory at the assigned location, + ;; suspends back to the main thread, then writes the assigned value into memory + (func $thread-start (param i32) + ;; Store the thread index into the assigned location + (i32.store (local.get 0) (call $thread-index)) + (drop (call $thread-suspend)) + (i32.store (global.get $g) (local.get 0)) + (global.set $g + (i32.add (global.get $g) (i32.const 4)))) + (export "thread-start" (func $thread-start)) + + ;; Initialize the function table with our thread-start function; this will be + ;; used by thread.new_indirect + (elem (table $indirect-function-table) (i32.const 0) func $thread-start) + + (func $new-thread (param i32) + (drop + (call $thread-yield-to + (call $thread-new-indirect (i32.const 0) (local.get 0))))) + + ;; The main entry point + (func (export "run") (result i32) + ;; Spawn 5 new threads with assigned numbers + (call $new-thread (i32.const 16)) + (call $new-thread (i32.const 12)) + (call $new-thread (i32.const 4)) + (call $new-thread (i32.const 0)) + (call $new-thread (i32.const 8)) + + ;; Yield to all threads in ascending order of assigned number + (drop (call $thread-yield-to (i32.load (i32.const 0)))) + (drop (call $thread-yield-to (i32.load (i32.const 4)))) + (drop (call $thread-yield-to (i32.load (i32.const 8)))) + (drop (call $thread-yield-to (i32.load (i32.const 12)))) + (drop (call $thread-yield-to (i32.load (i32.const 16)))) + + ;; Ensure all assigned numbers have been written to the buffer in order + (if (i32.ne (i32.load (i32.const 20)) (i32.const 0)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 24)) (i32.const 4)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 28)) (i32.const 8)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 32)) (i32.const 12)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 36)) (i32.const 16)) (then unreachable)) + + ;; Sentinel value + (i32.const 42))) + + ;; Instantiate the libc module to get the table + (core instance $libc (instantiate $libc)) + ;; Get access to `thread.new_indirect` that uses the table from libc + (core type $start-func-ty (func (param i32))) + (alias core export $libc "__indirect_function_table" (core table $indirect-function-table)) + + (core func $thread-new-indirect + (canon thread.new_indirect $start-func-ty (table $indirect-function-table))) + (core func $thread-yield (canon thread.yield)) + (core func $thread-index (canon thread.index)) + (core func $thread-yield-to (canon thread.yield-to)) + (core func $thread-resume-later (canon thread.resume-later)) + (core func $thread-switch-to (canon thread.switch-to)) + (core func $thread-suspend (canon thread.suspend)) + + ;; Instantiate the main module + (core instance $i ( + instantiate $m + (with "" (instance + (export "thread.new_indirect" (func $thread-new-indirect)) + (export "thread.index" (func $thread-index)) + (export "thread.yield-to" (func $thread-yield-to)) + (export "thread.yield" (func $thread-yield)) + (export "thread.switch-to" (func $thread-switch-to)) + (export "thread.suspend" (func $thread-suspend)) + (export "thread.resume-later" (func $thread-resume-later)))) + (with "libc" (instance $libc)))) + + ;; Export the main entry point + (func (export "run") (result u32) (canon lift (core func $i "run")))) + +(assert_return (invoke "run") (u32.const 42)) \ No newline at end of file diff --git a/tests/misc_testsuite/component-model-threading/stackful-cancellation.wast b/tests/misc_testsuite/component-model-threading/stackful-cancellation.wast new file mode 100644 index 000000000000..4eaa0abad99b --- /dev/null +++ b/tests/misc_testsuite/component-model-threading/stackful-cancellation.wast @@ -0,0 +1,385 @@ +;;! component_model_async = true +;;! component_model_async_stackful = true +;;! component_model_async_builtins = true +;;! component_model_threading = true +;;! reference_types = true + +;; Tests that cancellation works with the async threading intrinsics. +;; Consists of two components, C and D. C implements functions that mix cancellable and uncancellable yields and suspensions. +;; D calls these functions and cancels the resulting subtasks, ensuring that cancellation is only seen when expected. + +;; -- Component C -- + +;; `run-yield`: Yields twice, first with an uncancellable yield, then with a cancellable yield. +;; The caller cancels the subtask during the first yield, and ensures that the cancellation only takes effect +;; on the second yield. + +;; `run-yield-to`: Yields twice to a spawned thread, first with an uncancellable yield, then with a cancellable yield. +;; A complication is that we can't guarantee that if the spawned thread yields, the supertask will be scheduled to +;; cancel the subtask before the subtask's implicit thread is rescheduled. To handle this, the subtask's implicit +;; thread first waits on a future to be written by the supertask, then yields to the spawned thread. + +;; `run-suspend`: More complex, because executing an uncancellable suspension requires another +;; thread in the same subtask to explicitly wake it up. This is done by the subtask spawning a new thread that +;; waits on a future to be written by the supertask, and then resumes the main thread once that happens. +;; After setting up this thread, `run-suspend` performs an uncancellable suspend, then a cancellable suspend. +;; The caller cancels the subtask during the first suspend, writes to the future to make the spawned thread +;; resume the implicit thread, and ensures that the cancellation only takes effect on the second suspend. + +;; `run-switch-to`: Similar to `run-suspend`, but uses `thread.switch-to` instead of `thread.suspend`. + +;; -- Component D -- + +;; `run-test`: Calls one of the functions in C based on a test id, cancels the resulting subtask, and ensures that +;; cancellation is only seen when expected. + +;; `run`: Calls `run-test` for each of the functions in C. + +(component + (component $C + (type $FT (future)) + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + ;; Defines the table for the thread start functions, of which there are two + (core module $libc + (table (export "__indirect_function_table") 2 funcref)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.cancel" (func $task-cancel)) + (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.suspend" (func $thread-suspend (result i32))) + (import "" "thread.suspend-cancellable" (func $thread-suspend-cancellable (result i32))) + (import "" "thread.yield-to" (func $thread-yield-to (param i32) (result i32))) + (import "" "thread.yield-to-cancellable" (func $thread-yield-to-cancellable (param i32) (result i32))) + (import "" "thread.switch-to" (func $thread-switch-to (param i32) (result i32))) + (import "" "thread.switch-to-cancellable" (func $thread-switch-to-cancellable (param i32) (result i32))) + (import "" "thread.yield" (func $thread-yield (result i32))) + (import "" "thread.yield-cancellable" (func $thread-yield-cancellable (result i32))) + (import "" "thread.index" (func $thread-index (result i32))) + (import "" "thread.resume-later" (func $thread-resume-later (param i32))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "libc" "__indirect_function_table" (table $indirect-function-table 2 funcref)) + + ;; Indices into the function table for the thread start functions + (global $wake-from-suspend-ftbl-idx i32 (i32.const 0)) + (global $just-yield-ftbl-idx i32 (i32.const 1)) + + (func (export "run-yield") + ;; Yield back to the caller, who will attempt to cancel us, but we won't see it + ;; because we're using an uncancellable yield + (if (i32.ne (call $thread-yield) (i32.const 0)) (then unreachable)) + ;; Yield back to the caller again. This time, we should receive the cancellation immediately. + (if (i32.ne (call $thread-yield-cancellable) (i32.const 1)) (then unreachable)) + (call $task-cancel) + ) + + (func $wait-for-future-write (param i32) + (local $ret i32) + ;; Perform a future.read, which will block, waiting for the supertask to write + (local.set $ret (call $future.read (local.get 0) (i32.const 0xba5eba11))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + ) + + (func $wake-from-suspend (param i32) + ;; Extract the thread index and future to wait on from the argument structure + (local $thread-index i32) (local $future i32) + (local.set $thread-index (i32.load offset=0 (local.get 0))) + (local.set $future (i32.load offset=4 (local.get 0))) + + ;; Wait for the supertask to signal us to wake up suspended thread. + (call $wait-for-future-write (local.get $future)) + ;; Resume the main thread, which is suspended in an uncancellable suspend + (call $thread-resume-later (local.get $thread-index)) + ) + + (func $just-yield (param $explicit-thread-idx i32) + ;; Yield nondeterministically, either back to the supertask, who will then wait on cancellation to be acknowledged, + ;; or to the implicit thread, who will acknowledge the cancellation. + (if (i32.ne (call $thread-yield) (i32.const 0)) (then unreachable)) + ) + + ;; Initialize the function table that will be used by thread.new_indirect + (elem (table $indirect-function-table) (i32.const 0 (; wake-from-suspend-ftbl-idx ;)) func $wake-from-suspend) + (elem (table $indirect-function-table) (i32.const 1 (; just-yield-ftbl-idx ;)) func $just-yield) + + (func (export "run-yield-to") (param $futr i32) + (local $thread-index i32) + ;; Spawn a new thread that will wake us up from our uncancellable suspend; we'll switch to it next + (local.set $thread-index + (call $thread-new-indirect (global.get $just-yield-ftbl-idx) (call $thread-index))) + + ;; We can't guarantee that the supertask will be scheduled to cancel us before we're rescheduled, so we first + ;; wait on the future to be written, then yield to the spawned thread. This means that cancellation will be + ;; sent while we're waiting on the future rather than at the yield point, but the cancel will still be pending + ;; when we reach the yield point, so it should still be ignored by the uncancellable yield and only take effect + ;; when we reach the second, cancellable yield. + (call $wait-for-future-write (local.get $futr)) + + ;; Yield to the spawned thread uncancellably. We should eventually be rescheduled without being notified + ;; of the pending cancellation. + (if (i32.ne (call $thread-yield-to (local.get $thread-index)) (i32.const 0)) (then unreachable)) + ;; Yield to the spawned thread again. This time we should see the cancellation immediately. + (if (i32.ne (call $thread-yield-to-cancellable (local.get $thread-index)) (i32.const 1)) (then unreachable)) + (call $task-cancel) + ) + + (func (export "run-suspend") (param $futr i32) + ;; Set up the arguments for the wake-for-suspend thread start function. + ;; It expects a pointer to a structure containing the thread index to resume + ;; and the future to wait on before resuming it. + (local $wake-from-suspend-argp i32) + (local.set $wake-from-suspend-argp (i32.const 4)) + (i32.store offset=0 (local.get $wake-from-suspend-argp) (call $thread-index)) + (i32.store offset=4 (local.get $wake-from-suspend-argp) (local.get $futr)) + + ;; Spawn a new thread that will wake us up from our uncancellable suspend and schedule + ;; it to resume after we suspend. + (call $thread-resume-later + (call $thread-new-indirect (global.get $wake-from-suspend-ftbl-idx) (local.get $wake-from-suspend-argp))) + + ;; Request suspension. We will not be woken up by cancellation, because this is an uncancellable + ;; suspend. We will be woken up by the other thread we spawned above, which will be resumed after + ;; the supertask cancels our subtask. + (if (i32.ne (call $thread-suspend) (i32.const 0)) (then unreachable)) + ;; Request suspension again. This time we should see the cancellation immediately. + (if (i32.ne (call $thread-suspend-cancellable) (i32.const 1)) (then unreachable)) + (call $task-cancel) + ) + + (func (export "run-switch-to") (param $futr i32) + (local $thread-index i32) + ;; Set up the arguments for the wake-for-suspend thread start function. + ;; It expects a pointer to a structure containing the thread index to resume + ;; and the future to wait on before resuming it. + (local $wake-from-suspend-argp i32) + (local.set $wake-from-suspend-argp (i32.const 4)) + (i32.store offset=0 (local.get $wake-from-suspend-argp) (call $thread-index)) + (i32.store offset=4 (local.get $wake-from-suspend-argp) (local.get $futr)) + + ;; Spawn a new thread that will wake us up from our uncancellable suspend; we'll switch to it next + (local.set $thread-index + (call $thread-new-indirect (global.get $wake-from-suspend-ftbl-idx) (local.get $wake-from-suspend-argp))) + + ;; Request suspension by switching to the spawned thread. + ;; We will not be woken up by cancellation, because this is an uncancellable suspend. + ;; We will be woken up by the other thread we spawned above, which will be resumed after + ;; the supertask cancels our subtask. + (if (i32.ne (call $thread-switch-to (local.get $thread-index)) (i32.const 0)) (then unreachable)) + ;; Request suspension again. This time we should see the cancellation immediately. + (if (i32.ne (call $thread-switch-to-cancellable (local.get $thread-index)) (i32.const 1)) (then unreachable)) + (call $task-cancel) + ) + ) + + ;; Instantiate the libc module to get the table + (core instance $libc (instantiate $libc)) + ;; Get access to `thread.new_indirect` that uses the table from libc + (core type $start-func-ty (func (param i32))) + (alias core export $libc "__indirect_function_table" (core table $indirect-function-table)) + + (core func $task-cancel (canon task.cancel)) + (core func $thread-new-indirect + (canon thread.new_indirect $start-func-ty (table $indirect-function-table))) + (core func $thread-yield (canon thread.yield)) + (core func $thread-yield-cancellable (canon thread.yield cancellable)) + (core func $thread-index (canon thread.index)) + (core func $thread-yield-to (canon thread.yield-to)) + (core func $thread-yield-to-cancellable (canon thread.yield-to cancellable)) + (core func $thread-resume-later (canon thread.resume-later)) + (core func $thread-switch-to (canon thread.switch-to)) + (core func $thread-switch-to-cancellable (canon thread.switch-to cancellable)) + (core func $thread-suspend (canon thread.suspend)) + (core func $thread-suspend-cancellable (canon thread.suspend cancellable)) + (core func $future.read (canon future.read $FT (memory $memory "mem"))) + (core func $waitable-set.new (canon waitable-set.new)) + (core func $waitable.join (canon waitable.join)) + (core func $waitable-set.wait (canon waitable-set.wait (memory $memory "mem"))) + + ;; Instantiate the main module + (core instance $cm ( + instantiate $CM + (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.cancel" (func $task-cancel)) + (export "thread.new_indirect" (func $thread-new-indirect)) + (export "thread.index" (func $thread-index)) + (export "thread.yield-to" (func $thread-yield-to)) + (export "thread.yield-to-cancellable" (func $thread-yield-to-cancellable)) + (export "thread.yield" (func $thread-yield)) + (export "thread.yield-cancellable" (func $thread-yield-cancellable)) + (export "thread.switch-to" (func $thread-switch-to)) + (export "thread.switch-to-cancellable" (func $thread-switch-to-cancellable)) + (export "thread.suspend" (func $thread-suspend)) + (export "thread.suspend-cancellable" (func $thread-suspend-cancellable)) + (export "thread.resume-later" (func $thread-resume-later)) + (export "future.read" (func $future.read)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "waitable-set.new" (func $waitable-set.new)))) + (with "libc" (instance $libc)))) + + (func (export "run-yield") (result u32) (canon lift (core func $cm "run-yield") async)) + (func (export "run-yield-to") (param "fut" $FT) (result u32) (canon lift (core func $cm "run-yield-to") async)) + (func (export "run-suspend") (param "fut" $FT) (result u32) (canon lift (core func $cm "run-suspend") async)) + (func (export "run-switch-to") (param "fut" $FT) (result u32) (canon lift (core func $cm "run-switch-to") async)) + ) + + (component $D + (type $FT (future)) + (import "run-yield" (func $run-yield (result u32))) + (import "run-yield-to" (func $run-yield-to (param "fut" $FT) (result u32))) + (import "run-suspend" (func $run-suspend (param "fut" $FT) (result u32))) + (import "run-switch-to" (func $run-switch-to (param "fut" $FT) (result u32))) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $DM + (import "" "mem" (memory 1)) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + (import "" "run-yield" (func $run-yield (param i32) (result i32))) + (import "" "run-yield-to" (func $run-yield-to (param i32 i32) (result i32))) + (import "" "run-suspend" (func $run-suspend (param i32 i32) (result i32))) + (import "" "run-switch-to" (func $run-switch-to (param i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "thread.yield" (func $thread-yield (result i32))) + + (func $run-test (param $test-id i32) (result i32) + (local $ret i32) (local $subtask i32) + (local $ws i32) (local $event_code i32) + (local $run-retp i32) (local $wait-retp i32) + (local $ret64 i64) (local $futr i32) (local $futw i32) + + ;; Set up return value storage for run-suspend/switch-to and waitable-set.wait + (local.set $run-retp (i32.const 4)) + (local.set $wait-retp (i32.const 8)) + (i32.store (local.get $run-retp) (i32.const 0xbad0bad0)) + (i32.store (local.get $wait-retp) (i32.const 0xbad0bad0)) + + ;; Create a future that the subtask may wait on + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; Calling run-suspend/switch-to will start the thread, which will suspend. + ;; This is basically a switch statement: + ;; 0: run-yield + ;; 1: run-yield-to + ;; 2: run-suspend + ;; 3: run-switch-to + (if (i32.eq (local.get $test-id) (i32.const 0)) + (then (local.set $ret (call $run-yield (local.get $run-retp)))) + (else (if (i32.eq (local.get $test-id) (i32.const 1)) + (then (local.set $ret (call $run-yield-to (local.get $futr) (local.get $run-retp)))) + (else (if (i32.eq (local.get $test-id) (i32.const 2)) + (then (local.set $ret (call $run-suspend (local.get $futr) (local.get $run-retp)))) + (else (if (i32.eq (local.get $test-id) (i32.const 3)) + (then (local.set $ret (call $run-switch-to (local.get $futr) (local.get $run-retp)))) + (else unreachable)))))))) + + ;; Ensure that the thread started + (if (i32.ne (i32.and (local.get $ret) (i32.const 0xF)) (i32.const 1 (; STARTED ;))) + (then unreachable)) + ;; Extract the subtask index + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + ;; Cancel the subtask, which should block, because the initial suspend/yield is uncancellable + (local.set $ret (call $subtask.cancel (local.get $subtask))) + ;; Ensure the cancellation blocked + (if (i32.ne (local.get $ret) (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + + ;; If we're not testing run-yield, the subtask is expecting a write to our future, so write to it + (if (i32.ne (local.get $test-id) (i32.const 0)) + (then + (local.set $ret (call $future.write (local.get $futw) (i32.const 0xdeadbeef))) + ;; The write should succeed + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)))) + + ;; Wait on the subtask, which will eventually progress to a cancellable yield/suspend and acknowledge the cancellation + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $subtask) (local.get $ws)) + (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $wait-retp))) + ;; Ensure we got the subtask event + (if (i32.ne (local.get $event_code) (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + ;; Ensure the subtask index matches + (if (i32.ne (local.get $subtask) (i32.load (local.get $wait-retp))) + (then unreachable)) + ;; Ensure the subtask was cancelled before it returned + (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED=4 | (0<<4) ;)) + (i32.load offset=4 (local.get $wait-retp))) + (then unreachable)) + + ;; Return success + (i32.const 42) + ) + + (func $run (export "run") (result i32) + ;; test-id 0: run-yield + (if (i32.ne (call $run-test (i32.const 0)) (i32.const 42)) + (then unreachable)) + + ;; test-id 1: run-yield-to + (if (i32.ne (call $run-test (i32.const 1)) (i32.const 42)) + (then unreachable)) + + ;; test-id 2: run-suspend + (if (i32.ne (call $run-test (i32.const 2)) (i32.const 42)) + (then unreachable)) + + ;; test-id 3: run-switch-to + (if (i32.ne (call $run-test (i32.const 3)) (i32.const 42)) + (then unreachable)) + + ;; Return success + (i32.const 42) + ) + ) + + (core func $waitable-set.new (canon waitable-set.new)) + (core func $waitable-set.wait (canon waitable-set.wait (memory $memory "mem"))) + (core func $waitable.join (canon waitable.join)) + (core func $subtask.cancel (canon subtask.cancel async)) + (core func $future.new (canon future.new $FT)) + (core func $future.write (canon future.write $FT (memory $memory "mem"))) + (core func $thread.yield (canon thread.yield)) + (canon lower (func $run-yield) async (memory $memory "mem") (core func $run-yield')) + (canon lower (func $run-suspend) async (memory $memory "mem") (core func $run-suspend')) + (canon lower (func $run-switch-to) async (memory $memory "mem") (core func $run-switch-to')) + (canon lower (func $run-yield-to) async (memory $memory "mem") (core func $run-yield-to')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "run-yield" (func $run-yield')) + (export "run-suspend" (func $run-suspend')) + (export "run-switch-to" (func $run-switch-to')) + (export "run-yield-to" (func $run-yield-to')) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "subtask.cancel" (func $subtask.cancel)) + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "thread.yield" (func $thread.yield)) + )))) + (func (export "run") (result u32) (canon lift (core func $dm "run"))) + ) + + (instance $c (instantiate $C)) + (instance $d (instantiate $D + (with "run-yield" (func $c "run-yield")) + (with "run-yield-to" (func $c "run-yield-to")) + (with "run-suspend" (func $c "run-suspend")) + (with "run-switch-to" (func $c "run-switch-to")) + )) + (func (export "run") (alias export $d "run")) +) + +(assert_return (invoke "run") (u32.const 42)) \ No newline at end of file diff --git a/tests/misc_testsuite/component-model-threading/threading-builtins.wast b/tests/misc_testsuite/component-model-threading/threading-builtins.wast new file mode 100644 index 000000000000..2402f4bb505c --- /dev/null +++ b/tests/misc_testsuite/component-model-threading/threading-builtins.wast @@ -0,0 +1,102 @@ +;;! component_model_async = true +;;! component_model_threading = true + +;; Tests for basic functioning of all threading builtins with the implicit thread + one explicit thread +;; Switches between threads using all of the different threading intrinsics. + +(component + ;; Defines the table for the thread start function + (core module $libc + (table (export "__indirect_function_table") 1 funcref)) + ;; Defines the thread start function and a function that calls thread.new_indirect + (core module $m + ;; Import the threading builtins and the table from libc + (import "" "thread.new_indirect" (func $thread-new-indirect (param i32 i32) (result i32))) + (import "" "thread.suspend" (func $thread-suspend (result i32))) + (import "" "thread.yield-to" (func $thread-yield-to (param i32) (result i32))) + (import "" "thread.switch-to" (func $thread-switch-to (param i32) (result i32))) + (import "" "thread.yield" (func $thread-yield (result i32))) + (import "" "thread.index" (func $thread-index (result i32))) + (import "" "thread.resume-later" (func $thread-resume-later (param i32))) + (import "libc" "__indirect_function_table" (table $indirect-function-table 1 funcref)) + + ;; A global that we will set from the spawned thread + (global $g (mut i32) (i32.const 0)) + (global $main-thread-index (mut i32) (i32.const 0)) + + ;; The thread entry point, which sets the global to incrementing values starting from the context value + (func $thread-start (param i32) + ;; Set the global to the context value + (global.set $g (local.get 0)) + ;; The main thread switched to us, so is no longer scheduled, so we explicitly schedule it + (call $thread-resume-later (global.get $main-thread-index)) + ;; Yield back to the main thread (since that is the only other one) + (drop (call $thread-yield) + ;; Increment the global + (global.set $g (i32.add (global.get $g) (i32.const 1))) + ;; The main thread will have explicitly requested suspension, so yield to it directly + (drop (call $thread-yield-to (global.get $main-thread-index))) + ;; Increment the global again + (global.set $g (i32.add (global.get $g) (i32.const 1))) + ;; Reschedule the main thread so that it runs after we exit + (call $thread-resume-later (global.get $main-thread-index)))) + (export "thread-start" (func $thread-start)) + + ;; Initialize the function table with our thread-start function; this will be + ;; used by thread.new_indirect + (elem (table $indirect-function-table) (i32.const 0) func $thread-start) + + ;; The main entry point, which spawns a new thread to run `thread-start`, passing 42 + ;; as the context value, and then yields to it + (func (export "run") (result i32) + ;; Store the main thread's index for the spawned thread to yield to + (global.set $main-thread-index (call $thread-index)) + ;; Create a new thread, which starts suspended, and switch to it + (drop + (call $thread-switch-to + (call $thread-new-indirect (i32.const 0) (i32.const 42)))) + ;; After the thread yields back to us, check that the global was set to 42 + (if (i32.ne (global.get $g) (i32.const 42)) (then unreachable)) + ;; Suspend ourselves, which will cause the spawned thread to run + (drop (call $thread-suspend)) + ;; The spawned thread will resume us after incrementing the global, so check that it is now 43 + (if (i32.ne (global.get $g) (i32.const 43)) (then unreachable)) + ;; Suspend again, which will cause the spawned thread to run again + (drop (call $thread-suspend)) + ;; The spawned thread will reschedule us before it exits, so when we resume here the global should be 44 + (if (i32.ne (global.get $g) (i32.const 44)) (then unreachable)) + ;; Return success + (i32.const 42))) + + ;; Instantiate the libc module to get the table + (core instance $libc (instantiate $libc)) + ;; Get access to `thread.new_indirect` that uses the table from libc + (core type $start-func-ty (func (param i32))) + (alias core export $libc "__indirect_function_table" (core table $indirect-function-table)) + + (core func $thread-new-indirect + (canon thread.new_indirect $start-func-ty (table $indirect-function-table))) + (core func $thread-yield (canon thread.yield)) + (core func $thread-index (canon thread.index)) + (core func $thread-yield-to (canon thread.yield-to)) + (core func $thread-resume-later (canon thread.resume-later)) + (core func $thread-switch-to (canon thread.switch-to)) + (core func $thread-suspend (canon thread.suspend)) + + ;; Instantiate the main module + (core instance $i ( + instantiate $m + (with "" (instance + (export "thread.new_indirect" (func $thread-new-indirect)) + (export "thread.index" (func $thread-index)) + (export "thread.yield-to" (func $thread-yield-to)) + (export "thread.yield" (func $thread-yield)) + (export "thread.switch-to" (func $thread-switch-to)) + (export "thread.suspend" (func $thread-suspend)) + (export "thread.resume-later" (func $thread-resume-later)))) + (with "libc" (instance $libc)))) + + ;; Export the main entry point + (func (export "run") (result u32) (canon lift (core func $i "run")))) + +(assert_return (invoke "run") (u32.const 42)) \ No newline at end of file diff --git a/tests/misc_testsuite/component-model/async/futures-must-write.wast b/tests/misc_testsuite/component-model/async/futures-must-write.wast index 2158c888a8fe..3855558356dd 100644 --- a/tests/misc_testsuite/component-model/async/futures-must-write.wast +++ b/tests/misc_testsuite/component-model/async/futures-must-write.wast @@ -79,7 +79,7 @@ ;; call 'start-future' to get the future we'll be working with (local $fr i32) (local.set $fr (call $start-future)) - (if (i32.ne (i32.const 1) (local.get $fr)) + (if (i32.ne (i32.const 2) (local.get $fr)) (then unreachable)) ;; ok to immediately drop the readable end @@ -92,7 +92,7 @@ ;; call 'start-future' to get the future we'll be working with (local $fr i32) (local.set $fr (call $start-future)) - (if (i32.ne (i32.const 1) (local.get $fr)) + (if (i32.ne (i32.const 2) (local.get $fr)) (then unreachable)) ;; boom diff --git a/tests/misc_testsuite/component-model/async/partial-stream-copies.wast b/tests/misc_testsuite/component-model/async/partial-stream-copies.wast index e6410b0d1cad..314a614e0a62 100644 --- a/tests/misc_testsuite/component-model/async/partial-stream-copies.wast +++ b/tests/misc_testsuite/component-model/async/partial-stream-copies.wast @@ -43,16 +43,16 @@ ;; check the incoming readable stream end (global.set $insr (local.get 0)) - (if (i32.ne (i32.const 2) (global.get $insr)) + (if (i32.ne (i32.const 3) (global.get $insr)) (then unreachable)) ;; create a new stream r/w pair $outsr/$outsw (local.set $ret64 (call $stream.new)) (local.set $outsr (i32.wrap_i64 (local.get $ret64))) - (if (i32.ne (i32.const 3) (local.get $outsr)) + (if (i32.ne (i32.const 4) (local.get $outsr)) (then unreachable)) (global.set $outsw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) - (if (i32.ne (i32.const 4) (global.get $outsw)) + (if (i32.ne (i32.const 5) (global.get $outsw)) (then unreachable)) ;; start async read on $insr which will block @@ -161,10 +161,10 @@ ;; create a new stream r/w pair $insr/$insw (local.set $ret64 (call $stream.new)) (local.set $insr (i32.wrap_i64 (local.get $ret64))) - (if (i32.ne (i32.const 1) (local.get $insr)) + (if (i32.ne (i32.const 2) (local.get $insr)) (then unreachable)) (local.set $insw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) - (if (i32.ne (i32.const 2) (local.get $insw)) + (if (i32.ne (i32.const 3) (local.get $insw)) (then unreachable)) ;; call 'transform' which will return a readable stream $outsr eagerly @@ -173,7 +173,7 @@ (if (i32.ne (i32.const 2 (; RETURNED=2 | (0<<4) ;)) (local.get $ret)) (then unreachable)) (local.set $outsr (i32.load (local.get $retp))) - (if (i32.ne (i32.const 1) (local.get $outsr)) + (if (i32.ne (i32.const 2) (local.get $outsr)) (then unreachable)) ;; multiple write calls succeed until 12-byte buffer is filled diff --git a/tests/misc_testsuite/component-model/async/task-builtins.wast b/tests/misc_testsuite/component-model/async/task-builtins.wast index e2a22b023d52..3420f4c9122e 100644 --- a/tests/misc_testsuite/component-model/async/task-builtins.wast +++ b/tests/misc_testsuite/component-model/async/task-builtins.wast @@ -58,13 +58,13 @@ (core instance $i (instantiate $m (with "" (instance (export "waitable-set.poll" (func $waitable-set-poll)))))) ) -;; yield +;; thread.yield (component (core module $m - (import "" "yield" (func $yield (result i32))) + (import "" "thread.yield" (func $thread-yield (result i32))) ) - (core func $yield (canon thread.yield)) - (core instance $i (instantiate $m (with "" (instance (export "yield" (func $yield)))))) + (core func $thread-yield (canon thread.yield)) + (core instance $i (instantiate $m (with "" (instance (export "thread.yield" (func $thread-yield)))))) ) ;; subtask.drop diff --git a/tests/misc_testsuite/component-model/resources.wast b/tests/misc_testsuite/component-model/resources.wast index c9f94feca1cd..9c5ce16ac8b0 100644 --- a/tests/misc_testsuite/component-model/resources.wast +++ b/tests/misc_testsuite/component-model/resources.wast @@ -455,7 +455,7 @@ (instance $i (instantiate $inner)) ;; the rest of this component which is a single function that invokes `drop` - ;; for index 1. The index 1 should be valid within the above component, but + ;; for index 2. The index 2 should be valid within the above component, but ;; it is not valid within this component (alias export $i "r" (type $r)) (core func $drop (canon resource.drop $r)) @@ -464,7 +464,7 @@ (import "" "drop" (func $drop (param i32))) (func (export "f") - (call $drop (i32.const 1)) + (call $drop (i32.const 2)) ) ) (core instance $i (instantiate $m @@ -476,7 +476,7 @@ (func (export "f") (canon lift (core func $i "f"))) ) -(assert_trap (invoke "f") "unknown handle index 1") +(assert_trap (invoke "f") "unknown handle index 2") ;; Each instantiation of a component generates a unique resource type, so ;; allocating in one component and deallocating in another should fail. @@ -492,10 +492,10 @@ (import "" "drop" (func $drop (param i32))) (func (export "alloc") - (if (i32.ne (call $ctor (i32.const 100)) (i32.const 1)) (then (unreachable))) + (if (i32.ne (call $ctor (i32.const 100)) (i32.const 2)) (then (unreachable))) ) (func (export "dealloc") - (call $drop (i32.const 1)) + (call $drop (i32.const 2)) ) ) (core instance $i (instantiate $m @@ -552,10 +552,10 @@ (import "" "drop" (func $drop (param i32))) (func (export "alloc") - (if (i32.ne (call $ctor (i32.const 100)) (i32.const 1)) (then (unreachable))) + (if (i32.ne (call $ctor (i32.const 100)) (i32.const 2)) (then (unreachable))) ) (func (export "dealloc") - (call $drop (i32.const 1)) + (call $drop (i32.const 2)) ) ) (core instance $i (instantiate $m @@ -1025,8 +1025,8 @@ (local.set $r1 (call $ctor (i32.const 100))) (local.set $r2 (call $ctor (i32.const 200))) - (if (i32.ne (local.get $r1) (i32.const 1)) (then (unreachable))) - (if (i32.ne (local.get $r2) (i32.const 2)) (then (unreachable))) + (if (i32.ne (local.get $r1) (i32.const 2)) (then (unreachable))) + (if (i32.ne (local.get $r2) (i32.const 3)) (then (unreachable))) (call $assert-borrow (local.get $r2) (i32.const 200)) (call $assert-borrow (local.get $r1) (i32.const 100)) @@ -1086,6 +1086,6 @@ ) (component instance $C1 $C) -(assert_trap (invoke "drop-r1-as-r2") "handle index 1 used with the wrong type, expected guest-defined resource but found a different guest-defined resource") +(assert_trap (invoke "drop-r1-as-r2") "handle index 2 used with the wrong type, expected guest-defined resource but found a different guest-defined resource") (component instance $C1 $C) -(assert_trap (invoke "return-r1-as-r2") "handle index 1 used with the wrong type, expected guest-defined resource but found a different guest-defined resource") +(assert_trap (invoke "return-r1-as-r2") "handle index 2 used with the wrong type, expected guest-defined resource but found a different guest-defined resource")