agentleFS
po4ykapo4yka/rust-skills/skills/rust-wasm/SKILL.md

rust-wasm

Skill5 starsChanged 10 days agoBSD-3-ClauseView on GitHub

Use when building, testing, or shrinking Rust for WebAssembly (wasm32-unknown-unknown, wasm32-wasip1, wasm32-wasip2, wasm32v1-none) in a browser, Node.js, a WASI runtime, or a WebAssembly Component Model host, or when diagnosing a wasm-bindgen boundary, instantiation, link, target-feature, or wasm binary size failure. Triggers on wasm-pack, wasm-bindgen, wit-bindgen, wasmtime, WASI, getrandom wasm_js, and "time not implemented on this platform".

3 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

18 rules. 0 are common, 5 are distinctive, and 13 are very specific to this project.

  • 0

    Common

    Most of the 3 edited versions we read keep these.

  • 5

    Distinctive

    This author's own choices. Worth a look.

  • 13

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When you add a WebAssembly target, a host, or a published package, record these facts.

    line 19
  2. Very specific:

    Do not infer the host from target_arch = "wasm32".

    line 30
  3. Very specific:

    Do not use wasm32-unknown-unknown as a generic WASI target.

    line 44
  4. Distinctive:

    Do not enable a WebAssembly target feature only because the compiler accepts it.

    line 55
  5. Distinctive:

    Use four layers. Stop only when all applicable layers pass.

    line 87
  6. Very specific:

    Run portable logic on the host with cargo nextest run --locked, or cargo test --locked when cargo-nextest is not installed.

    line 89
  7. Distinctive:

    Run boundary tests in each real host mode.

    line 92
  8. Very specific:

    Do not replace browser tests with Node.js tests.

    line 104
  9. Distinctive:

    Add one JavaScript integration test that imports the generated package, awaits initialization when required, calls a success path, and …

    line 108
  10. Very specific:

    Read references/wasi.md when a matrix row targets wasm32-wasip1 or wasm32-wasip2.

    line 131
  11. Very specific:

    Run the checks again after a toolchain bump, a dependency bump, or a wasm-opt change.

    line 139
  12. Very specific:

    Put parsing, validation, state transitions, and business rules in a normal Rust library.

    line 164
  13. Very specific:

    Use target predicates only for facts that the compiler knows: cfg(all(target_family = "wasm", target_os = "unknown")) for a bare JavaScript …

    line 167
  14. Very specific:

    Read references/javascript-host.md when the crate exports to a browser or Node.js through wasm-bindgen: it has the crate type, the …

    line 214
  15. Very specific:

    Read references/javascript-host.md when you design exports, imports, callbacks, or error values that cross the JavaScript boundary.

    line 220
  16. Very specific:

    Read references/panic-and-errors.md when you choose the panic strategy, install a panic hook, or decide whether an instance survives a trap.

    line 225
  17. Very specific:

    Read references/async-hosts.md when the code awaits a JavaScript Promise, spawns a future, uses Tokio or threads, or does async I/O on WASI.

    line 230
  18. Very specific:

    Read references/wasi.md when you ship a WASI component or compile C or C++ for a WASI target.

    line 235

“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Rust WebAssembly

Build and verify Rust for the exact WebAssembly host that runs it. A .wasm suffix does not define an ABI, an import set, or a runtime.

Related skills, when installed: cargo-workflows for features and profiles, rust-async-internals for executor-independent futures, rust-performance and rust-hot-path for a measured hotspot. This skill owns the host event loop, WebAssembly limits, artifact size, and boundary costs.

Write the host contract first

When you add a WebAssembly target, a host, or a published package, record these facts. They decide the target, the dependencies, and the test matrix:

  1. Name the Rust target triple.
  2. Name the host and its minimum version.
  3. Name the artifact format: core module or component.
  4. List allowed imports and capabilities.
  5. Name the JavaScript output mode when JavaScript glue exists.
  6. Name the engine feature baseline: the WebAssembly features that the oldest supported engine accepts.

Do not infer the host from target_arch = "wasm32". Browser, Node.js, WASIp1, and WASIp2 builds can share that architecture and still have incompatible APIs.

Select the target

Host contractRust targetOutputImportant constraint
Browser or JavaScript host with explicit importswasm32-unknown-unknownCore module, often with generated JSstd::fs fails, println! does nothing, std::thread::spawn panics, and Instant::now() and SystemTime::now() panic
Existing WASI Preview 1 hostwasm32-wasip1Core module with wasi_snapshot_preview1 importsUse for compatibility; WASIp1 interfaces no longer grow
WASI 0.2 and Component Model hostwasm32-wasip2ComponentThe host must support components and the WASI interfaces that the program imports
WASIp1 host with the threads proposalwasm32-wasip1-threadsShared-memory core moduleThe host must provide threads and shared memory
Minimal WebAssembly 1.0 engine with no_stdwasm32v1-noneCore moduleUse only when core and alloc are sufficient
WASI 0.3 async component hostwasm32-wasip3ComponentTier 3 as of Rust 1.98.1: rustup ships no std for it. Do not select it for a stable-toolchain product (target page)

