Rust to WebAssembly skill for Codex

Coarse-grained Wasm modules that stay small. Codex first checks that Wasm fits the job, heavy compute in few calls rather than chatty DOM work, then sets up a cdylib crate with a narrow wasm-bindgen API that returns JsError instead of panicking. It picks the right wasm-pack target for your bundler, keeps the CLI and crate versions in lockstep, shrinks the output with release profile settings and wasm-opt, and tests both natively and in a headless browser.

  • Languages
  • $rust-wasm
  • 202 lines
  • 2 files
  • ~1,900 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- rust-wasm

Installs to ~/.agents/skills/rust-wasm/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $rust-wasm, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Compiles Rust to WebAssembly for browsers and Node with wasm-bindgen and wasm-pack - cdylib setup, wasm_bindgen attribute exports, js-sys and web-sys features, async with wasm-bindgen-futures, serde-wasm-bindgen for data, panic hooks, getrandom on wasm, size reduction with wasm-opt, and headless tests - plus when WASI targets fit instead. Use when moving Rust code into a web app, debugging wasm-bindgen version mismatches or unreachable panics, shrinking a .wasm bundle, or deciding whether Wasm is faster than plain JavaScript for a task.

Use it when

  • Exposing a Rust library to browser or Node JavaScript
  • wasm-bindgen version mismatches, getrandom or web-sys build errors
  • Shrinking or profiling a .wasm bundle
  • Deciding whether Wasm beats JavaScript for a workload

Use something else when

How to check the result

cargo test and wasm-pack test pass, each export is called once from the consuming app including an error path, and the .wasm size is recorded before and after optimisation.

The mistake it prevents

Using a wasm-bindgen CLI whose version differs from the wasm-bindgen crate in Cargo.lock, which fails with a schema mismatch.

~/.agents/skills/rust-wasm/SKILL.md

--- name: rust-wasm description: Compiles Rust to WebAssembly for browsers and Node with wasm-bindgen and wasm-pack - cdylib setup, wasm_bindgen attribute exports, js-sys and web-sys… license: MIT ---

A good Rust/Wasm module has a narrow, coarse-grained JS interface (few calls, large chunks of work per call), ships a small optimised .wasm, reports panics readably in the console, and is tested both as Rust and through its generated JS bindings.

Decide first

GoalFit
CPU-heavy work on large data (parsing, codecs, image/geometry, crypto, simulation)good
Reusing an existing Rust library in the browsergood
DOM-heavy UI logicpoor: every DOM call crosses the boundary; keep it in JS or use a Rust UI framework knowingly
Many tiny calls per framepoor: boundary and copying costs dominate
Server-side sandboxed plugins, CLI toolsWASI (wasm32-wasip1 / wasm32-wasip2) with Wasmtime, not wasm-bindgen

Measure before committing: a plain JS implementation with typed arrays is often within 2x, and the Wasm version adds a build step and download size.

Setup

  1. Read AGENTS.md, the crate’s Cargo.toml, and the consuming JS project’s bundler (Vite, webpack, Next.js) and package.json.
  2. Install the target: rustup target add wasm32-unknown-unknown. Tools: cargo install wasm-pack (or wasm-bindgen-cli). These need network access; ask first.
  3. Tooling status: the rustwasm GitHub org was archived in 2025. wasm-bindgen continues under its own organisation; wasm-pack moved to a community maintainer. Both work, but check the current repository and docs location before relying on old book links.
[package]
name = "imgcore"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib", "rlib"]     # cdylib for wasm, rlib so native tests and benches work

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
serde = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.6"
console_error_panic_hook = { version = "0.1", optional = true }

[dependencies.web-sys]
version = "0.3"
features = ["console", "ImageData"]  # each Web API type is opt-in

[dev-dependencies]
wasm-bindgen-test = "0.3"

[features]
default = ["console_error_panic_hook"]

[profile.release]
opt-level = "z"        # or "s"; measure both, "3" if speed matters more than size
lto = true
codegen-units = 1
panic = "abort"

Exporting an API

use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
pub fn init() {
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();
}

#[derive(Deserialize)]
pub struct BlurOptions { radius: u32 }

#[derive(Serialize)]
pub struct Stats { min: u8, max: u8, mean: f32 }

/// Blurs RGBA pixels in place. One call per image, not per pixel.
#[wasm_bindgen]
pub fn blur(pixels: &mut [u8], width: u32, height: u32, opts: JsValue) -> Result<(), JsError> {
    let opts: BlurOptions = serde_wasm_bindgen::from_value(opts)?;
    if pixels.len() != (width * height * 4) as usize {
        return Err(JsError::new("pixel buffer does not match width * height * 4"));
    }
    crate::core::blur_rgba(pixels, width, height, opts.radius);
    Ok(())
}

