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 .wasm fá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 a wasm-bindgen pá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, vagy wasm32-unknown-unknown targetet támogató crate-et azonnal felhasználhatsz.
Tipp

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"
Megjegyzés

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.

Jó tudni

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.

Figyelem

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.