Skip to content

Commit 0823d50

Browse files
Daniel SalinasClaude
authored and
Daniel Salinas
committed
Add WASM32 core platform abstractions
This commit adds foundational WASM32 support to matrix-sdk-common by introducing platform-agnostic abstractions for async operations: - async_lock.rs: WASM32-compatible async RwLock implementation using std::sync primitives with async interface - stream.rs: WASM32 stream utilities using LocalBoxStream instead of BoxStream - executor.rs: Enhanced with comprehensive WASM runtime abstraction including WasmRuntimeHandle, unified Handle/Runtime types, and improved JoinError handling - Dependencies: Added futures-executor for WASM32 and async-compat for non-WASM32 targets These abstractions enable matrix-rust-sdk to run on both WASM32 and native targets without conditional compilation in consuming code. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 4d027ec commit 0823d50

File tree

7 files changed

+375
-10
lines changed

7 files changed

+375
-10
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ assert-json-diff = "2.0.2"
2727
assert_matches = "1.5.0"
2828
assert_matches2 = "0.1.2"
2929
async-rx = "0.1.3"
30+
async-compat = { git = "https://github.com/element-hq/async-compat", rev = "5a27c8b290f1f1dcfc0c4ec22c464e38528aa591" }
3031
async-stream = "0.3.5"
3132
async-trait = "0.1.85"
3233
as_variant = "1.3.0"

crates/matrix-sdk-common/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ uniffi = { workspace = true, optional = true }
4040

4141
[target.'cfg(target_arch = "wasm32")'.dependencies]
4242
futures-util = { workspace = true, features = ["channel"] }
43+
futures-executor = { workspace = true }
4344
wasm-bindgen-futures = { version = "0.4.33", optional = true }
4445
gloo-timers = { workspace = true, features = ["futures"] }
4546
web-sys = { workspace = true, features = ["console"] }
@@ -56,6 +57,7 @@ insta = { workspace = true }
5657
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
5758
# Enable the test macro.
5859
tokio = { workspace = true, features = ["rt", "macros"] }
60+
async-compat = { workspace = true }
5961