#[wasm_bindgen]
pub fn stats(pixels: &[u8]) -> Result<JsValue, JsError> {
    let s: Stats = crate::core::stats(pixels);
    Ok(serde_wasm_bindgen::to_value(&s)?)
}

Boundary rules:

  • &[u8] / &mut [u8] map to Uint8Array (copied in, and for &mut copied back). Vec<u8> return values are copied out. For very large buffers, expose a pointer into Wasm memory only if profiling shows copies matter, and document that the view is invalidated when memory grows.
  • Return Result<T, JsError>: errors become thrown JS exceptions with your message. Never unwrap() on input from JS; a panic becomes RuntimeError: unreachable.
  • Exported structs (#[wasm_bindgen] pub struct) live in Wasm memory; JS must call .free() (or use using / FinalizationRegistry support in recent wasm-bindgen). Prefer plain data via serde for results.
  • Keep the core logic in a plain Rust module with no wasm-bindgen types so it is testable natively and reusable.

Async and browser APIs

use wasm_bindgen::{prelude::*, JsCast};
use wasm_bindgen_futures::JsFuture;
use web_sys::Response;

#[wasm_bindgen]
pub async fn fetch_text(url: String) -> Result<String, JsValue> {
    let window = web_sys::window().ok_or("no window")?;
    let resp: Response = JsFuture::from(window.fetch_with_str(&url)).await?.dyn_into()?;
    let text = JsFuture::from(resp.text()?).await?;
    Ok(text.as_string().unwrap_or_default())
}

Requires wasm-bindgen-futures and the Window and Response web-sys features. async exports return a JS Promise. Threads need SharedArrayBuffer, cross-origin isolation headers and a nightly toolchain; avoid unless essential.

Building

ConsumerCommand
Vite / webpack / bundlerwasm-pack build --release --target bundler
Plain ES modules in the browser, no bundlerwasm-pack build --release --target web then await init()
Node.jswasm-pack build --release --target nodejs
Denowasm-pack build --release --target deno

Without wasm-pack:

cargo build --release --target wasm32-unknown-unknown
wasm-bindgen --target web --out-dir pkg target/wasm32-unknown-unknown/release/imgcore.wasm
wasm-opt -Oz -o pkg/imgcore_bg.opt.wasm pkg/imgcore_bg.wasm   # binaryen

The wasm-bindgen CLI version must equal the wasm-bindgen crate version in Cargo.lock exactly, or you get a schema mismatch error. Pin both, and install the CLI with cargo install wasm-bindgen-cli --version <same>.

Vite needs vite-plugin-wasm (and often vite-plugin-top-level-await) for --target bundler output, or use --target web and call init() yourself.

Common failures

SymptomFix
RuntimeError: unreachablea panic; enable console_error_panic_hook to see the message, then replace the unwrap/index with an error
getrandom fails to compile for wasm32enable getrandom’s JS backend: for 0.2 the js feature; for 0.3 the wasm_js feature and, depending on the 0.3 release, the getrandom_backend="wasm_js" cfg; follow the getrandom docs for the locked version
“it looks like the Rust project used to create this wasm file was linked against version X of wasm-bindgen”CLI and crate versions differ; align them
Huge .wasm (MBs)release profile above, wasm-opt -Oz, drop format!-heavy code and large deps; inspect with twiggy top
web_sys::X not foundadd the feature for that type in Cargo.toml
Works in dev, fails after deployserver must serve .wasm as application/wasm for streaming instantiation

Testing

#[cfg(test)]
mod tests {
    use wasm_bindgen_test::*;
    wasm_bindgen_test_configure!(run_in_browser);

    #[wasm_bindgen_test]
    fn rejects_bad_buffer() {
        let mut px = vec![0u8; 3];
        assert!(super::blur(&mut px, 1, 1, wasm_bindgen::JsValue::NULL).is_err());
    }
}
cargo test                                  # core logic, native
wasm-pack test --headless --firefox         # or --chrome; needs the browser + driver
wasm-pack test --node                       # without run_in_browser

Verification

  • cargo test (native) and wasm-pack test (Wasm) pass.
  • cargo clippy --target wasm32-unknown-unknown is clean.
  • Record .wasm size before and after (raw and gzip/brotli): ls -l pkg/*.wasm.
  • A smoke test in the consuming app calls each export once, including an error path.
  • Benchmark against the JS baseline if performance was the reason for Wasm.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Rust to WebAssembly"
  short_description: "Coarse-grained Wasm modules that stay small"
  default_prompt: "$rust-wasm Compile our Rust image library to WebAssembly and call it from the Vite app"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem