Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

threads: add thread.spawn_indirect #2042

Merged
merged 5 commits into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/wasm-encoder/src/component/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,9 @@ impl ComponentBuilder {
inc(&mut self.core_funcs)
}

/// Declares a new `thread.spawn` intrinsic.
pub fn thread_spawn(&mut self, ty: u32) -> u32 {
self.canonical_functions().thread_spawn(ty);
/// Declares a new `thread.spawn_ref` intrinsic.
pub fn thread_spawn_ref(&mut self, ty: u32) -> u32 {
self.canonical_functions().thread_spawn_ref(ty);
inc(&mut self.core_funcs)
}

Expand Down
18 changes: 14 additions & 4 deletions crates/wasm-encoder/src/component/canonicals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,19 +158,29 @@ impl CanonicalFunctionSection {
self
}

/// Defines a function which will spawns a new thread by invoking a shared
/// Defines a function which will spawn a new thread by invoking a shared
/// function of type `ty_index`.
pub fn thread_spawn(&mut self, ty_index: u32) -> &mut Self {
self.bytes.push(0x05);
pub fn thread_spawn_ref(&mut self, ty_index: u32) -> &mut Self {
self.bytes.push(0x40);
ty_index.encode(&mut self.bytes);
self.num_added += 1;
self
}

/// Defines a function which will spawn a new thread by invoking a shared
/// function indirectly through a `funcref` table.
pub fn thread_spawn_indirect(&mut self, ty_index: u32, table_index: u32) -> &mut Self {
self.bytes.push(0x41);
ty_index.encode(&mut self.bytes);
table_index.encode(&mut self.bytes);
self.num_added += 1;
self
}

/// Defines a function which will return the number of threads that can be
/// expected to execute concurrently.
pub fn thread_available_parallelism(&mut self) -> &mut Self {
self.bytes.push(0x06);
self.bytes.push(0x42);
self.num_added += 1;
self
}
Expand Down
12 changes: 10 additions & 2 deletions crates/wasm-encoder/src/reencode/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -953,9 +953,17 @@ pub mod component_utils {
let resource = reencoder.component_type_index(resource);
section.resource_rep(resource);
}
wasmparser::CanonicalFunction::ThreadSpawn { func_ty_index } => {
wasmparser::CanonicalFunction::ThreadSpawnRef { func_ty_index } => {
let func_ty = reencoder.type_index(func_ty_index);
section.thread_spawn(func_ty);
section.thread_spawn_ref(func_ty);
}
wasmparser::CanonicalFunction::ThreadSpawnIndirect {
func_ty_index,
table_index,
} => {
let func_ty = reencoder.type_index(func_ty_index);
let table_index = reencoder.table_index(table_index);
section.thread_spawn_indirect(func_ty, table_index);
}
wasmparser::CanonicalFunction::ThreadAvailableParallelism => {
section.thread_available_parallelism();
Expand Down
24 changes: 18 additions & 6 deletions crates/wasmparser/src/readers/component/canonicals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,18 @@ pub enum CanonicalFunction {
resource: u32,
},
/// A function which spawns a new thread by invoking the shared function.
ThreadSpawn {
/// The index of the function to spawn.
ThreadSpawnRef {
/// The index of the function type to spawn.
func_ty_index: u32,
},
/// A function which spawns a new thread by invoking the shared function
/// passed as an index into a `funcref` table.
ThreadSpawnIndirect {
/// The index of the function type to spawn.
func_ty_index: u32,
/// The index of the table to use for the indirect spawn.
table_index: u32,
},
/// A function which returns the number of threads that can be expected to
/// execute concurrently
ThreadAvailableParallelism,
Expand Down Expand Up @@ -277,10 +285,6 @@ impl<'a> FromReader<'a> for CanonicalFunction {
0x04 => CanonicalFunction::ResourceRep {
resource: reader.read()?,
},
0x05 => CanonicalFunction::ThreadSpawn {
func_ty_index: reader.read()?,
},
0x06 => CanonicalFunction::ThreadAvailableParallelism,
0x08 => CanonicalFunction::BackpressureSet,
0x09 => CanonicalFunction::TaskReturn {
result: crate::read_resultlist(reader)?,
Expand Down Expand Up @@ -355,6 +359,14 @@ impl<'a> FromReader<'a> for CanonicalFunction {
},
0x22 => CanonicalFunction::WaitableSetDrop,
0x23 => CanonicalFunction::WaitableJoin,
0x40 => CanonicalFunction::ThreadSpawnRef {
func_ty_index: reader.read()?,
},
0x41 => CanonicalFunction::ThreadSpawnIndirect {
func_ty_index: reader.read()?,
table_index: reader.read()?,
},
0x42 => CanonicalFunction::ThreadAvailableParallelism,
x => return reader.invalid_leading_byte(x, "canonical function"),
})
}
Expand Down
92 changes: 75 additions & 17 deletions crates/wasmparser/src/validator/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -987,9 +987,13 @@ impl ComponentState {
CanonicalFunction::ResourceRep { resource } => {
self.resource_rep(resource, types, offset)
}
CanonicalFunction::ThreadSpawn { func_ty_index } => {
self.thread_spawn(func_ty_index, types, offset, features)
CanonicalFunction::ThreadSpawnRef { func_ty_index } => {
self.thread_spawn_ref(func_ty_index, types, offset, features)
}
CanonicalFunction::ThreadSpawnIndirect {
func_ty_index,
table_index,
} => self.thread_spawn_indirect(func_ty_index, table_index, types, offset, features),
CanonicalFunction::ThreadAvailableParallelism => {
self.thread_available_parallelism(types, offset, features)
}
Expand Down Expand Up @@ -1924,7 +1928,7 @@ impl ComponentState {
bail!(offset, "type index {} is not a resource type", idx)
}

fn thread_spawn(
fn thread_spawn_ref(
&mut self,
func_ty_index: u32,
types: &mut TypeAlloc,
Expand All @@ -1934,11 +1938,76 @@ impl ComponentState {
if !features.shared_everything_threads() {
bail!(
offset,
"`thread.spawn` requires the shared-everything-threads proposal"
"`thread.spawn_ref` requires the shared-everything-threads proposal"
)
}
let core_type_id = self.validate_spawn_type(func_ty_index, types, offset)?;

// Validate the type accepted by `thread.spawn`.
// Insert the core function.
let packed_index = PackedIndex::from_id(core_type_id).ok_or_else(|| {
format_err!(offset, "implementation limit: too many types in `TypeList`")
})?;
let start_func_ref = RefType::concrete(true, packed_index);
let func_ty = FuncType::new([ValType::Ref(start_func_ref), ValType::I32], [ValType::I32]);
let core_ty = SubType::func(func_ty, true);
let id = types.intern_sub_type(core_ty, offset);
self.core_funcs.push(id);

Ok(())
}

fn thread_spawn_indirect(
&mut self,
func_ty_index: u32,
table_index: u32,
types: &mut TypeAlloc,
offset: usize,
features: &WasmFeatures,
) -> Result<()> {
if !features.shared_everything_threads() {
bail!(
offset,
"`thread.spawn_indirect` requires the shared-everything-threads proposal"
)
}
let _ = self.validate_spawn_type(func_ty_index, types, offset)?;

// Check this much like `call_indirect` (see
// `OperatorValidatorTemp::check_call_indirect_ty`), but loosen the
// table type restrictions to just a `funcref`. See the component model
// for more details:
// https://github.com/WebAssembly/component-model/blob/6e08e283/design/mvp/CanonicalABI.md#-canon-threadspawn_indirect.
let table = self.table_at(table_index, offset)?;
// TODO: check that the table is shared once components allow shared
// tables.
Comment on lines +1981 to +1982
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this TODO still applicable?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I added it because we still prevent shared tables from being used in components. At some point we will have to relax that but it seems like a separate PR. Speaking of: when can or should we relax that? We have this not-accepted.wast file that gates some of this but I can't fully remember why we added it:

;; RUN: wast --assert default --snapshot tests/snapshots % -f shared-everything-threads
(assert_invalid
(component
(core module $A
(memory (export "m") 1 2 shared))
(core instance $A (instantiate $A))
(alias core export $A "m" (core memory $m))
)
"shared linear memories are not compatible with components yet")
(assert_invalid
(component
(core module $A
(table (export "m") shared 1 2 (ref null (shared func)))
)
(core instance $A (instantiate $A))
(alias core export $A "m" (core table $m))
)
"shared tables are not compatible with components yet")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that was due to #1970. What I added there was a very coarse version of "you can't lower into a 64-bit memory".

What really needs to happen is that every location that uses a memory (or table) needs to perform a subtype check on what the actual table/memory received is. For example all canonical options referring to a memory have to refer to a non-shared 32-bit linear memory.

Basically #1970 was a coarse over-approximation that didn't break anyone "on stable". To remove the logic from #1970 we'll need to audit usage of memory_at and table_at to ensure everything is doing a subtype check as appropriate.

let expected_ty = RefType::FUNCREF
.shared()
.expect("a funcref can always be shared");
if table.element_type != expected_ty {
bail!(offset, "expected a table of shared `funcref`");
}

// Insert the core function.
let func_ty = FuncType::new([ValType::I32, ValType::I32], [ValType::I32]);
let core_ty = SubType::func(func_ty, true);
let id = types.intern_sub_type(core_ty, offset);
self.core_funcs.push(id);

Ok(())
}

/// Validates the type of a `thread.spawn*` instruction.
///
/// This is currently limited to shared functions with the signature `[i32]
/// -> []`. See component model [explanation] for more details.
///
/// [explanation]: https://github.com/WebAssembly/component-model/blob/6e08e283/design/mvp/CanonicalABI.md#-canon-threadspawn_ref
fn validate_spawn_type(
&self,
func_ty_index: u32,
types: &TypeAlloc,
offset: usize,
) -> Result<CoreTypeId> {
let core_type_id = match self.core_type_at(func_ty_index, offset)? {
ComponentCoreTypeId::Sub(c) => c,
ComponentCoreTypeId::Module(_) => bail!(offset, "expected a core function type"),
Expand All @@ -1961,18 +2030,7 @@ impl ComponentState {
}
_ => bail!(offset, "spawn type must be a function"),
}

// Insert the core function.
let packed_index = PackedIndex::from_id(core_type_id).ok_or_else(|| {
format_err!(offset, "implementation limit: too many types in `TypeList`")
})?;
let start_func_ref = RefType::concrete(true, packed_index);
let func_ty = FuncType::new([ValType::Ref(start_func_ref), ValType::I32], [ValType::I32]);
let core_ty = SubType::func(func_ty, true);
let id = types.intern_sub_type(core_ty, offset);
self.core_funcs.push(id);

Ok(())
Ok(core_type_id)
}

fn thread_available_parallelism(
Expand Down
35 changes: 18 additions & 17 deletions crates/wasmprinter/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -892,26 +892,27 @@ impl Printer<'_, '_> {
me.print_idx(&state.component.type_names, resource)
})?;
}
CanonicalFunction::ThreadSpawn {
func_ty_index: func_index,
CanonicalFunction::ThreadSpawnRef { func_ty_index } => {
self.print_intrinsic(state, "canon thread.spawn_ref ", &|me, state| {
me.print_idx(&state.core.type_names, func_ty_index)
})?;
}
CanonicalFunction::ThreadSpawnIndirect {
func_ty_index,
table_index,
} => {
self.start_group("core func ")?;
self.print_name(&state.core.func_names, state.core.funcs)?;
self.result.write_str(" ")?;
self.start_group("canon thread.spawn ")?;
self.print_idx(&state.core.type_names, func_index)?;
self.end_group()?;
self.end_group()?;
state.core.funcs += 1;
self.print_intrinsic(state, "canon thread.spawn_indirect ", &|me, state| {
me.print_idx(&state.core.type_names, func_ty_index)?;
me.result.write_str(" ")?;
me.start_group("table ")?;
me.print_idx(&state.core.table_names, table_index)?;
me.end_group()
})?;
}
CanonicalFunction::ThreadAvailableParallelism => {
self.start_group("core func ")?;
self.print_name(&state.core.func_names, state.core.funcs)?;
self.result.write_str(" ")?;
self.start_group("canon thread.available_parallelism")?;
self.end_group()?;
self.end_group()?;
state.core.funcs += 1;
self.print_intrinsic(state, "canon thread.available_parallelism", &|_, _| {
Ok(())
})?;
}
CanonicalFunction::BackpressureSet => {
self.print_intrinsic(state, "canon backpressure.set", &|_, _| Ok(()))?;
Expand Down
9 changes: 7 additions & 2 deletions crates/wast/src/component/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,14 @@ impl<'a> Encoder<'a> {
self.core_func_names.push(name);
self.funcs.resource_rep(info.ty.into());
}
CoreFuncKind::ThreadSpawn(info) => {
CoreFuncKind::ThreadSpawnRef(info) => {
self.core_func_names.push(name);
self.funcs.thread_spawn(info.ty.into());
self.funcs.thread_spawn_ref(info.ty.into());
}
CoreFuncKind::ThreadSpawnIndirect(info) => {
self.core_func_names.push(name);
self.funcs
.thread_spawn_indirect(info.ty.into(), info.table.idx.into());
}
CoreFuncKind::ThreadAvailableParallelism(_info) => {
self.core_func_names.push(name);
Expand Down
37 changes: 30 additions & 7 deletions crates/wast/src/component/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ pub enum CoreFuncKind<'a> {
ResourceNew(CanonResourceNew<'a>),
ResourceDrop(CanonResourceDrop<'a>),
ResourceRep(CanonResourceRep<'a>),
ThreadSpawn(CanonThreadSpawn<'a>),
ThreadSpawnRef(CanonThreadSpawnRef<'a>),
ThreadSpawnIndirect(CanonThreadSpawnIndirect<'a>),
ThreadAvailableParallelism(CanonThreadAvailableParallelism),
BackpressureSet,
TaskReturn(CanonTaskReturn<'a>),
Expand Down Expand Up @@ -109,8 +110,10 @@ impl<'a> CoreFuncKind<'a> {
Ok(CoreFuncKind::ResourceDrop(parser.parse()?))
} else if l.peek::<kw::resource_rep>()? {
Ok(CoreFuncKind::ResourceRep(parser.parse()?))
} else if l.peek::<kw::thread_spawn>()? {
Ok(CoreFuncKind::ThreadSpawn(parser.parse()?))
} else if l.peek::<kw::thread_spawn_ref>()? {
Ok(CoreFuncKind::ThreadSpawnRef(parser.parse()?))
} else if l.peek::<kw::thread_spawn_indirect>()? {
Ok(CoreFuncKind::ThreadSpawnIndirect(parser.parse()?))
} else if l.peek::<kw::thread_available_parallelism>()? {
Ok(CoreFuncKind::ThreadAvailableParallelism(parser.parse()?))
} else if l.peek::<kw::backpressure_set>()? {
Expand Down Expand Up @@ -471,23 +474,43 @@ impl<'a> Parse<'a> for CanonResourceRep<'a> {
}
}

/// Information relating to the `thread.spawn` intrinsic.
/// Information relating to the `thread.spawn_ref` intrinsic.
#[derive(Debug)]
pub struct CanonThreadSpawn<'a> {
pub struct CanonThreadSpawnRef<'a> {
/// The function type that is being spawned.
pub ty: Index<'a>,
}

impl<'a> Parse<'a> for CanonThreadSpawn<'a> {
impl<'a> Parse<'a> for CanonThreadSpawnRef<'a> {
fn parse(parser: Parser<'a>) -> Result<Self> {
parser.parse::<kw::thread_spawn>()?;
parser.parse::<kw::thread_spawn_ref>()?;

Ok(Self {
ty: parser.parse()?,
})
}
}

/// Information relating to the `thread.spawn_indirect` intrinsic.
///
/// This should look quite similar to parsing of `CallIndirect`.
#[derive(Debug)]
pub struct CanonThreadSpawnIndirect<'a> {
/// The function type that is being spawned.
pub ty: Index<'a>,
/// The table that this spawn is going to be indexing.
pub table: CoreItemRef<'a, kw::table>,
}

impl<'a> Parse<'a> for CanonThreadSpawnIndirect<'a> {
fn parse(parser: Parser<'a>) -> Result<Self> {
parser.parse::<kw::thread_spawn_indirect>()?;
let ty = parser.parse()?;
let table = parser.parens(|p| p.parse())?;
Ok(Self { ty, table })
}
}

/// Information relating to the `thread.spawn` intrinsic.
#[derive(Debug)]
pub struct CanonThreadAvailableParallelism;
Expand Down
7 changes: 6 additions & 1 deletion crates/wast/src/component/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,9 +387,13 @@ impl<'a> Resolver<'a> {
CoreFuncKind::ResourceDrop(info) => {
self.resolve_ns(&mut info.ty, Ns::Type)?;
}
CoreFuncKind::ThreadSpawn(info) => {
CoreFuncKind::ThreadSpawnRef(info) => {
self.resolve_ns(&mut info.ty, Ns::CoreType)?;
}
CoreFuncKind::ThreadSpawnIndirect(info) => {
self.resolve_ns(&mut info.ty, Ns::CoreType)?;
self.core_item_ref(&mut info.table)?;
}
CoreFuncKind::ThreadAvailableParallelism(_)
| CoreFuncKind::BackpressureSet
| CoreFuncKind::Yield(_)
Expand Down Expand Up @@ -1094,6 +1098,7 @@ component_item!(kw::module, CoreModule);

core_item!(kw::func, CoreFunc);
core_item!(kw::memory, CoreMemory);
core_item!(kw::table, CoreTable);
core_item!(kw::r#type, CoreType);
core_item!(kw::r#instance, CoreInstance);

Expand Down
Loading