feat: platform abstraction layer for wasm32 compilation (Stage 1)
Core swactor now compiles for wasm32-unknown-unknown: - Added `wasm` feature flag (bundles no_random + web-time) - Platform-aware Instant: web_time::Instant on wasm, std::time on native - cfg-gated Runtime::run() and RuntimeHandle (thread::spawn unavailable on wasm32 — browser crate provides Web Worker-based alternative) - Validated: crossbeam-queue, atomics, Mutex, RwLock, thread parking all work on wasm32 with atomics target feature - Updated crates/wasm/ to use new `wasm` feature Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
parent
63cba1c4eb
commit
2649a54feb
9 changed files with 150 additions and 37 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -2723,6 +2723,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"swactor-std",
|
"swactor-std",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"web-time",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3735,6 +3736,16 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,13 @@ serde = ["dep:serde"]
|
||||||
tracing = ["dep:tracing"]
|
tracing = ["dep:tracing"]
|
||||||
no_random = [] # compile without access to a source of randomness
|
no_random = [] # compile without access to a source of randomness
|
||||||
transport = [] # transport-agnostic messaging (no mandatory deps; codec is user-provided)
|
transport = [] # transport-agnostic messaging (no mandatory deps; codec is user-provided)
|
||||||
|
wasm = ["no_random", "dep:web-time"] # browser/wasm32 target support
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
getrandom = { version = "0.2", optional = true }
|
getrandom = { version = "0.2", optional = true }
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
tracing = { version = "0.1", optional = true }
|
tracing = { version = "0.1", optional = true }
|
||||||
|
web-time = { version = "0.2", optional = true }
|
||||||
crossbeam-queue = "0.3.12"
|
crossbeam-queue = "0.3.12"
|
||||||
crossbeam-utils = "0.8.21"
|
crossbeam-utils = "0.8.21"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,6 @@
|
||||||
|
|
||||||
## Cycle 0 — Research (complete)
|
## Cycle 0 — Research (complete)
|
||||||
Investigated Lunatic, wasmCloud, Actix-wasm attempts. Analyzed core swactor platform deps: 4 blockers (thread spawn, park/unpark, yield, Instant). User confirmed: performance-first, SharedArrayBuffer+wasm-threads, Rust-only actors, all feasible features, future STUN/TURN. Produced constraints.md, research_synthesis.md, 6 feature-stage docs. Next: Stage 1 platform abstraction.
|
Investigated Lunatic, wasmCloud, Actix-wasm attempts. Analyzed core swactor platform deps: 4 blockers (thread spawn, park/unpark, yield, Instant). User confirmed: performance-first, SharedArrayBuffer+wasm-threads, Rust-only actors, all feasible features, future STUN/TURN. Produced constraints.md, research_synthesis.md, 6 feature-stage docs. Next: Stage 1 platform abstraction.
|
||||||
|
|
||||||
|
## Stage 1 — Platform Abstraction (complete)
|
||||||
|
Added `wasm` feature + `web-time` dep. Replaced `std::time::Instant` → `crate::Instant` (cfg-gated re-export). Gated `Runtime::run()` and `RuntimeHandle` for non-wasm. Key finding: only `thread::spawn` needed gating — park/unpark/yield/Mutex/RwLock/atomics/crossbeam all work on wasm32 with atomics. Cleaned unused Mutex import in worker.rs. Updated crates/wasm/ to use `wasm` feature. All native tests pass, wasm32 compilation succeeds. Files changed: Cargo.toml, src/lib.rs, src/runtime.rs, src/worker.rs, crates/wasm/Cargo.toml.
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,47 @@
|
||||||
## Current Task
|
## Current Task
|
||||||
Cycle 0 — Research
|
Stage 1 — Platform Abstraction Layer
|
||||||
Step: Complete
|
Step: Complete
|
||||||
Attempt: 1 of 3
|
Attempt: 1 of 3
|
||||||
|
|
||||||
## Key Files (read these first on resume)
|
## Key Files (read these first on resume)
|
||||||
- `big-feature-phase/TASK.md` — workflow rules
|
- `big-feature-phase/TASK.md` — workflow rules
|
||||||
- `big-feature-phase/notes/constraints.md` — guardrails for in-browser runtime
|
- `big-feature-phase/notes/constraints.md` — guardrails
|
||||||
- `big-feature-phase/notes/research_synthesis.md` — P0-P4 priority ranking
|
- `big-feature-phase/notes/feature-stages/02-single-worker-browser.md` — Stage 2 spec
|
||||||
- `big-feature-phase/notes/feature-stages/01-platform-abstraction.md` — Stage 1 spec
|
- `Cargo.toml` — `wasm` feature flag, `web-time` dep (lines 23, 30)
|
||||||
- `src/runtime.rs` — thread spawn (line 351), park handles (line 110), Instant (line 205)
|
- `src/lib.rs` — platform-aware `Instant` re-export (lines 20-24)
|
||||||
- `src/worker.rs` — backoff loop (lines 495-523), Instant timing (9 calls)
|
- `src/runtime.rs` — cfg-gated `run()` (line 340) and `RuntimeHandle` (line 73)
|
||||||
- `src/delivery.rs` — TickContext.worker_threads (line 246)
|
- `crates/wasm/Cargo.toml` — now uses `features = ["wasm"]`
|
||||||
- `src/channel.rs` — crossbeam HybridChannel (validate wasm compilation)
|
- `docs/development_history/in-browser/PLATFORM_ABSTRACTION.md` — what was done
|
||||||
- `crates/wasm/src/lib.rs` — existing PoC browser runtime
|
|
||||||
|
|
||||||
## Last Action & Result
|
## Last Action & Result
|
||||||
Completed Cycle 0 research phase:
|
Completed Stage 1 (Platform Abstraction Layer):
|
||||||
- Investigated similar projects (Lunatic, wasmCloud, Actix wasm attempts)
|
- Added `web-time` dep + `wasm` feature (`no_random` + `web-time`)
|
||||||
- Analyzed all platform-specific code in core swactor (4 blockers found)
|
- Replaced `std::time::Instant` → `crate::Instant` in runtime.rs, worker.rs
|
||||||
- User confirmed: Rust-only actors, performance-first, Web Workers, all features, full peer
|
- cfg-gated `Runtime::run()` and `RuntimeHandle` for `not(target_arch = "wasm32")`
|
||||||
- Key decision: SharedArrayBuffer + wasm-threads (not postMessage isolation)
|
- Removed unused `Mutex` import from worker.rs
|
||||||
- Wrote constraints.md, research_synthesis.md, 6 feature-stage docs
|
- Updated `crates/wasm/` to use `wasm` feature
|
||||||
|
- Key finding: most std::sync/thread primitives work on wasm32 with atomics; only `thread::spawn` needed gating
|
||||||
|
- All native tests pass, wasm32 compilation succeeds
|
||||||
|
|
||||||
## Next Action
|
## Next Action
|
||||||
Begin Stage 1 (Platform Abstraction Layer) — `notes/feature-stages/01-platform-abstraction.md`:
|
Begin Stage 2 (Single-Worker Browser Runtime) — `notes/feature-stages/02-single-worker-browser.md`:
|
||||||
1. Add `web-time` dependency to core Cargo.toml
|
1. Create `crates/wasm-browser/` crate structure
|
||||||
2. Replace `std::time::Instant` with `web_time::Instant` in runtime.rs and worker.rs
|
2. Implement `BrowserRuntime` wasm-bindgen API (spawn, send, tick, try_recv, stats)
|
||||||
3. Implement `ParkHandle` abstraction with cfg gates
|
3. Actor registration macro/pattern for JS-accessible spawning
|
||||||
4. Gate `Runtime::run()` thread spawning for non-wasm
|
4. Auto-scheduling via setTimeout(0) loop
|
||||||
5. Validate crossbeam-queue compiles for wasm32 with atomics
|
5. Message serialization across JS↔Wasm boundary
|
||||||
6. Add `wasm` feature flag
|
6. Tests (wasm-pack test or Node.js)
|
||||||
|
|
||||||
## Completed This Session
|
## Completed This Session
|
||||||
- [x] Read TASK.md, state.md, history.md (context restart checklist)
|
- [x] Cycle 0 research artifacts (constraints.md, research_synthesis.md, 6 stage docs)
|
||||||
- [x] Investigated similar codebases (Lunatic, wasmCloud, Actix-wasm)
|
- [x] Stage 1: `web-time` dep + `wasm` feature flag in Cargo.toml
|
||||||
- [x] Analyzed platform dependencies in core swactor
|
- [x] Stage 1: Platform-aware `Instant` re-export in src/lib.rs
|
||||||
- [x] Asked and resolved design questions with user
|
- [x] Stage 1: cfg-gated `Runtime::run()` and `RuntimeHandle` in src/runtime.rs
|
||||||
- [x] Wrote `notes/constraints.md`
|
- [x] Stage 1: Updated crates/wasm/ to use `wasm` feature
|
||||||
- [x] Wrote `notes/research_synthesis.md`
|
- [x] Stage 1: Validated wasm32 compilation and native tests
|
||||||
- [x] Created `notes/feature-stages/` with 6 stage docs
|
- [x] Stage 1: Development history doc
|
||||||
|
|
||||||
## Open Questions / Blockers
|
## Open Questions / Blockers
|
||||||
- Need to verify crossbeam-queue compiles for wasm32 with atomics (Stage 1 task)
|
- Stage 2: Need to decide on message serialization (serde-wasm-bindgen vs raw bytes)
|
||||||
- Need to find/verify correct `core::arch::wasm32` APIs for memory_atomic_wait/notify
|
- Stage 2: Actor registration pattern — macro vs manual factory map
|
||||||
- Nightly Rust toolchain + wasm32 target needs to be set up in CI
|
- Stage 3: Web Worker thread state initialization needs investigation (does std::thread::current() work in a Web Worker context?)
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,5 @@ edition = "2024"
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
swactor = { path = "../..", default-features = false, features = ["no_random"] }
|
swactor = { path = "../..", default-features = false, features = ["wasm"] }
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
|
|
|
||||||
82
docs/development_history/in-browser/PLATFORM_ABSTRACTION.md
Normal file
82
docs/development_history/in-browser/PLATFORM_ABSTRACTION.md
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
# Platform Abstraction Layer — Development History
|
||||||
|
|
||||||
|
> Stage 1 of the in-browser swactor runtime. Makes core swactor compile for
|
||||||
|
> `wasm32-unknown-unknown` without behavioral changes on native targets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### 1. `web-time` dependency + `wasm` feature flag
|
||||||
|
|
||||||
|
**File**: `Cargo.toml`
|
||||||
|
|
||||||
|
Added `web-time` as an optional dependency and a `wasm` feature that bundles
|
||||||
|
`no_random` + `web-time`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
wasm = ["no_random", "dep:web-time"]
|
||||||
|
web-time = { version = "0.2", optional = true }
|
||||||
|
```
|
||||||
|
|
||||||
|
`web-time` is a drop-in replacement for `std::time::Instant`:
|
||||||
|
- Native: re-exports `std::time::Instant` (zero-cost)
|
||||||
|
- wasm32: uses `performance.now()` via `js-sys`
|
||||||
|
|
||||||
|
### 2. Platform-aware `Instant` re-export
|
||||||
|
|
||||||
|
**File**: `src/lib.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
pub(crate) use web_time::Instant;
|
||||||
|
#[cfg(not(feature = "wasm"))]
|
||||||
|
pub(crate) use std::time::Instant;
|
||||||
|
```
|
||||||
|
|
||||||
|
All modules (`runtime.rs`, `worker.rs`) now use `crate::Instant` instead of
|
||||||
|
`std::time::Instant`. Single point of truth — no cfg noise in consumer code.
|
||||||
|
|
||||||
|
### 3. cfg-gated `Runtime::run()` and `RuntimeHandle`
|
||||||
|
|
||||||
|
**File**: `src/runtime.rs`
|
||||||
|
|
||||||
|
`Runtime::run()` calls `std::thread::spawn()` which is not available on wasm32.
|
||||||
|
Both `run()` and `RuntimeHandle` (which holds `JoinHandle<()>`) are gated:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn run(self) -> Result<RuntimeHandle, Error> { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
On wasm32, the browser crate will provide its own `run()` via Web Workers.
|
||||||
|
`tick()` remains available on all platforms for single-threaded driving.
|
||||||
|
|
||||||
|
### 4. Updated `crates/wasm/` to use `wasm` feature
|
||||||
|
|
||||||
|
**File**: `crates/wasm/Cargo.toml`
|
||||||
|
|
||||||
|
Changed from `features = ["no_random"]` to `features = ["wasm"]` to pick up
|
||||||
|
the `web-time` Instant on wasm32.
|
||||||
|
|
||||||
|
## What Did NOT Need Abstraction
|
||||||
|
|
||||||
|
Key discovery: on wasm32 with the `+atomics` target feature, most of
|
||||||
|
`std::sync` and `std::thread` works:
|
||||||
|
|
||||||
|
- `OnceLock<Thread>` — compiles and works (futex-based)
|
||||||
|
- `Thread::unpark()` — works (futex → `memory.atomic.notify`)
|
||||||
|
- `thread::park_timeout()` — works (futex → `memory.atomic.wait32`)
|
||||||
|
- `thread::yield_now()` — works (no-op on wasm)
|
||||||
|
- `Mutex`, `RwLock` — work (futex-based)
|
||||||
|
- `crossbeam-queue` — works (uses `core::sync::atomic`)
|
||||||
|
- `AtomicBool/Usize/U64` — work (wasm atomic instructions)
|
||||||
|
|
||||||
|
Only `std::thread::spawn()` and `JoinHandle` are not functional on wasm32.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `cargo test` — all native tests pass (no regressions)
|
||||||
|
- `cargo test --features wasm` — all native tests pass with wasm feature
|
||||||
|
- `cargo build --target wasm32-unknown-unknown --features wasm --no-default-features` — compiles
|
||||||
|
- `cargo build --target wasm32-unknown-unknown -p wasm` — existing PoC crate compiles
|
||||||
|
|
@ -18,6 +18,13 @@ pub mod runtime;
|
||||||
#[cfg(feature = "transport")]
|
#[cfg(feature = "transport")]
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
|
|
||||||
|
// Platform-aware Instant: web_time on wasm, std::time on native.
|
||||||
|
// web_time is a no-op re-export of std::time::Instant on non-wasm targets.
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
pub(crate) use web_time::Instant;
|
||||||
|
#[cfg(not(feature = "wasm"))]
|
||||||
|
pub(crate) use std::time::Instant;
|
||||||
|
|
||||||
#[cfg(feature = "getrandom")]
|
#[cfg(feature = "getrandom")]
|
||||||
pub(crate) fn get_random(buf: &mut [u8]) {
|
pub(crate) fn get_random(buf: &mut [u8]) {
|
||||||
getrandom::getrandom(buf).unwrap()
|
getrandom::getrandom(buf).unwrap()
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ use std::any::Any;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex, OnceLock};
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
use std::thread::{self, JoinHandle, Thread};
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use std::time::Instant;
|
use std::thread::{self, JoinHandle};
|
||||||
|
use std::thread::Thread;
|
||||||
|
use crate::Instant;
|
||||||
|
|
||||||
use crate::actor::{Actor, ActorAddress, ActorExited, ActorInterface, AnyActor, ExitReason, Message, StopSignal, TimerRequest};
|
use crate::actor::{Actor, ActorAddress, ActorExited, ActorInterface, AnyActor, ExitReason, Message, StopSignal, TimerRequest};
|
||||||
use crate::channel::{Receiver, Sender};
|
use crate::channel::{Receiver, Sender};
|
||||||
|
|
@ -67,11 +69,13 @@ impl<R: Message> Ask<R> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle for dealing with a runtime that has started via the `Runtime::run()` method.
|
/// Handle for dealing with a runtime that has started via the `Runtime::run()` method.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub struct RuntimeHandle {
|
pub struct RuntimeHandle {
|
||||||
pub runtime: Arc<Runtime>,
|
pub runtime: Arc<Runtime>,
|
||||||
threads: Vec<JoinHandle<()>>,
|
threads: Vec<JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl RuntimeHandle {
|
impl RuntimeHandle {
|
||||||
pub fn join(self) {
|
pub fn join(self) {
|
||||||
for handle in self.threads {
|
for handle in self.threads {
|
||||||
|
|
@ -333,6 +337,9 @@ impl Runtime {
|
||||||
///
|
///
|
||||||
/// Works in both single-threaded and multi-threaded configurations.
|
/// Works in both single-threaded and multi-threaded configurations.
|
||||||
/// In single-threaded mode, one background thread is spawned.
|
/// In single-threaded mode, one background thread is spawned.
|
||||||
|
///
|
||||||
|
/// Not available on wasm32 — use the browser crate's Web Worker-based run instead.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn run(self) -> Result<RuntimeHandle, Error> {
|
pub fn run(self) -> Result<RuntimeHandle, Error> {
|
||||||
self.is_running.store(true, Ordering::Release);
|
self.is_running.store(true, Ordering::Release);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ use std::any::Any;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Instant;
|
use crate::Instant;
|
||||||
|
|
||||||
use crate::actor::{ActorAddress, ActorExited, AnyActor, CloneMsg, ContextInner, Ctx, ExitReason, StopReason, StopSignal, TimerRequest};
|
use crate::actor::{ActorAddress, ActorExited, AnyActor, CloneMsg, ContextInner, Ctx, ExitReason, StopReason, StopSignal, TimerRequest};
|
||||||
use crate::channel::Receiver;
|
use crate::channel::Receiver;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue