feat(worker): background WASM workers with isolated fuel/memory and m…#48
Conversation
…essage passing
Adds a background worker capability so guest apps can offload CPU-heavy work to a
separate .wasm module running on its own thread, keeping the frame loop responsive.
Workers never share Rust types or linear memory with the parent — communication is
pure byte-message passing, consistent with Oxide's airtight-sandbox model.
Host (oxide-browser/src/worker.rs):
- WorkerState registry plus a per-worker thread that fetches (http(s)/file://),
compiles, instantiates with its own bounded memory and 500M fuel, runs
start_app(), then delivers inbox messages to the worker's on_message export.
Reuses the api_load_module instantiation pattern.
- register_worker_functions wires six host imports into register_host_functions:
- parent side: api_spawn_worker, api_worker_post_message, api_worker_recv,
api_worker_terminate
- worker side: api_worker_post (reply to parent), api_worker_message_read
(pull current payload, mirroring the events API)
- HostState gains workers (lazy registry), worker_outbox, and worker_current_msg.
Workers share only the persistent KV store and console with their parent;
canvas, input, timers, and memory are isolated.
SDK (oxide-sdk/src/lib.rs):
- FFI declarations and documented safe wrappers: spawn_worker, worker_post_message,
worker_recv, worker_terminate, worker_post, worker_message_read.
Examples:
- worker-demo: UI that spawns a worker (URL resolved relative to its own module),
sends a limit, polls for the result, and animates a spinner to show the main
thread never blocks.
- worker-demo-bg: counts primes below the given limit off-thread and posts it back.
Design notes:
- Pull-based delivery (on_message(len) + worker_message_read) avoids requiring a
guest allocator and matches the existing events pattern.
- Poll-based replies (worker_recv) mirror ws_recv/fetch_recv idioms.
Checks off the spawn/post/terminate items under Phase 4 (Background Workers) in
ROADMAP.md. Opt-in shared memory and worker pools are intentionally left out.
Co-authored-by: Cursor <cursoragent@cursor.com>
📝 WalkthroughWalkthroughThis pull request implements a complete background worker system for Oxide guest WASM modules, enabling CPU-bound tasks to run on dedicated threads without blocking the main UI frame. The system includes guest-side SDK bindings, a host-side worker manager with message-passing IPC, and two runnable examples demonstrating prime-counting offloading. ChangesBackground Workers Feature
Sequence DiagramsequenceDiagram
participant MainApp as Main App (worker-demo)
participant HostAPI as Host API
participant WorkerThread as Worker Thread
participant WorkerModule as Worker Module (worker-demo-bg)
MainApp->>HostAPI: spawn_worker("worker-demo-bg.wasm")
HostAPI->>HostAPI: fetch_worker_bytes (HTTP)
HostAPI->>WorkerThread: Create OS thread
WorkerThread->>WorkerModule: Instantiate + start_app()
MainApp->>HostAPI: worker_post_message(handle, limit_bytes)
HostAPI->>WorkerThread: Enqueue message in inbox
WorkerThread->>WorkerModule: on_message(len)
WorkerModule->>WorkerModule: Read limit, compute primes
WorkerModule->>HostAPI: worker_post(result_bytes)
HostAPI->>HostAPI: Enqueue in parent outbox
MainApp->>HostAPI: worker_recv(handle)
HostAPI->>MainApp: Return result_bytes
MainApp->>HostAPI: worker_terminate(handle)
HostAPI->>WorkerThread: Send Terminate
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/worker-demo-bg/src/lib.rs`:
- Around line 41-53: The loop condition in is_prime uses d * d which can
overflow u32 when d >= 65536; change the check to avoid multiplying (e.g., use
(d as u64) * (d as u64) <= n as u64 or, preferably, use d <= n / d) and adjust
comparisons accordingly so the loop terminates correctly for large n; also
either confirm the project MSRV/toolchain is >= 1.87.0 before keeping
u32::is_multiple_of or replace n.is_multiple_of(d) with n % d == 0 to avoid
relying on that stabilization.
In `@examples/worker-demo/src/lib.rs`:
- Around line 86-101: Prevent re-entry by ensuring the spawn button cannot run
while a worker is active: before calling ui_button/spawn_worker check the state
variable s.phase (and/or s.handle) and only allow spawning when s.phase != 1
(i.e., no worker running); if s.phase == 1 skip the spawn logic (or render the
button disabled) so you do not call spawn_worker or overwrite s.handle while a
worker is active, and keep existing worker_post_message and transition logic
unchanged for the normal spawn path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: cc38398b-8a5d-4149-bcfa-a35f487e8885
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (10)
Cargo.tomlROADMAP.mdexamples/worker-demo-bg/Cargo.tomlexamples/worker-demo-bg/src/lib.rsexamples/worker-demo/Cargo.tomlexamples/worker-demo/src/lib.rsoxide-browser/src/capabilities.rsoxide-browser/src/lib.rsoxide-browser/src/worker.rsoxide-sdk/src/lib.rs
| fn is_prime(n: u32) -> bool { | ||
| if n < 2 { | ||
| return false; | ||
| } | ||
| let mut d = 2u32; | ||
| while d * d <= n { | ||
| if n.is_multiple_of(d) { | ||
| return false; | ||
| } | ||
| d += 1; | ||
| } | ||
| true | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In which Rust version was the integer method is_multiple_of (e.g. u32::is_multiple_of) stabilized?
💡 Result:
The integer method is_multiple_of (e.g., u32::is_multiple_of) was stabilized in Rust 1.87.0 [1]. Although the initial implementation for unsigned integers was merged into the master branch in 2024 (as part of the 1.82.0 milestone) [2], the feature completed its stabilization process later, with the final stabilization pull request being merged in February 2025 [1]. The tracking issue confirms that the method is part of the 1.87.0 release [1].
Citations:
- 1: Tracking Issue for
unsigned_is_multiple_ofrust-lang/rust#128101 - 2: add
is_multiple_offor unsigned integer types rust-lang/rust#128103
Fix u32 overflow in is_prime and check is_multiple_of MSRV
-
d * doverflowsu32onced >= 65536, which can break the loop condition for largen(release wrapping vs debug panics), risking incorrect results.🛡️ Proposed fix using widened multiplication
-
let mut d = 2u32;
-
while d * d <= n {
- let mut d = 2u32;
- while (d as u64) * (d as u64) <= n as u64 {
if n.is_multiple_of(d) {
return false;
}
d += 1;
}
</details>
- `u32::is_multiple_of` is stabilized in Rust **1.87.0**; confirm the project’s MSRV/toolchain is ≥ 1.87.0.
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
fn is_prime(n: u32) -> bool {
if n < 2 {
return false;
}
let mut d = 2u32;
while (d as u64) * (d as u64) <= n as u64 {
if n.is_multiple_of(d) {
return false;
}
d += 1;
}
true
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/worker-demo-bg/src/lib.rs` around lines 41 - 53, The loop condition
in is_prime uses d * d which can overflow u32 when d >= 65536; change the check
to avoid multiplying (e.g., use (d as u64) * (d as u64) <= n as u64 or,
preferably, use d <= n / d) and adjust comparisons accordingly so the loop
terminates correctly for large n; also either confirm the project MSRV/toolchain
is >= 1.87.0 before keeping u32::is_multiple_of or replace n.is_multiple_of(d)
with n % d == 0 to avoid relying on that stabilization.
| if ui_button(1, 20.0, 110.0, 240.0, 32.0, "Run in background worker") { | ||
| let base = get_url(); | ||
| if let Some(url) = url_resolve(&base, "worker_demo_bg.wasm") { | ||
| let handle = spawn_worker(&url); | ||
| if handle > 0 { | ||
| s.handle = handle as u32; | ||
| s.phase = 1; | ||
| s.result = 0; | ||
| worker_post_message(s.handle, &LIMIT.to_le_bytes()); | ||
| } else { | ||
| log("Failed to spawn worker."); | ||
| } | ||
| } else { | ||
| log("Could not resolve worker URL."); | ||
| } | ||
| } |
There was a problem hiding this comment.
Guard the spawn button against re-entry while computing.
The button is evaluated every frame irrespective of s.phase. Clicking it while a worker is still running (phase == 1) spawns a second worker and overwrites s.handle, leaking the first worker thread — its handle is lost so it can never be terminated.
🛠️ Proposed guard
- if ui_button(1, 20.0, 110.0, 240.0, 32.0, "Run in background worker") {
+ if s.phase != 1 && ui_button(1, 20.0, 110.0, 240.0, 32.0, "Run in background worker") {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/worker-demo/src/lib.rs` around lines 86 - 101, Prevent re-entry by
ensuring the spawn button cannot run while a worker is active: before calling
ui_button/spawn_worker check the state variable s.phase (and/or s.handle) and
only allow spawning when s.phase != 1 (i.e., no worker running); if s.phase == 1
skip the spawn logic (or render the button disabled) so you do not call
spawn_worker or overwrite s.handle while a worker is active, and keep existing
worker_post_message and transition logic unchanged for the normal spawn path.
…essage passing
Adds a background worker capability so guest apps can offload CPU-heavy work to a separate .wasm module running on its own thread, keeping the frame loop responsive. Workers never share Rust types or linear memory with the parent — communication is pure byte-message passing, consistent with Oxide's airtight-sandbox model.
Host (oxide-browser/src/worker.rs):
SDK (oxide-sdk/src/lib.rs):
Examples:
Design notes:
Checks off the spawn/post/terminate items under Phase 4 (Background Workers) in ROADMAP.md. Opt-in shared memory and worker pools are intentionally left out.
Summary by CodeRabbit
New Features
Documentation
Chores