6062
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
6163
# Enable the JS feature for getrandom.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#[cfg(target_arch = "wasm32")]
2+
use std::sync::{
3+
RwLock as StdRwLock, RwLockReadGuard as StdRwLockReadGuard,
4+
RwLockWriteGuard as StdRwLockWriteGuard,
5+
};
6+
#[cfg(not(target_arch = "wasm32"))]
7+
pub use tokio::sync::{
8+
RwLock as AsyncRwLock, RwLockReadGuard as AsyncRwLockReadGuard,
9+
RwLockWriteGuard as AsyncRwLockWriteGuard,
10+
};
11+
12+
#[cfg(target_arch = "wasm32")]
13+
use std::{
14+
future::Future,
15+
pin::Pin,
16+
sync::Arc,
17+
task::{Context, Poll, Waker},
18+
};
19+
20+
/// A platform-independent `AsyncRwLock`.
21+
#[cfg(target_arch = "wasm32")]
22+
#[derive(Debug)]
23+
pub struct AsyncRwLock<T> {
24+
inner: Arc<StdRwLock<T>>,
25+
}
26+
27+
#[cfg(target_arch = "wasm32")]
28+
impl<T: Default> Default for AsyncRwLock<T> {
29+
fn default() -> Self {
30+
Self { inner: Arc::new(StdRwLock::new(T::default())) }
31+
}
32+
}
33+
34+
#[cfg(target_arch = "wasm32")]
35+
impl<T> AsyncRwLock<T> {
36+
/// Create a new `AsyncRwLock`.
37+
pub fn new(value: T) -> Self {
38+
Self { inner: Arc::new(StdRwLock::new(value)) }
39+
}
40+
41+
/// Acquire a read lock asynchronously.
42+
pub async fn read(&self) -> AsyncRwLockReadGuard<'_, T> {
43+
AsyncRwLockReadFuture { lock: &self.inner, waker: None }.await
44+
}
45+
46+
/// Acquire a write lock asynchronously.
47+
pub async fn write(&self) -> AsyncRwLockWriteGuard<'_, T> {
48+
AsyncRwLockWriteFuture { lock: &self.inner, waker: None }.await
49+
}
50+
}
51+
52+
#[cfg(target_arch = "wasm32")]
53+
/// A future for acquiring a read lock.
54+
struct AsyncRwLockReadFuture<'a, T> {
55+
lock: &'a StdRwLock<T>,
56+
waker: Option<Waker>,
57+
}
58+
59+
#[cfg(target_arch = "wasm32")]
60+
impl<'a, T> Future for AsyncRwLockReadFuture<'a, T> {
61+
type Output = AsyncRwLockReadGuard<'a, T>;
62+
63+
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
64+
// Store the waker so we can wake the task if needed
65+
self.waker = Some(cx.waker().clone());
66+
67+
// Try to acquire the read lock - this is a non-blocking operation in wasm
68+
match self.lock.try_read() {
69+
Ok(guard) => Poll::Ready(AsyncRwLockReadGuard { guard }),
70+
Err(_) => {
71+
// In a true async environment we would register a waker to be notified
72+
// when the lock becomes available, but in wasm we'll just yield to
73+
// the executor and try again next time.
74+
cx.waker().wake_by_ref();
75+
Poll::Pending
76+
}
77+
}
78+
}
79+
}
80+
81+
#[cfg(target_arch = "wasm32")]
82+
/// A future for acquiring a write lock.
83+
struct AsyncRwLockWriteFuture<'a, T> {
84+
lock: &'a StdRwLock<T>,
85+
waker: Option<Waker>,
86+
}
87+
88+
#[cfg(target_arch = "wasm32")]
89+
impl<'a, T> Future for AsyncRwLockWriteFuture<'a, T> {
90+
type Output = AsyncRwLockWriteGuard<'a, T>;
91+
92+
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
93+
// Store the waker so we can wake the task if needed
94+
self.waker = Some(cx.waker().clone());
95+
96+
// Try to acquire the write lock - this is a non-blocking operation in wasm
97+
match self.lock.try_write() {
98+
Ok(guard) => Poll::Ready(AsyncRwLockWriteGuard { guard }),
99+
Err(_) => {
100+
// In a true async environment we would register a waker to be notified
101+
// when the lock becomes available, but in wasm we'll just yield to
102+
// the executor and try again next time.
103+
cx.waker().wake_by_ref();
104+
Poll::Pending
105+
}
106+
}
107+
}
108+
}
109+
110+
#[cfg(target_arch = "wasm32")]
111+
#[derive(Debug)]
112+
/// A read guard for `AsyncRwLock`.
113+
pub struct AsyncRwLockReadGuard<'a, T> {
114+
guard: StdRwLockReadGuard<'a, T>,
115+
}
116+
117+
#[cfg(target_arch = "wasm32")]
118+
impl<T> std::ops::Deref for AsyncRwLockReadGuard<'_, T> {
119+
type Target = T;
120+
121+
fn deref(&self) -> &Self::Target {
122+
&self.guard
123+
}
124+
}
125+
126+
#[cfg(target_arch = "wasm32")]
127+
#[derive(Debug)]
128+
/// A write guard for `AsyncRwLock`.
129+
pub struct AsyncRwLockWriteGuard<'a, T> {
130+
guard: StdRwLockWriteGuard<'a, T>,
131+
}
132+
133+
#[cfg(target_arch = "wasm32")]
134+
impl<T> std::ops::Deref for AsyncRwLockWriteGuard<'_, T> {
135+
type Target = T;
136+
137+
fn deref(&self) -> &Self::Target {
138+
&self.guard
139+
}
140+
}
141+
142+
#[cfg(target_arch = "wasm32")]
143+
impl<T> std::ops::DerefMut for AsyncRwLockWriteGuard<'_, T> {
144+
fn deref_mut(&mut self) -> &mut Self::Target {
145+
&mut self.guard
146+
}
147+
}

