Skip to content

Commit 455344b

Browse files
feat: add support for atomic waiters / notify
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent 455ffd4 commit 455344b

11 files changed

Lines changed: 312 additions & 55 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Added WebAssembly threads support, including atomic wait/notify and cross-store shared memory with `MemoryShared`.
1213
- Added new `Memory::data` and `Memory::data_mut` methods for direct access to linear memory.
1314
- Added several internal methods to support a future c-api crate
1415

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ See the [examples](./examples) directory and [documentation](https://docs.rs/tin
4646
## Cargo Features
4747

4848
- **`full`:** Enables `archive`, `debug`, `parallel-parser`, `parser`, `state`, and `validate`. Enabled by default.
49-
- **`std`:** Enables `std` and parsing from files and streams. Enabled by default.
49+
- **`std`:** Enables parsing from files and streams, shared memory handles, and blocking atomic wait/notify across stores. Enabled by default.
5050
- **`parser`:** Enables `tinywasm-parser` and top-level parse helpers. Enabled by default.
5151
- **`validate`:** Enables WebAssembly validation while parsing. Enabled by default and configurable through [`ParserOptions`](https://docs.rs/tinywasm/latest/tinywasm/parser/struct.ParserOptions.html).
5252
- **`parallel-parser`:** Parallelizes function parsing when `std` is enabled. Enabled by default.
@@ -104,8 +104,8 @@ TinyWasm generally implements non-JavaScript core proposals at [phase 4](https:/
104104
| [**Garbage Collection**](https://github.com/WebAssembly/gc) | 🟢 | 0.11.0 |
105105
| [**Exception Handling**](https://github.com/WebAssembly/exception-handling) | 🟢 | 0.11.0 |
106106
| [**Compact Import Section**](https://github.com/WebAssembly/compact-import-section) | 🟢 | 0.11.0 |
107+
| [**Threads**](https://github.com/WebAssembly/threads) | 🚧 | next |
107108
| [**Stack Switching**](https://github.com/WebAssembly/stack-switching) | 🌑 | - |
108-
| [**Threads**](https://github.com/WebAssembly/threads) | 🌑 | - |
109109

110110
**Legend**\
111111
🌑 -- not available\

‎crates/parser/src/visit.rs‎

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ use alloc::{
1111
vec::Vec,
1212
};
1313
use tinywasm_types::{
14-
AtomicArg, AtomicOp, AtomicWidth, BinOp, BinOp128, CmpOp, ExceptionHandler, Global, Import, ImportKind,
15-
Instruction, MemoryType, Operand64, Operand64Idx, Operand128, Operand128Idx, StorageType, TableDefinition, TagType,
16-
TypeSection, ValueCounts, ValueLane, WasmFunctionData,
14+
AtomicArg, AtomicOp, AtomicWaitOp, AtomicWidth, BinOp, BinOp128, CmpOp, ExceptionHandler, Global, Import,
15+
ImportKind, Instruction, MemoryType, Operand64, Operand64Idx, Operand128, Operand128Idx, StorageType,
16+
TableDefinition, TagType, TypeSection, ValueCounts, ValueLane, WasmFunctionData,
1717
};
1818
use wasmparser::{FunctionBody, OperatorsReader, OperatorsReaderAllocations, VisitSimdOperator};
1919

@@ -231,6 +231,21 @@ impl<'a> FunctionBuilder<'a> {
231231
self.uses_local_memory |= memory >= self.metadata.imported_memories;
232232
}
233233

234+
fn atomic_wait(&mut self, memarg: wasmparser::MemArg, width: u8, op: AtomicWaitOp) -> Result<()> {
235+
if memarg.align != width.trailing_zeros() as u8 {
236+
return Err(crate::ParseError::Other("invalid atomic alignment".into()));
237+
}
238+
let address = self.metadata.memory_size(memarg.memory)?;
239+
self.mark_memory(memarg.memory);
240+
let memory = self.push128(Operand128::<tinywasm_types::MemoryOperand>::new(memarg.offset, memarg.memory))?;
241+
let inputs: &[ValueLane] = match op {
242+
AtomicWaitOp::Notify => &[address, ValueLane::S32],
243+
AtomicWaitOp::Wait32 => &[address, ValueLane::S32, ValueLane::S64],
244+
AtomicWaitOp::Wait64 => &[address, ValueLane::S64, ValueLane::S64],
245+
};
246+
self.emit(inputs, &[ValueLane::S32], Instruction::AtomicWait(memory, op))
247+
}
248+
234249
fn visit_struct_get_impl(
235250
&mut self,
236251
type_index: u32,
@@ -539,14 +554,14 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> {
539554
self.emit(&[], &[], Instruction::AtomicFence)
540555
}
541556

542-
fn visit_memory_atomic_notify(&mut self, _: wasmparser::MemArg) -> Self::Output {
543-
Err(crate::ParseError::UnsupportedOperator("memory.atomic.notify".into()))
557+
fn visit_memory_atomic_notify(&mut self, memarg: wasmparser::MemArg) -> Self::Output {
558+
self.atomic_wait(memarg, 4, AtomicWaitOp::Notify)
544559
}
545-
fn visit_memory_atomic_wait32(&mut self, _: wasmparser::MemArg) -> Self::Output {
546-
Err(crate::ParseError::UnsupportedOperator("memory.atomic.wait32".into()))
560+
fn visit_memory_atomic_wait32(&mut self, memarg: wasmparser::MemArg) -> Self::Output {
561+
self.atomic_wait(memarg, 4, AtomicWaitOp::Wait32)
547562
}
548-
fn visit_memory_atomic_wait64(&mut self, _: wasmparser::MemArg) -> Self::Output {
549-
Err(crate::ParseError::UnsupportedOperator("memory.atomic.wait64".into()))
563+
fn visit_memory_atomic_wait64(&mut self, memarg: wasmparser::MemArg) -> Self::Output {
564+
self.atomic_wait(memarg, 8, AtomicWaitOp::Wait64)
550565
}
551566

552567
atomic_visitors! {

‎crates/tinywasm/Cargo.toml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ name = "test-wasm-multi-memory"
4242
[[test]]
4343
harness = false
4444
name = "test-wasm-threads"
45-
test = false
4645

4746
[[test]]
4847
harness = false

‎crates/tinywasm/src/interpreter/executor/instructions.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ macro_rules! instruction_handlers {
324324
MemorySize(addr) => executor.exec_memory_size(*addr)?,
325325
MemoryGrow(addr) => executor.exec_memory_grow(*addr)?,
326326
Atomic(arg) => executor.exec_atomic(*arg)?,
327+
AtomicWait(memory, op) => executor.exec_atomic_wait(*memory, *op)?,
327328
AtomicFence => crate::shared::fence(crate::shared::Ordering::SeqCst),
328329

329330
// Bulk memory operations

‎crates/tinywasm/src/interpreter/executor/mod.rs‎

Lines changed: 72 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -561,10 +561,7 @@ impl<'store> Executor<'store> {
561561
fn throw_exception(&mut self, exception: ValueRef, protected_ip: usize) -> ExecResult<ExecFlow> {
562562
match self.dispatch_exception(exception, protected_ip)? {
563563
Some(landing_pad) => Ok(ExecFlow::next(landing_pad)),
564-
None => match self.store.root_exception(exception) {
565-
Ok(exception) => Err(Error::Exception(exception).into()),
566-
Err(error) => Err(error.into()),
567-
},
564+
None => Err(Error::Exception(self.store.root_exception(exception)?).into()),
568565
}
569566
}
570567

@@ -734,6 +731,7 @@ impl<'store> Executor<'store> {
734731
let type_addr = operand.a();
735732
let table_addr = operand.b();
736733
self.charge_call_fuel(FUEL_COST_CALL_TOTAL);
734+
737735
// verify that the table is of the right type, this should be validated by the parser already
738736
let table_addr = self.module.resolve_table_addr(table_addr);
739737
let table_idx = self.pop_table_operand(self.store.state.get_table(table_addr).kind.arch())?;
@@ -1100,14 +1098,13 @@ impl<'store> Executor<'store> {
11001098
let type_index = operand.a();
11011099
let len = operand.b();
11021100
let type_addr = self.module.resolve_type_addr(type_index);
1103-
let storage =
1104-
self.store.state.get_type(type_addr).as_array().expect("validated array.new_fixed type").field.storage;
1101+
let arr_type = self.store.state.get_type(type_addr).as_array().expect("validated array.new_fixed type");
11051102
let len = len as usize;
11061103
self.store.state.gc.check_allocation(len, self.store.state.gc_type_has_references(type_addr))?;
11071104
let mut values = Vec::new();
11081105
cold_err!(values.try_reserve_exact(len)).map_err(|_| Trap::OutOfMemory)?;
11091106
for _ in 0..len {
1110-
values.push(pop_value(&mut self.store.value_stack, storage));
1107+
values.push(pop_value(&mut self.store.value_stack, arr_type.field.storage));
11111108
}
11121109
values.reverse();
11131110
self.push_gc_object(type_addr, values)
@@ -1257,8 +1254,8 @@ impl<'store> Executor<'store> {
12571254
let object = self.store.state.gc_object(reference, type_addr)?;
12581255
let object_len = self.store.state.gc.get_handle(object).expect("validated array").values.len();
12591256
dst.checked_add(len).filter(|end| *end <= object_len).ok_or(Trap::ArrayOutOfBounds)?;
1260-
let items =
1261-
self.store.state.elements[self.module.resolve_elem_addr(elem_index) as usize].items_range(src, len)?;
1257+
let elem_addr = self.module.resolve_elem_addr(elem_index);
1258+
let items = self.store.state.elements[elem_addr as usize].items_range(src, len)?;
12621259
let mut values = Vec::new();
12631260
cold_err!(values.try_reserve_exact(len)).map_err(|_| Trap::OutOfMemory)?;
12641261
values.extend(items.iter().copied().map(RuntimeValue::ValueRef));
@@ -1267,11 +1264,9 @@ impl<'store> Executor<'store> {
12671264
}
12681265

12691266
fn exec_memory_size(&mut self, addr: u32) -> Result<(), Trap> {
1270-
let mem_addr = self.mem_addr(addr);
1271-
let (arch, pages) = self.store.state.memory_size(mem_addr);
1272-
match arch {
1273-
MemoryArch::I64 => i64::stack_push(&mut self.store.value_stack, pages as i64),
1274-
MemoryArch::I32 => i32::stack_push(&mut self.store.value_stack, pages as i32),
1267+
match self.store.state.memory_size(self.mem_addr(addr)) {
1268+
(MemoryArch::I64, pages) => i64::stack_push(&mut self.store.value_stack, pages as i64),
1269+
(MemoryArch::I32, pages) => i32::stack_push(&mut self.store.value_stack, pages as i32),
12751270
}
12761271
}
12771272

@@ -1441,24 +1436,65 @@ impl<'store> Executor<'store> {
14411436
}
14421437
}
14431438

1439+
fn exec_atomic_wait(&mut self, arg: Operand128Idx<MemoryOperand>, op: AtomicWaitOp) -> Result<(), Trap> {
1440+
let timeout = if op == AtomicWaitOp::Notify { 0 } else { i64::stack_pop(&mut self.store.value_stack) };
1441+
let value = match op {
1442+
AtomicWaitOp::Wait64 => u64::stack_pop(&mut self.store.value_stack),
1443+
_ => u32::stack_pop(&mut self.store.value_stack) as u64,
1444+
};
1445+
let memory = arg.resolve(&self.func.data);
1446+
let mem_addr = self.mem_addr(memory.memory());
1447+
let width = if op == AtomicWaitOp::Wait64 { 8 } else { 4 };
1448+
let addr = crate::store::with_memory!(self.store.state, mem_addr, |mem, kind| {
1449+
let base = self.store.value_stack.pop_memory_operand(kind.arch())?;
1450+
let addr = cold_err!(mem.effective_addr::<1>(base, memory.offset()))?;
1451+
if addr % width != 0 {
1452+
return cold!(Err(Trap::UnalignedAtomic));
1453+
}
1454+
1455+
if op != AtomicWaitOp::Notify && !kind.shared() {
1456+
return cold!(Err(Trap::Other("atomic wait requires shared memory")));
1457+
}
1458+
1459+
// Check the entire access even for notify and even when no waiter exists.
1460+
if mem.checked_range(addr, width).is_none() {
1461+
return cold!(Err(Trap::MemoryOutOfBounds { offset: addr, len: width, max: mem.len() }));
1462+
}
1463+
1464+
Ok(addr)
1465+
})?;
1466+
1467+
#[cfg(not(feature = "std"))]
1468+
let _ = (timeout, value, addr);
1469+
1470+
#[cfg(feature = "std")]
1471+
if mem_addr & crate::store::SHARED_MEM_BIT != 0 {
1472+
let shared = &self.store.state.shared_memories[(mem_addr & !crate::store::SHARED_MEM_BIT) as usize];
1473+
let result = match op {
1474+
AtomicWaitOp::Notify => shared.notify(addr, value as u32),
1475+
AtomicWaitOp::Wait32 => shared.wait::<4>(addr, value, timeout)?,
1476+
AtomicWaitOp::Wait64 => shared.wait::<8>(addr, value, timeout)?,
1477+
};
1478+
return u32::stack_push(&mut self.store.value_stack, result);
1479+
}
1480+
1481+
u32::stack_push(&mut self.store.value_stack, 0)
1482+
}
1483+
14441484
fn exec_atomic_width<const N: usize>(&mut self, arg: AtomicArg) -> Result<(), Trap> {
14451485
let op = arg.op();
1446-
let value = if op == AtomicOp::Load {
1447-
0
1448-
} else if arg.is_64() {
1449-
u64::stack_pop(&mut self.store.value_stack)
1450-
} else {
1451-
u32::stack_pop(&mut self.store.value_stack) as u64
1486+
let value = match op {
1487+
AtomicOp::Load => 0,
1488+
_ if arg.is_64() => u64::stack_pop(&mut self.store.value_stack),
1489+
_ => u32::stack_pop(&mut self.store.value_stack) as u64,
14521490
};
1453-
let expected = if op == AtomicOp::Cmpxchg {
1454-
if arg.is_64() {
1455-
u64::stack_pop(&mut self.store.value_stack)
1456-
} else {
1457-
u32::stack_pop(&mut self.store.value_stack) as u64
1458-
}
1459-
} else {
1460-
0
1491+
1492+
let expected = match op {
1493+
AtomicOp::Cmpxchg if arg.is_64() => u64::stack_pop(&mut self.store.value_stack),
1494+
AtomicOp::Cmpxchg => u32::stack_pop(&mut self.store.value_stack) as u64,
1495+
_ => 0,
14611496
};
1497+
14621498
let memory = arg.memory.resolve(&self.func.data);
14631499
let mem_addr = self.mem_addr(memory.memory());
14641500
crate::store::with_memory!(self.store.state, mem_addr, |mem, kind| {
@@ -1493,10 +1529,10 @@ impl<'store> Executor<'store> {
14931529
bytes.copy_from_slice(&next.to_le_bytes()[..N]);
14941530
cold_err!(mem.write_fixed::<N>(addr, &bytes))?;
14951531
}
1496-
if arg.is_64() {
1497-
u64::stack_push(&mut self.store.value_stack, old)?;
1498-
} else {
1499-
u32::stack_push(&mut self.store.value_stack, old as u32)?;
1532+
1533+
match arg.is_64() {
1534+
true => u64::stack_push(&mut self.store.value_stack, old)?,
1535+
false => u32::stack_push(&mut self.store.value_stack, old as u32)?,
15001536
}
15011537
Ok(())
15021538
})
@@ -1580,19 +1616,16 @@ impl<'store> Executor<'store> {
15801616

15811617
fn exec_table_init(&mut self, index: Operand64Idx<(u32, u32)>) -> Result<(), Trap> {
15821618
let operand = index.resolve(&self.func.data);
1583-
let elem_index = operand.a();
1584-
let table_index = operand.b();
15851619
let size = self.pop_table_operand(MemoryArch::I32)?; // n
15861620
let offset = self.pop_table_operand(MemoryArch::I32)?; // s
1587-
let table_addr = self.module.resolve_table_addr(table_index);
1621+
let table_addr = self.module.resolve_table_addr(operand.b());
15881622
let dst = self.pop_table_operand(self.store.state.get_table(table_addr).kind.arch())?; // d
1589-
let elem_addr = self.module.resolve_elem_addr(elem_index) as usize;
1623+
let elem_addr = self.module.resolve_elem_addr(operand.a()) as usize;
15901624
let elem = self.store.state.elements.get(elem_addr).ok_or_else(|| Trap::Other("element not found"))?;
15911625
let items = elem.items_range(offset, size)?;
15921626

1593-
let table =
1594-
self.store.state.tables.get_mut(table_addr as usize).ok_or_else(|| Trap::Other("table not found"))?;
1595-
table.init(dst, items)
1627+
let table = self.store.state.tables.get_mut(table_addr as usize);
1628+
table.ok_or_else(|| Trap::Other("table not found"))?.init(dst, items)
15961629
}
15971630

15981631
fn exec_table_grow(&mut self, table_index: u32) -> Result<(), Trap> {

‎crates/tinywasm/src/store/memory/shared.rs‎

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
use crate::std::sync::{Mutex, MutexGuard};
2-
use alloc::{sync::Arc, vec::Vec};
1+
use crate::std::sync::{Condvar, Mutex, MutexGuard};
2+
use crate::std::time::Duration;
3+
use alloc::{
4+
collections::{BTreeMap, VecDeque},
5+
sync::Arc,
6+
vec::Vec,
7+
};
38
use core::sync::atomic::{AtomicUsize, Ordering};
49
use tinywasm_types::MemoryType;
510

@@ -15,6 +20,13 @@ struct MemorySharedInstance {
1520
kind: MemoryType,
1621
pages: AtomicUsize,
1722
bytes: Mutex<MemoryStorage>,
23+
// Lock order: bytes, waiters, then an individual waiter's notified flag.
24+
waiters: Mutex<BTreeMap<usize, VecDeque<Arc<Waiter>>>>,
25+
}
26+
27+
struct Waiter {
28+
notified: Mutex<bool>,
29+
wake: Condvar,
1830
}
1931

2032
#[cfg(feature = "debug")]
@@ -35,7 +47,7 @@ impl MemoryShared {
3547
let kind = instance.kind;
3648
let pages = AtomicUsize::new(instance.page_count);
3749
let bytes = Mutex::new(instance.inner);
38-
Self(Arc::new(MemorySharedInstance { kind, pages, bytes }))
50+
Self(Arc::new(MemorySharedInstance { kind, pages, bytes, waiters: Mutex::new(BTreeMap::new()) }))
3951
}
4052

4153
/// Whether two handles reference the same instance, even across store slots.
@@ -115,6 +127,76 @@ impl MemoryShared {
115127
guard.inner.fill(offset, len, val).ok_or_else(|| memory_oob(offset, len, guard.inner.len()).into())
116128
}
117129

130+
/// Waits until notified or the signed nanosecond timeout expires.
131+
pub(crate) fn wait<const N: usize>(
132+
&self,
133+
addr: usize,
134+
expected: u64,
135+
timeout: i64,
136+
) -> core::result::Result<u32, Trap> {
137+
let guard = self.lock();
138+
let bytes = guard.inner.read_fixed::<N>(addr)?;
139+
let mut value = [0u8; 8];
140+
value[..N].copy_from_slice(&bytes);
141+
if u64::from_le_bytes(value) != expected {
142+
return Ok(1);
143+
}
144+
if timeout == 0 {
145+
return Ok(2);
146+
}
147+
let waiter = Arc::new(Waiter { notified: Mutex::new(false), wake: Condvar::new() });
148+
self.0
149+
.waiters
150+
.lock()
151+
.unwrap_or_else(|poison| poison.into_inner())
152+
.entry(addr)
153+
.or_default()
154+
.push_back(waiter.clone());
155+
drop(guard);
156+
157+
let notified = waiter.notified.lock().unwrap_or_else(|poison| poison.into_inner());
158+
if timeout < 0 {
159+
drop(waiter.wake.wait_while(notified, |ready| !*ready).unwrap_or_else(|poison| poison.into_inner()));
160+
} else {
161+
drop(
162+
waiter
163+
.wake
164+
.wait_timeout_while(notified, Duration::from_nanos(timeout as u64), |ready| !*ready)
165+
.unwrap_or_else(|poison| poison.into_inner())
166+
.0,
167+
);
168+
}
169+
// The condvar guard was dropped before taking waiters, preserving lock order.
170+
let mut waiters = self.0.waiters.lock().unwrap_or_else(|poison| poison.into_inner());
171+
let notified = *waiter.notified.lock().unwrap_or_else(|poison| poison.into_inner());
172+
if let Some(queue) = waiters.get_mut(&addr) {
173+
queue.retain(|entry| !Arc::ptr_eq(entry, &waiter));
174+
if queue.is_empty() {
175+
waiters.remove(&addr);
176+
}
177+
}
178+
Ok(if notified { 0 } else { 2 })
179+
}
180+
181+
/// Wakes at most `count` waiters at the given byte address.
182+
pub(crate) fn notify(&self, addr: usize, count: u32) -> u32 {
183+
let _guard = self.lock();
184+
let mut waiters = self.0.waiters.lock().unwrap_or_else(|poison| poison.into_inner());
185+
let mut woken = 0;
186+
if let Some(queue) = waiters.get_mut(&addr) {
187+
while woken < count {
188+
let Some(waiter) = queue.pop_front() else { break };
189+
*waiter.notified.lock().unwrap_or_else(|poison| poison.into_inner()) = true;
190+
waiter.wake.notify_one();
191+
woken += 1;
192+
}
193+
if queue.is_empty() {
194+
waiters.remove(&addr);
195+
}
196+
}
197+
woken
198+
}
199+
118200
/// Grows the memory by the given number of pages, returning its previous size.
119201
pub fn grow(&self, pages: i64) -> Result<Option<i64>> {
120202
Ok(self.grow_with_limiter(pages, None)?)

0 commit comments

Comments
 (0)