Skip to content

Commit 898b5bd

Browse files
authored
feat: ECMAScript Futex
2 parents f0d4434 + 94308c4 commit 898b5bd

File tree

13 files changed

+619
-383
lines changed

13 files changed

+619
-383
lines changed

.github/workflows/rust.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
name: Rust
22
on:
3-
push: { branches: "main" }
4-
pull_request: { branches: "*" }
3+
push: { branches: ["main"] }
4+
pull_request: { branches: ["*"] }
55
jobs:
66
build-and-test:
77
strategy:
88
matrix:
9-
os: [ubuntu-latest, ubuntu-22.04-arm, windows-latest, windows-11-arm, macos-latest]
9+
os:
10+
[
11+
ubuntu-latest,
12+
ubuntu-22.04-arm,
13+
windows-latest,
14+
windows-11-arm,
15+
macos-latest,
16+
]
1017
runs-on: ${{ matrix.os }}
1118
steps:
1219
- uses: actions/checkout@v4
@@ -26,4 +33,3 @@ jobs:
2633
run: cargo build --verbose
2734
- name: Run tests
2835
run: cargo nextest run --verbose
29-

Cargo.toml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
[package]
2-
name = "wait_on_address"
3-
description = "Cross-platform atomic wait and wake (aka futex) functionality."
4-
repository = "https://github.com/DouglasDwyer/wait_on_address"
5-
keywords = ["atomic", "futex"]
2+
name = "ecmascript_futex"
3+
description = "Cross-platform atomic wait and wake (aka futex) functionality using the ECMAScript Atomics memory model."
4+
repository = "https://github.com/trynova/ecmascript_futex"
5+
keywords = ["atomic", "futex", "ecmascript"]
66
version = "0.1.1"
77
edition = "2024"
88
license = "BSD-2-Clause"
99
categories = ["concurrency", "os", "no-std"]
1010

11+
[dependencies]
12+
ecmascript_atomics = { version = "0.2.3" }
13+
1114
[target.'cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd", target_os = "macos"))'.dependencies]
1215
libc = { version = "0.2", default-features = false }
1316

@@ -20,4 +23,7 @@ wasm-bindgen = { version = "0.2.90", default-features = false }
2023
web-sys = { version = "0.3.24", default-features = false, features = [ "Window" ] }
2124

2225
[build-dependencies]
23-
rustversion = { version = "1.0.14", default-features = false }
26+
rustversion = { version = "1.0.14", default-features = false }
27+
28+
[dev-dependencies]
29+
ecmascript_atomics = { version = "0.2.3", features = ["alloc"] }

README.md

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
# wait_on_address
1+
# ecmascript_futex
22