crates/matrix-sdk-common/src/executor.rs

Lines changed: 142 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,41 @@ use std::{
2323
};
2424

2525
#[cfg(target_arch = "wasm32")]
26-
pub use futures_util::future::Aborted as JoinError;
26+
pub use futures_util::future::AbortHandle;
2727
#[cfg(target_arch = "wasm32")]
2828
use futures_util::{
29-
future::{AbortHandle, Abortable, RemoteHandle},
29+
future::{Abortable, RemoteHandle},
3030
FutureExt,
3131
};
3232
#[cfg(not(target_arch = "wasm32"))]
33-
pub use tokio::task::{spawn, JoinError, JoinHandle};
33+
pub use tokio::task::{spawn, AbortHandle, JoinError, JoinHandle};
3434

35+
#[cfg(target_arch = "wasm32")]
36+
#[derive(Debug)]
37+
pub enum JoinError {
38+
Cancelled,
39+
Panic,
40+
}
41+
#[cfg(target_arch = "wasm32")]
42+
impl JoinError {
43+
/// Returns true if the error was caused by the task being cancelled.
44+
///
45+
/// See [the module level docs] for more information on cancellation.
46+
///
47+
/// [the module level docs]: crate::task#cancellation
48+
pub fn is_cancelled(&self) -> bool {
49+
matches!(self, JoinError::Cancelled)
50+
}
51+
}
52+
#[cfg(target_arch = "wasm32")]
53+
impl std::fmt::Display for JoinError {
54+
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55+
match &self {
56+
JoinError::Cancelled => write!(fmt, "task was cancelled"),
57+
JoinError::Panic => write!(fmt, "task panicked"),
58+
}
59+
}
60+
}
3561
#[cfg(target_arch = "wasm32")]
3662
pub fn spawn<F, T>(future: F) -> JoinHandle<T>
3763
where
@@ -47,24 +73,28 @@ where
4773
let _ = future.await;
4874
});
4975

50-
JoinHandle { remote_handle: Some(remote_handle), abort_handle }
76+
JoinHandle { remote_handle: Some(remote_handle), the_abort_handle: abort_handle }
5177
}
5278

5379
#[cfg(target_arch = "wasm32")]
5480
#[derive(Debug)]
5581
pub struct JoinHandle<T> {
5682
remote_handle: Option<RemoteHandle<T>>,
57-
abort_handle: AbortHandle,
83+
the_abort_handle: AbortHandle,
5884
}
5985

6086
#[cfg(target_arch = "wasm32")]
6187
impl<T> JoinHandle<T> {
6288
pub fn abort(&self) {
63-
self.abort_handle.abort();
89+
self.the_abort_handle.abort();
90+
}
91+
92+
pub fn abort_handle(&self) -> AbortHandle {
93+
self.the_abort_handle.clone()
6494
}
6595

6696
pub fn is_finished(&self) -> bool {
67-
self.abort_handle.is_aborted()
97+
self.the_abort_handle.is_aborted()
6898
}
6999
}
70100

@@ -83,17 +113,119 @@ impl<T: 'static> Future for JoinHandle<T> {
83113
type Output = Result<T, JoinError>;
84114

85115
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
86-
if self.abort_handle.is_aborted() {
116+
if self.the_abort_handle.is_aborted() {
87117
// The future has been aborted. It is not possible to poll it again.
88-
Poll::Ready(Err(JoinError))
118+
Poll::Ready(Err(JoinError::Cancelled))
89119
} else if let Some(handle) = self.remote_handle.as_mut() {
90120
Pin::new(handle).poll(cx).map(Ok)
91121
} else {
92-
Poll::Ready(Err(JoinError))
122+
Poll::Ready(Err(JoinError::Panic))
93123
}
94124
}
95125
}
96126

