A WebAssembly (röviden Wasm) mára nem csak egy érdekes kísérlet, hanem egy komolyan vehető céltechnológia, amit a legtöbb modern böngésző natívan támogat. A Rust ökoszisztéma pedig kifejezetten jól felkészült arra, hogy Wasm modulokat gyártsunk vele – nincs garbage collector, kiszámítható a teljesítmény, és a fordító már eleve céltámogatást ad hozzá.
Miért érdemes Rustot használni WebAssembly fejlesztéshez
Mielőtt belevágnánk a gyakorlatba, érdemes tisztázni, miért éppen Rust az egyik legjobb választás Wasm célra.
- Nincs runtime overhead: Rust nem hoz magával garbage collectort vagy nagy runtime-ot, így a lefordított
.wasmfájl kicsi és gyors marad. - Memóriabiztonság ownership-pel: a borrow checker ugyanúgy megvédi a Wasm oldali kódot a memóriahibáktól, mint natív binárisnál.
- Kiváló tooling: a
wasm-packés awasm-bindgenpáros gyakorlatilag "batteries included" élményt ad – nem kell kézzel babrálni a JS-Wasm interop réteget. - Cargo ökoszisztéma: bármilyen
no_std-kompatibilis, vagywasm32-unknown-unknowntargetet támogató crate-et azonnal felhasználhatsz.
Ha csak számításigényes logikát (pl. képfeldolgozás, tömörítés, fizikai szimuláció) szeretnél kiszervezni JavaScriptből, a Rust+Wasm páros tipikusan 2-10x gyorsulást hoz a natív JS implementációhoz képest, főleg ha sok apró objektumallokációt spórolunk meg.
A wasm-pack telepítése és projekt inicializálása
A wasm-pack egy build eszköz, amely egy paranccsal lefordítja a Rust kódot Wasm-má, legenerálja a JS bindingokat, és becsomagolja mindezt egy npm-kompatibilis package formájába.
Telepítés (feltéve, hogy a Rust toolchain már megvan rustup-pal):
cargo install wasm-pack
Ez lefordítja és telepíti a wasm-pack binárist a ~/.cargo/bin alá. Ha inkább a hivatalos telepítő scriptet szeretnéd használni, az is működik:
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
Ezután hozzunk létre egy új projektet a cargo generate sablonjával, vagy egyszerűen kézzel egy sima cargo new --lib-bel:
cargo new --lib rust-wasm-demo
cd rust-wasm-demo
A Cargo.toml-ba be kell húznunk a wasm-bindgen crate-et, és be kell állítanunk a crate típusát cdylib-re, mert a Wasm fordítás dinamikus könyvtárként várja a kimenetet:
[package]
name = "rust-wasm-demo"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
A crate-type mezőben az rlib-et azért hagyjuk meg, hogy a projektet natívan is lehessen tesztelni cargo test-tel – a cdylib önmagában nem támogatja a natív unit teszteket.
Rust függvények exportálása JavaScript felé wasm-bindgen segítségével
A wasm-bindgen lényege, hogy egy #[wasm_bindgen] attribútummal megjelölt függvényt, struct-ot vagy impl blokkot automatikusan exportálhatóvá tesz JS felé, és legenerálja a hozzá tartozó glue kódot.
Írjunk egy egyszerű lib.rs-t:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Szia, {name}! Ezt a szöveget Rust generálta WebAssembly-ben.")
}
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
let (mut a, mut b) = (0u64, 1u64);
for _ in 0..n {
let next = a + b;
a = b;
b = next;
}
a
}
A #[wasm_bindgen] makró gondoskodik arról, hogy a String visszatérési érték automatikusan JS string-gé konvertálódjon, illetve hogy a &str paraméter is megfelelően átjöjjön a JS oldalról. Nem kell kézzel foglalkoznunk a lineáris memóriával vagy pointerekkel – ezt mind a generált binding kód intézi.
Ha exportálni akarunk egy egész struct-ot metódusokkal együtt, az is megy:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct Counter {
value: i32,
}
#[wasm_bindgen]
impl Counter {
#[wasm_bindgen(constructor)]
pub fn new() -> Counter {
Counter { value: 0 }
}
pub fn increment(&mut self) -> i32 {
self.value += 1;
self.value
}
pub fn get(&self) -> i32 {
self.value
}
}
JavaScript oldalon ez a struct egy sima osztályként fog megjelenni, new Counter()-rel instanciálható, a metódusok pedig szinte natívan hívhatók.
A wasm-bindgen nem tud tetszőleges Rust típust átvinni a WebAssembly-JS határon. Alapvetően primitíveket, String-et, &str-t, egyszerű struct-okat és Vec<u8>-ot (byte tömbként) kezel jól. Bonyolultabb generikus típusoknál (pl. HashMap<K, V>) érdemes szerializálni JSON-ná, vagy a serde-wasm-bindgen crate-et bevonni.
Egy egyszerű böngészős demó elkészítése
Fordítsuk le a projektet wasm-pack-kal:
wasm-pack build --target web
A --target web opció azt jelenti, hogy a generált JS modult natívan, ES modulként tudjuk betölteni <script type="module">-ból, bundler nélkül is. A build eredménye a pkg/ mappában landol: ott találjuk a .wasm fájlt, a JS bindingot és a .d.ts típusdefiníciókat is.
Egy minimál index.html így nézhet ki:
<!DOCTYPE html>
<html lang="hu">
<head>
<meta charset="UTF-8">
<title>Rust + Wasm demo</title>
</head>
<body>
<h1 id="output">Betöltés...</h1>
<script type="module">
import init, { greet, fibonacci, Counter } from "./pkg/rust_wasm_demo.js";
async function run() {
await init();
document.getElementById("output").textContent = greet("Rust fejlesztő");
console.log("10. Fibonacci szám:", fibonacci(10));
const counter = new Counter();
console.log(counter.increment());
console.log(counter.increment());
console.log("Végső érték:", counter.get());
}
run();
</script>
</body>
</html>
Ahhoz, hogy a böngésző betöltse a .wasm fájlt fetch-csel, egy helyi HTTP szervert kell futtatnunk (a file:// protokoll ugyanis blokkolja a Wasm betöltést CORS okokból). Erre kiváló egy egyszerű Python szerver, vagy a npx serve npm eszköz:
npx serve .
Ezután a böngészőben megnyitva a megadott URL-t, láthatjuk, hogy a <h1> szöveg tartalma a Rust greet függvény kimenete lesz.
Build és bundlerezés (webpack) alapjai
Nagyobb projekteknél gyakran nem elég a natív ES modul betöltés – szükség lehet npm dependency-kre, CSS-re, vagy több oldalra is. Ilyenkor jön képbe a webpack (vagy más bundler).
Ezúttal a --target bundler opcióval fordítsunk:
wasm-pack build --target bundler
Ez a mód olyan JS/Wasm interop kódot generál, amit a webpack (vagy Rollup, Vite) natívan tud kezelni import statementekkel, aszinkron betöltéssel együtt.
Hozzunk létre egy minimál package.json-t és webpack.config.js-t:
{
"name": "rust-wasm-demo-app",
"version": "1.0.0",
"scripts": {
"build": "webpack --config webpack.config.js"
},
"dependencies": {
"rust-wasm-demo": "file:./pkg"
},
"devDependencies": {
"webpack": "^5.0.0",
"webpack-cli": "^5.0.0"
}
}
const path = require("path");
module.exports = {
entry: "./index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
},
mode: "development",
experiments: {
asyncWebAssembly: true,
},
};
Az experiments.asyncWebAssembly: true beállítás fontos, mert a webpack 5 alapból nem engedi be a Wasm modulokat aszinkron import()-tal, ezt explicit engedélyezni kell.
Ha a wasm-pack build után cargo verziófüggő hibát kapsz a wasm-bindgen crate és a CLI eszköz verziója között, mindig ellenőrizd, hogy a Cargo.toml-ban szereplő wasm-bindgen verzió megegyezik (vagy kompatibilis) a globálisan telepített wasm-bindgen-cli verziójával. A wasm-pack maga kezeli ezt legtöbbször automatikusan, de manuális wasm-bindgen-cli használat esetén ez gyakori hibaforrás.
A npm run build lefuttatása után a dist/bundle.js már egy önálló, böngészőbe illeszthető fájl lesz, amiben a Rust logika és a JS glue kód egyben utazik.
Összefoglalás
A Rust és WebAssembly páros ma már nem kísérleti terep, hanem egy éles projektekben is bevethető, kiforrott munkafolyamat. A wasm-pack és wasm-bindgen kombináció leveszi a terhet a kezünkből a JS-Wasm interop terén, így a fókuszunk a valódi logikán marad – legyen szó egy kis segédfüggvényről, vagy egy komplett számításigényes modulról. Ha most kezdtél bele, a legjobb tanács, hogy indulj ki egy egyszerű greet-szerű függvényből, próbáld ki mindkét build targetet (web és bundler), és onnan fokozatosan bővítsd a projektet komplexebb típusokkal, esetleg serde-wasm-bindgen-nel a strukturáltabb adatátvitelhez.