3-
[![Crates.io](https://img.shields.io/crates/v/wait_on_address.svg)](https://crates.io/crates/wait_on_address)
4-
[![Docs.rs](https://docs.rs/wait_on_address/badge.svg)](https://docs.rs/wait_on_address)
3+
Cross platform library for implementing ECMAScript `Atomics.wait`,
4+
`Atomics.wakeAsync`, and `Atomics.notify` (aka futex) functionality in Rust,
5+
operating on ECMAScript memory as produced by the
6+
[`ecmascript_atomics`](https://github.com/trynova/ecmascript_atomics) crate.
7+
This crate is a fork of
8+
[`wait_on_address`](https://github.com/DouglasDwyer/wait_on_address) which is
9+
itself a fork of [`atomic-wait`](https://github.com/m-ou-se/atomic-wait). The
10+
changes inherited and kept from `wait_on_address` are:
511

6-
Cross platform atomic wait and wake (aka futex) functionality. This crate is a fork of [`atomic-wait`](https://github.com/m-ou-se/atomic-wait), and extends the original code with the following functionality:
7-
8-
- Support for `AtomicI32`, `AtomicI64`, and `AtomicU64`
912
- Support for waiting with a timeout
1013
- Support for `wasm32` on nightly using `std::arch`
1114
- Polyfill for all other platforms
1215

16+
The main
17+
1318
Natively-supported platforms:
1419

1520
- Windows 8+, Windows Server 2012+
@@ -21,16 +26,18 @@ Natively-supported platforms:
2126
## Usage
2227

2328
```rust
24-
use std::{sync::atomic::AtomicU64, time::Duration};
25-
use wait_on_address::AtomicWait;
29+
use core::time::Duration;
30+
use ecmascript_atomics::{Racy, RacyBox};
31+
use ecmascript_futex::ECMAScriptAtomicWait;
2632

27-
let a = AtomicU64::new(0);
33+
let a = RacyBox::new(0u64).unwrap();
34+
let a = a.as_slice().get(0).unwrap();
2835

2936
a.wait(1); // If the value is 1, wait.
3037

3138
a.wait_timeout(2, Duration::from_millis(100)); // If the value is 2, wait at most 100 milliseconds
3239

33-
a.notify_one(); // Wake one waiting thread.
40+
a.notify_many(1); // Wake one waiting thread.
3441

3542
a.notify_all(); // Wake all waiting threads.
3643
```
@@ -43,8 +50,11 @@ On FreeBSD, this uses the `_umtx_op` syscall.
4350

4451
On Windows, this uses the `WaitOnAddress` and `WakeByAddress` APIs.
4552

46-
On macOS (and iOS and watchOS), this uses the `os_sync_wait_on_address` and `os_sync_wake_by_address` APIs.
53+
On macOS (and iOS and watchOS), this uses the `os_sync_wait_on_address` and
54+
`os_sync_wake_by_address` APIs.
4755

48-
On wasm32 with `nightly`, this uses `memory_atomic_wait32`, `memory_atomic_wait64`, and `memory_atomic_notify` instructions.
56+
On wasm32 with `nightly`, this uses `memory_atomic_wait32`,
57+
`memory_atomic_wait64`, and `memory_atomic_notify` instructions.
4958

50-
All other platforms with `std` support fall back to a fixed-size hashmap of `Condvar`s, similar to `libstdc++`'s implementation for `std::atomic<T>`.
59+
All other platforms with `std` support fall back to a fixed-size hashmap of
60+
`Condvar`s, similar to `libstdc++`'s implementation for `std::atomic<T>`.

rust_toolchain.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[toolchain]
2+
channel = "stable"
3+
components = ["rustfmt", "clippy"]

src/condvar_table.rs

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::{
44
time::Duration,
55
};
66

7+
use crate::FutexError;
8+
79
/// The number of OS synchronization primitives to use.
810
const TABLE_SIZE: usize = 256;
911

@@ -12,9 +14,14 @@ static TABLE: [TableEntry; TABLE_SIZE] = [TableEntry::DEFAULT; TABLE_SIZE];
1214

1315
/// Puts the current thread to sleep if `condition` evaluates to `true`.
1416
/// The thread will be woken after `timeout` if it is provided.
15-
pub fn wait(ptr: *const (), condition: impl FnOnce() -> bool, timeout: Option<Duration>) {
17+
pub fn wait(
18+
ptr: *const (),
19+
condition: impl Fn() -> bool,
20+
timeout: Option<Duration>,
21+
) -> Result<(), FutexError> {
1622
let entry = &TABLE[entry_for_ptr(ptr) as usize];
1723
let mut guard = spin_lock(&entry.mutex);
24+
let mut timedout = false;
1825
if condition() {
1926
if guard.waiting_count == 0 {
2027
guard.address = ptr;
@@ -25,42 +32,58 @@ pub fn wait(ptr: *const (), condition: impl FnOnce() -> bool, timeout: Option<Du
2532
guard.waiting_count += 1;
2633

2734
guard = if let Some(time) = timeout {
28-
entry
35+
let (guard, result) = entry
2936
.condvar
3037
.wait_timeout(guard, time)
31-
.expect("Failed to lock mutex")
32-
.0
38+
.expect("Failed to lock mutex");
39+
timedout = result.timed_out();
40+
guard
3341
} else {
3442
entry.condvar.wait(guard).expect("Failed to lock mutex")
3543
};
3644

3745
guard.waiting_count -= 1;
46+
47+
if timedout {
48+
Err(FutexError::Timeout)
49+
} else {
50+
Ok(())
51+
}
52+
} else {
53+
Err(FutexError::NotEqual)
3854
}
3955
}
4056

4157
/// Wakes all threads waiting on `ptr`.
42-
pub fn notify_all(ptr: *const ()) {
43-
if !ptr.is_null() {
44-
let entry = &TABLE[entry_for_ptr(ptr) as usize];
45-
let metadata = *spin_lock(&entry.mutex);
46-
if 0 < metadata.waiting_count {
47-
entry.condvar.notify_all();
48-
}
58+
pub fn notify_all(ptr: *const ()) -> usize {
59+
if ptr.is_null() {
60+
return 0;
4961
}
62+
let entry = &TABLE[entry_for_ptr(ptr) as usize];
63+
let metadata = *spin_lock(&entry.mutex);
64+
if 0 < metadata.waiting_count {
65+
entry.condvar.notify_all();
66+
}
67+
metadata.waiting_count
5068
}
5169

5270
/// Wakes at least one thread waiting on `ptr`.
53-
pub fn notify_one(ptr: *const ()) {
54-
if !ptr.is_null() {
55-
let entry = &TABLE[entry_for_ptr(ptr) as usize];
56-
let metadata = *spin_lock(&entry.mutex);
57-
if 0 < metadata.waiting_count {
58-
if metadata.address.is_null() {
59-
entry.condvar.notify_all();
60-
} else if metadata.address == ptr {
61-
entry.condvar.notify_one();
62-
}
71+
pub fn notify_many(ptr: *const (), count: usize) -> usize {
72+
if ptr.is_null() {
73+
return 0;
74+
}
75+
let entry = &TABLE[entry_for_ptr(ptr) as usize];
76+
let metadata = *spin_lock(&entry.mutex);
77+
if metadata.waiting_count == 0 {
78+
0
79+
} else if metadata.waiting_count < count || metadata.address.is_null() {
80+
entry.condvar.notify_all();
81+
metadata.waiting_count
82+
} else {
83+
for _ in 0..count {
84+
entry.condvar.notify_one();
6385
}
86+
count
6487
}
6588
}
6689

@@ -79,9 +102,9 @@ fn spin_lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
79102
/// Gets the entry index to use for the given address.
80103
fn entry_for_ptr(ptr: *const ()) -> u8 {
81104
let x_64 = ptr as u64;
82-
let x_32 = (x_64 >> 32) as u32 | x_64 as u32;
83-
let x_16 = (x_32 >> 16) as u16 | x_32 as u16;
84-
(x_16 >> 8) as u8 | x_16 as u8
105+
let x_32 = (x_64 >> 32) as u32 ^ x_64 as u32;
106+
let x_16 = (x_32 >> 16) as u16 ^ x_32 as u16;
107+
(x_16 >> 8) as u8 ^ (x_16 >> 2) as u8
85108
}
86109

87110
/// Holds metadata that gets written while locking.

src/fallback.rs

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,53 @@
1-
use std::{
2-
sync::atomic::{AtomicU32, AtomicU64, Ordering},
3-
time::Duration,
4-
};
1+
use core::time::Duration;
52

6-
use crate::{condvar_table, private::AtomicWaitImpl};
3+
use ecmascript_atomics::{Ordering, Racy};
74

8-
impl AtomicWaitImpl for AtomicU32 {
5+
use crate::{FutexError, condvar_table, private::ECMAScriptAtomicWaitImpl};
6+
7+
impl ECMAScriptAtomicWaitImpl for Racy<'_, u32> {
98
type AtomicInner = u32;
109

11-
fn wait_timeout(&self, value: Self::AtomicInner, timeout: Option<Duration>) {
10+
fn wait_timeout(
11+
&self,
12+
value: Self::AtomicInner,
13+
timeout: Option<Duration>,
14+
) -> Result<(), FutexError> {
1215
condvar_table::wait(
13-
self as *const _ as *const _,
14-
|| self.load(Ordering::Acquire) == value,
16+
self.addr(),
17+
|| self.load(Ordering::SeqCst) == value,
1518
timeout,
16-
);
19+
)
1720
}
1821

19-
fn notify_all(&self) {
20-
condvar_table::notify_all(self as *const _ as *const _);
22+
fn notify_all(&self) -> usize {
23+
condvar_table::notify_all(self.addr())
2124
}
2225

23-
fn notify_one(&self) {
24-
condvar_table::notify_one(self as *const _ as *const _);
26+
fn notify_many(&self, count: usize) -> usize {
27+
condvar_table::notify_many(self.addr(), count)
2528
}
2629
}
2730

28-
impl AtomicWaitImpl for AtomicU64 {
31+
impl ECMAScriptAtomicWaitImpl for Racy<'_, u64> {
2932
type AtomicInner = u64;
3033

31-
fn wait_timeout(&self, value: Self::AtomicInner, timeout: Option<Duration>) {
34+
fn wait_timeout(
35+
&self,
36+
value: Self::AtomicInner,
37+
timeout: Option<Duration>,
38+
) -> Result<(), FutexError> {
3239
condvar_table::wait(
33-
self as *const _ as *const _,
34-
|| self.load(Ordering::Acquire) == value,
40+
self.addr(),
41+
|| self.load(Ordering::SeqCst) == value,
3542
timeout,
36-
);
43+
)
3744
}
3845

39-
fn notify_all(&self) {
40-
condvar_table::notify_all(self as *const _ as *const _);
46+
fn notify_all(&self) -> usize {
47+
condvar_table::notify_all(self.addr())
4148
}
4249

43-
fn notify_one(&self) {
44-
condvar_table::notify_one(self as *const _ as *const _);
50+
fn notify_many(&self, count: usize) -> usize {
51+
condvar_table::notify_many(self.addr(), count)
4552
}
4653
}

0 commit comments

Comments
 (0)