127+
#[cfg(target_arch = "wasm32")]
128+
use futures_executor;
129+
130+
/// A handle to a runtime for executing async tasks and futures.
131+
///
132+
/// This is a unified type that represents either:
133+
/// - A `tokio::runtime::Handle` on non-WASM platforms
134+
/// - A `WasmRuntimeHandle` on WASM platforms
135+
///
136+
/// This abstraction allows code to run on both WASM and non-WASM platforms
137+
/// without conditional compilation.
138+
#[cfg(not(target_arch = "wasm32"))]
139+
pub type Handle = tokio::runtime::Handle;
140+
#[cfg(not(target_arch = "wasm32"))]
141+
pub type Runtime = tokio::runtime::Runtime;
142+
143+
#[cfg(target_arch = "wasm32")]
144+
pub type Handle = WasmRuntimeHandle;
145+
#[cfg(target_arch = "wasm32")]
146+
pub type Runtime = WasmRuntimeHandle;
147+
148+
#[cfg(target_arch = "wasm32")]
149+
#[derive(Debug)]
150+
/// A dummy guard that does nothing when dropped.
151+
/// This is used for the WASM implementation to match tokio::runtime::EnterGuard.
152+
pub struct WasmRuntimeGuard;
153+
154+
#[cfg(target_arch = "wasm32")]
155+
impl Drop for WasmRuntimeGuard {
156+
fn drop(&mut self) {
157+
// No-op, as there's no special context to exit in WASM
158+
}
159+
}
160+
161+
#[cfg(target_arch = "wasm32")]
162+
#[derive(Default, Debug)]
163+
/// A runtime handle implementation for WebAssembly targets.
164+
///
165+
/// This implements a minimal subset of the tokio::runtime::Handle API
166+
/// that is needed for the matrix-rust-sdk to function on WASM.
167+
pub struct WasmRuntimeHandle;
168+
169+
#[cfg(target_arch = "wasm32")]
170+
impl WasmRuntimeHandle {
171+
/// Spawns a future in the wasm32 bindgen runtime.
172+
#[track_caller]
173+
pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
174+
where
175+
F: Future + 'static,
176+
F::Output: 'static,
177+
{
178+
spawn(future)
179+
}
180+
181+
/// Runs the provided function on an executor dedicated to blocking
182+
/// operations.
183+
#[track_caller]
184+
pub fn spawn_blocking<F, R>(&self, func: F) -> JoinHandle<R>
185+
where
186+
F: FnOnce() -> R + 'static,
187+
R: 'static,
188+
{
189+
spawn(async move { func() })
190+
}
191+
192+
/// Runs a future to completion on the current thread.
193+
pub fn block_on<F, T>(&self, future: F) -> T
194+
where
195+
F: Future<Output = T>,
196+
{
197+
futures_executor::block_on(future)
198+
}
199+
200+
/// Enters the runtime context.
201+
///
202+
/// For WebAssembly, this is a no-op that returns a dummy guard.
203+
pub fn enter(&self) -> WasmRuntimeGuard {
204+
WasmRuntimeGuard
205+
}
206+
}
207+
208+
/// Get a runtime handle appropriate for the current target platform.
209+
///
210+
/// This function returns a unified `Handle` type that works across both
211+
/// WASM and non-WASM platforms, allowing code to be written that is
212+
/// agnostic to the platform-specific runtime implementation.
213+
///
214+
/// Returns:
215+
/// - A `tokio::runtime::Handle` on non-WASM platforms
216+
/// - A `WasmRuntimeHandle` on WASM platforms
217+
pub fn get_runtime_handle() -> Handle {
218+
#[cfg(target_arch = "wasm32")]
219+
{
220+
WasmRuntimeHandle
221+
}
222+
223+
#[cfg(not(target_arch = "wasm32"))]
224+
{
225+
async_compat::get_runtime_handle()
226+
}
227+
}
228+
97229
#[cfg(test)]
98230
mod tests {
99231
use assert_matches::assert_matches;

0 commit comments

Comments
 (0)