Do not use wasm32-unknown-unknown as a generic WASI target. It deliberately has no WASI imports. Do not send a WASIp2 component to an API that accepts only a core module.

The std targets do not emit WebAssembly 1.0 code. They enable multivalue, mutable-globals, reference-types, and sign-ext by default, and bulk-memory and nontrapping-fptoint since Rust 1.87 (LLVM 20). An engine without one of these features can reject the module. For a 1.0 engine, use wasm32v1-none on stable, or rebuild std for the MVP on nightly. Read references/target-setup.md when the engine baseline is older than these defaults or the MSRV is below 1.87.

Do not enable a WebAssembly target feature only because the compiler accepts it. The final module can contain those instructions even when a runtime path does not execute them. Gate optional SIMD code with #[cfg(target_feature = "simd128")] and keep a portable implementation.

Install each target with rustup target add <triple>. Rust 1.84 removed the wasm32-wasi name, so that command fails for it. Rename it to wasm32-wasip1 in CI, .cargo/config.toml, and rust-toolchain.toml.

Triage

SymptomLikely causeFirst checkFix
unknown import at instantiationWrong target or missing host capabilityInspect imports with wasm-tools printSelect the correct target or implement and grant the import
rust-lld: error: ...: undefined symbol: <name> on Rust 1.96+WebAssembly targets no longer pass --allow-undefined, so an undeclared import no longer becomes an env importFind the extern block or the C object that should provide <name>Put #[link(wasm_import_module = "<module>")] on the extern block of a host import; fix the C build otherwise. Use -Clink-arg=--allow-undefined only as a temporary migration shim
Wasmtime: expected a WebAssembly module but was given a WebAssembly component; V8 or Node.js: expected version 01 00 00 00, found 0d 00 01 00Host accepts a core module, but the artifact is a componentRun wasm-tools component witUse a component host or build the required core-module target
compile_error! from getrandom on wasm32-unknown-unknownNo random backend for a JavaScript hostcargo tree --locked --target wasm32-unknown-unknown -e features -i getrandomEnable the backend for each major version in the final cdylib or binary, never in a library (see "Keep portable logic separate")
"rust Wasm file schema version" from wasm-bindgenCLI version differs from the locked crateCompare wasm-bindgen --version with Cargo.lockInstall the exact locked version, or build through wasm-pack
wasm-opt rejects bulk-memory or non-trapping float-to-int instructionsThe optimizer does not enable the Rust 1.87+ default featuresFind found wasm-opt at ... in the wasm-pack log (absent means the bundled binaryen ran), then run wasm-opt --version on that binaryEnable both features in the wasm-opt arguments; references/size-and-optimizer.md has the setting
cc build script fails for a WASI target, for example 'stdio.h' file not foundcc fell back to a system clang with no WASI sysrootecho "$WASI_SDK_PATH", ls "$WASI_SDK_PATH/bin", and the cc version in Cargo.lock (1.2.39 or later)Install wasi-sdk, set WASI_SDK_PATH, and update cc, or set CC_<target>; references/wasi.md has the details
Export is undefinedWrong generated-JS mode or initialization did not finishInspect the generated module and package testMatch the consumer mode and await initialization
"time not implemented on this platform", or a bare RuntimeError: unreachable near timing codeInstant::now() or SystemTime::now() on wasm32-unknown-unknownInstall the panic hook in a debug build and read the messageRead the clock in the JavaScript adapter. For a dependency, enable its wasm or JavaScript feature, or replace it (web-time gives drop-in types)
unreachable or RuntimeError after a Rust failurePanic became a trapEnable bounded panic diagnostics and inspect the first panicReturn a typed error for expected failure; fix the panic; discard the instance when state integrity matters
Callback fails after setup returnsIts Rust Closure was droppedFind the owner and unregister pathStore the closure until listener removal
View returns corrupt or old bytesLinear memory grewCompare memory.buffer before and after allocationRecreate the typed-array view
Browser passes but Node.js failsDOM or ES-module assumption reached Node.jsRun the generated Node.js package testMove the API behind the correct host adapter
Host build passes but Wasm target failsUnconditional OS or thread dependencyRun cargo tree --target <triple> -e features for the rowGate the dependency and expose a portable interface
Module validates but fails on the oldest engineDefault post-1.0 features, an enabled proposal, or a WIT version the host lackswasm-tools validate --features=wasm1 (or wasm2), then the host interface versionsBuild for the baseline (wasm32v1-none, or an MVP std build on nightly) or raise the documented minimum
Size jumps after a small API changeGeneric duplication, extra web-sys features, or glue growthCompare twiggy top or wasm-tools objdump output and package contentsRemove the measured source of growth

Verify the matrix that ships

Use four layers. Stop only when all applicable layers pass.

  1. Run portable logic on the host with cargo nextest run --locked, or cargo test --locked when cargo-nextest is not installed.
  2. Compile every supported target and feature set.
  3. Run boundary tests in each real host mode.
  4. Load the final packaged artifact from the JavaScript package or deployment directory, not from Cargo's target directory.

Browser and Node.js tests:

wasm-pack test --headless --chrome
wasm-pack test --headless --firefox
wasm-pack test --node

Do not replace browser tests with Node.js tests. Node.js has no DOM and uses a different module loader. Do not replace runtime execution with cargo check --target; it does not instantiate the module or resolve imports.

Add one JavaScript integration test that imports the generated package, awaits initialization when required, calls a success path, and verifies the public error shape and any 64-bit value round trip. This catches stale glue, wrong package metadata, and a missing .wasm deployment asset.

Build an explicit matrix in CI. Include the target, Cargo features, host, and minimum host version in each row. Use cargo tree --locked --target <triple> -e features to find an additive Cargo feature that leaks an unsupported dependency into a row.

Validate each final artifact against the engine baseline. Plain wasm-tools validate accepts every phase 4 proposal, so it proves nothing about an old engine. wasm2 includes SIMD; append ,-simd when the baseline has no SIMD. A wasm1 or wasm2 group replaces the whole feature set and turns off the component model, so for a component add ,component-model.

wasm-tools validate --features=wasm2 pkg/<name>_bg.wasm   # WebAssembly 2.0 engine
wasm-tools validate --features=wasm1 pkg/<name>_bg.wasm   # WebAssembly 1.0 engine
wasm-tools validate --features=wasm2,component-model target/wasm32-wasip2/release/<binary>.wasm
wasm-tools print pkg/<name>_bg.wasm | grep '(import '     # compare with the host contract

Read references/wasi.md when a matrix row targets wasm32-wasip1 or wasm32-wasip2. It has the build, validate, and wasmtime run commands. Grant a WASI run only the directories, environment variables, sockets, and other host capabilities that it needs. A run with broad inherited access does not prove the production capability policy. Pin the WIT package versions and the host runtime together. A component can validate and still fail against a different world or interface version.

Run the checks again after a toolchain bump, a dependency bump, or a wasm-opt change. Load the module in the oldest supported engine when the contract names one.

Measure the final packaged .wasm, raw and gzip-compressed, against a size budget. Pin the wasm-pack version and the wasm-opt that it runs: a wasm-pack upgrade can silently change the bundled optimizer. Read references/size-and-optimizer.md when you set a size budget, change the release profile, or change how wasm-opt runs.

Completion evidence

Report:

  • the target, host, minimum version, artifact type, and capability policy;
  • the exact Cargo feature set and the engine feature baseline;
  • the build, validation, and runtime commands for every shipped matrix row, including the wasm-tools validate --features=... baseline check and the import list of each final artifact;
  • a successful import of the final JavaScript package when JS glue exists;
  • raw and compressed artifact sizes against the budget;
  • any browser, Node.js, WASI, or component row that could not run.

Keep portable logic separate

Put parsing, validation, state transitions, and business rules in a normal Rust library. Put browser, Node.js, and WASI adapters at the edge.

Use target predicates only for facts that the compiler knows: cfg(all(target_family = "wasm", target_os = "unknown")) for a bare JavaScript host, and cfg(all(target_os = "wasi", target_env = "p1")) or target_env = "p2" for a WASI preview. Rust cannot detect browser against Node.js with cfg. If the same crate ships to both, select adapters with explicit Cargo features and reject invalid feature sets. Keep one neutral default when possible.

#[cfg(all(feature = "browser", feature = "node"))]
compile_error!("select only one JavaScript host feature");

fn main() {}

Target-specific dependency tables stop browser-only crates from entering WASI resolution:

[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"

[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies.web-sys]
version = "0.3"
features = ["Window", "Performance", "console"]

Enable only the web-sys interfaces that the code uses. Each one adds binding code, compile time, and possible artifact size.

Inject the clock. On wasm32-unknown-unknown, read time in the JavaScript adapter and pass it to the portable code, because std::time panics there. Use js_sys::Date::now(), or window.performance() and then .now() on the result. now is an instance method of web_sys::Performance, so write web_sys::window().and_then(|w| w.performance()).map(|p| p.now()). It needs the Window and Performance features.

getrandom has no default backend on wasm32-unknown-unknown. Enable feature wasm_js (0.3.4 and later, 0.4) or js (0.2) for each major version in the graph. Set it in the final cdylib or binary crate, not in a library: upstream warns that it breaks non-JavaScript WebAssembly builds. Read references/target-setup.md when the graph holds getrandom 0.3.0 to 0.3.3 or more than one major version.

Build for a JavaScript host

Read references/javascript-host.md when the crate exports to a browser or Node.js through wasm-bindgen: it has the crate type, the wasm-pack output modes, the CLI version rule, and initialization.

Design the JavaScript boundary

Read references/javascript-host.md when you design exports, imports, callbacks, or error values that cross the JavaScript boundary.

Handle panic and error behavior

Read references/panic-and-errors.md when you choose the panic strategy, install a panic hook, or decide whether an instance survives a trap.

Use the host async model

Read references/async-hosts.md when the code awaits a JavaScript Promise, spawns a future, uses Tokio or threads, or does async I/O on WASI.

Build WASI artifacts

Read references/wasi.md when you ship a WASI component or compile C or C++ for a WASI target.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.