Skip to content

feat(worker): background WASM workers with isolated fuel/memory and m…#48

Merged
niklabh merged 1 commit into
mainfrom
feat/background-workers
May 29, 2026
Merged

feat(worker): background WASM workers with isolated fuel/memory and m…#48
niklabh merged 1 commit into
mainfrom
feat/background-workers

Conversation

@niklabh
Copy link
Copy Markdown
Owner

@niklabh niklabh commented May 29, 2026

…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.

Summary by CodeRabbit

  • New Features

    • Added background worker support, enabling concurrent task execution with message-based communication between parent and worker threads.
    • Included example applications demonstrating worker spawning and messaging patterns.
  • Documentation

    • Updated roadmap with worker messaging API specifications.
  • Chores

    • Extended workspace configuration to include new example projects.

Review Change Stack

…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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

This 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.

Changes

Background Workers Feature

Layer / File(s) Summary
Worker API bindings in oxide-sdk
oxide-sdk/src/lib.rs
Guest code calls spawn_worker(url) to create a worker, worker_post_message(handle, data) to send messages, worker_recv(handle) to drain replies, worker_terminate(handle) to clean up, and worker-side calls worker_post(data) to reply and worker_message_read(buf) to access inbound messages.
Worker threading model and module loading
oxide-browser/src/worker.rs (intro, structures, helpers)
Defines WorkerState hashmap for worker lifecycle tracking, inbox/outbox message channels, and synchronization primitives; provides fetch_worker_bytes to load worker WASM from HTTP(S) or file:// URLs with timeout and proper headers.
Worker main loop and instance lifecycle
oxide-browser/src/worker.rs (worker_main)
Creates a Wasmtime store, compiles and instantiates the worker module, calls start_app once, then polls the inbox in a loop to invoke on_message per message with fuel limits applied; logs and recovers from traps.
Worker host function wiring
oxide-browser/src/worker.rs (api_* functions)
Registers six host functions: api_spawn_worker (allocates worker id and thread), api_worker_post_message (enqueues message), api_worker_recv (drains outbox), api_worker_terminate (stops worker), api_worker_post (replies from worker), api_worker_message_read (accesses inbound buffer).
HostState updates and module registration
oxide-browser/src/capabilities.rs, oxide-browser/src/lib.rs
HostState struct adds workers registry and per-worker message buffers; register_host_functions now calls register_worker_functions(linker) to wire the worker API; module is exported as pub mod worker.
Background worker implementation (worker-demo-bg)
examples/worker-demo-bg/Cargo.toml, examples/worker-demo-bg/src/lib.rs
Example WASM worker exports start_app and on_message(len) that reads a prime limit, counts primes via trial division using an is_prime helper, and posts the count back as little-endian u64.
Main app using worker (worker-demo)
examples/worker-demo/Cargo.toml, examples/worker-demo/src/lib.rs
Example WASM app exports start_app and on_frame that spawns the worker module on button press, posts a prime limit, non-blockingly polls for results with worker_recv, decodes the reply, and renders UI status and animated spinner.
Workspace and documentation updates
Cargo.toml, ROADMAP.md
Adds examples/worker-demo and examples/worker-demo-bg to workspace members; updates Background Workers roadmap section to document poll-based worker_recv and on_message(len) exports instead of callback-based messaging.

Sequence Diagram

sequenceDiagram
  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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A rabbit's tale of workers bold,
Threading spinners, primes to fold,
Messages pass through queues so bright,
Main frame spins while threads compute right,
WASM workers dance on dedicated ground!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature: background WASM workers with isolated execution environments and message passing, matching the primary changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/background-workers

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between bc89298 and 2262851.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • Cargo.toml
  • ROADMAP.md
  • examples/worker-demo-bg/Cargo.toml
  • examples/worker-demo-bg/src/lib.rs
  • examples/worker-demo/Cargo.toml
  • examples/worker-demo/src/lib.rs
  • oxide-browser/src/capabilities.rs
  • oxide-browser/src/lib.rs
  • oxide-browser/src/worker.rs
  • oxide-sdk/src/lib.rs

Comment on lines +41 to +53
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
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 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:


Fix u32 overflow in is_prime and check is_multiple_of MSRV

  • d * d overflows u32 once d >= 65536, which can break the loop condition for large n (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.

Comment on lines +86 to +101
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.");
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

@niklabh niklabh merged commit d5c5597 into main May 29, 2026
5 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant