Skip to content

Instantly share code, notes, and snippets.

@lmmx
Last active March 25, 2025 14:21
Show Gist options
  • Save lmmx/1c223daaeb9cfb5606230b736117b873 to your computer and use it in GitHub Desktop.
Save lmmx/1c223daaeb9cfb5606230b736117b873 to your computer and use it in GitHub Desktop.
A simple Rust & WebAssembly example that uses a thread-local RefCell to manage shared state, updated via an HTML slider.

Demo of WASM Shared State Using a RefCell

This code demonstrates how to maintain shared state in a WebAssembly (Wasm) application written in Rust. It uses a thread-local RefCell to store data that can be accessed and updated by multiple pieces of code (e.g., event handlers). Specifically, it wires an HTML slider to:

  1. Read the slider’s current value.
  2. Store that value in the shared RefCell (making it globally accessible).
  3. Update a text element in the DOM to display the newly stored value.

Even though it’s called “thread_local,” in typical single-threaded Wasm setups there’s only one “thread,” so effectively you get a convenient global store. The RefCell allows safe interior mutability—meaning you can change the data without needing to pass around mutable references everywhere.

# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "log"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]]
name = "once_cell"
version = "1.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "trunk-hello-world"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"once_cell",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[package]
name = "trunk-hello-world"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
path = "lib.rs"
[dependencies]
console_error_panic_hook = "0.1.7"
once_cell = { version = "1.21.1", default-features = false }
wasm-bindgen = "0.2.100"
web-sys = { version = "0.3.77", features = ["Window", "Document", "HtmlElement", "HtmlInputElement", "Text", "Event"] }
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My WASM Demo</title>
</head>
<body>
<h1>Vanilla Rust + WebSys + Float Array Demo</h1>
<p>Drag the slider to update a float array in Rust, see the result in the DOM.</p>
<input type="range" id="slider" min="0" max="100" value="50" />
<div id="output"></div>
<!-- Trunk will build and inject our WASM+JS -->
<script data-trunk src="lib.rs"></script>
</body>
</html>
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{window, HtmlInputElement, HtmlElement};
use console_error_panic_hook;
use std::cell::RefCell;
// ----------------------
// PART 1: Shared State
// ----------------------
struct SharedState {
data: Vec<f32>,
}
impl SharedState {
fn new(len: usize) -> Self {
Self {
data: vec![0.0; len],
}
}
fn set_value(&mut self, index: usize, val: f32) {
self.data[index] = val;
}
fn get_value(&self, index: usize) -> f32 {
self.data[index]
}
}
// A single-thread “thread-local” global. Each thread would have its own instance.
// For most Wasm setups, there's only one thread anyway.
thread_local! {
static SHARED_STATE: RefCell<SharedState> = RefCell::new(SharedState::new(10));
}
// Helpers to modify/read the state. Use `SHARED_STATE.with(...)`.
fn set_shared_value(index: usize, val: f32) {
SHARED_STATE.with(|cell| {
cell.borrow_mut().set_value(index, val);
});
}
fn get_shared_value(index: usize) -> f32 {
SHARED_STATE.with(|cell| cell.borrow().get_value(index))
}
// ----------------------
// PART 2: Startup Code
// ----------------------
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
console_error_panic_hook::set_once();
// Let's store an initial value to prove it's working
set_shared_value(0, 50.0);
// 1) Grab document & body
let document = window()
.and_then(|win| win.document())
.ok_or("failed to get document")?;
let output_div = document
.get_element_by_id("output")
.ok_or("no #output element")?
.dyn_into::<HtmlElement>()?;
// 2) Grab the slider
let slider = document
.get_element_by_id("slider")
.ok_or("no #slider element")?
.dyn_into::<HtmlInputElement>()?;
// Clone them for the closure
let slider_clone = slider.clone();
let output_div_clone = output_div.clone();
// 3) Create a closure that updates the shared array and the DOM
let closure = Closure::wrap(Box::new(move || {
let val = slider_clone.value().parse::<f32>().unwrap_or(0.0);
set_shared_value(0, val);
let stored = get_shared_value(0);
output_div_clone.set_inner_text(&format!("Stored value is: {:.2}", stored));
}) as Box<dyn FnMut()>);
// 4) Attach the closure as input event listener
slider.add_event_listener_with_callback("input", closure.as_ref().unchecked_ref())?;
// keep closure alive
closure.forget();
// 5) Run it once at startup
slider.dispatch_event(&web_sys::Event::new("input")?)?;
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment