wasm-bindgen & web-sys — WebAssembly

wasm-bindgen & web-sys Wasm secara default hanya bisa komunikasi dengan angka. wasm-bindgen otomatis generate glue code agar Rust bisa berinteraksi dengan…

wasm-bindgen membuat kode perantara (glue) di kedua sisi supaya fungsi Rust bisa menerima dan mengembalikan string, objek JS, dan closure, bukan cuma angka. Di atasnya ada dua crate: js-sys untuk objek bawaan JavaScript (Array, Date, Math, Promise) dan web-sys untuk Web API (console, document, canvas, fetch). Bayangin tamu dari luar negeri menginap di Homestay Kabut. Dia ditemani pemandu yang menerjemahkan dua arah dan membawakan barangnya. Pemandu itu membawa buku frasa umum untuk percakapan sehari-hari, dan peta tempat wisata yang dibeli per lembar sesuai tujuan. Tamunya kode Rust, homestay-nya browser, pemandunya wasm-bindgen, buku frasanya js-sys, dan lembar-lembar petanya feature web-sys: mau ke canvas, beli lembar canvas.

🧪 Tebakan cepat: crate Rust berisi hargaInap saja, di-build dengan wasm-bindgen, jadi 13.590 byte. Lalu ditambah tiga fungsi: sapa (string masuk dan keluar), catat (console.log), dan gambarHarga (menggambar di canvas). File .wasm-nya jadi kira-kira berapa: 14 KB, 22 KB, atau 200 KB? Tebak dulu, jawabannya ada di bagian "Ongkos ukuran glue".

Proyeknya: kabut-canvas

Toolchain-nya sama dengan pelajaran wasm-pack (rustc 1.99.0, wasm-pack 0.15.0, wasm-bindgen 0.2.129), ditambah web-sys 0.3.106:

[dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["console", "CanvasRenderingContext2d"] }
use wasm_bindgen::prelude::*;
use web_sys::{console, CanvasRenderingContext2d};

/// &str masuk, String keluar.
#[wasm_bindgen]
pub fn sapa(nama: &str) -> String {
    format!("Selamat datang di Homestay Kabut, {nama}!")
}

/// console.log dari Rust.
#[wasm_bindgen]
pub fn catat(pesan: &str) {
    console::log_1(&JsValue::from_str(pesan));
}

/// Total harga tiga kamar untuk sekian malam, digambar di canvas.
#[wasm_bindgen(js_name = gambarHarga)]
pub fn gambar_harga(ctx: &CanvasRenderingContext2d, malam: i32) {
    let kamar = [("Teras", 350_000), ("Loteng", 275_000), ("Kebun", 425_000)];
    ctx.clear_rect(0.0, 0.0, 300.0, 140.0);
    ctx.set_font("13px sans-serif");
    for (i, (nama, harga)) in kamar.iter().enumerate() {
        let total = malam * harga;
        let y = 12.0 + i as f64 * 42.0;
        ctx.set_fill_style_str("#7aa6d6");
        ctx.fill_rect(0.0, y, total as f64 / 10_000.0, 28.0); // Rp 10.000 = 1 piksel
        ctx.set_fill_style_str("#1d2733");
        ctx.fill_text(&format!("{nama}: Rp {total}"), 6.0, y + 19.0).unwrap();
    }
    console::log_1(&format!("gambarHarga: {malam} malam").into());
}

(hargaInap dari pelajaran wasm-pack juga ada di crate ini.) Di halaman:

import init, { sapa, catat, gambarHarga } from './pkg/kabut_canvas.js';

await init();
console.log(sapa('Renée'));   // Selamat datang di Homestay Kabut, Renée!
catat('Halo dari Rust');      // muncul di console, dikirim dari Rust
const ctx = document.querySelector('canvas').getContext('2d');
gambarHarga(ctx, 2);          // tiga batang harga di canvas

Apa yang dibuat wasm-bindgen

wasm-pack build --target web menulis pkg/kabut_canvas.js (11.776 byte). Ini potongan glue untuk sapa dan gambarHarga:

export function sapa(nama) {
    let deferred2_0;
    let deferred2_1;
    try {
        const ptr0 = passStringToWasm0(nama, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
        const len0 = WASM_VECTOR_LEN;
        const ret = wasm.sapa(ptr0, len0);
        deferred2_0 = ret[0];
        deferred2_1 = ret[1];
        return getStringFromWasm0(ret[0], ret[1]);
    } finally {
        wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
    }
}

export function gambarHarga(ctx, malam) {
    wasm.gambarHarga(ctx, malam);
}

Dan potongan impor yang dipanggil Rust:

__wbg_fillRect_0ef59adb9acb7d06: function(arg0, arg1, arg2, arg3, arg4) {
    arg0.fillRect(arg1, arg2, arg3, arg4);
},
__wbg_log_17c30ef363c61cf4: function(arg0) {
    console.log(arg0);
},

js-sys dan web-sys

error[E0432]: unresolved import `web_sys::CanvasRenderingContext2d`
 --> src/lib.rs:2:24
  |
2 | use web_sys::{console, CanvasRenderingContext2d};
  |                        ^^^^^^^^^^^^^^^^^^^^^^^^ no `CanvasRenderingContext2d` in the root

Dokumentasi web-sys di docs.rs menulis di tiap tipe feature apa yang dibutuhkannya.

📌 Closure: Rust sebagai event listener

Fungsi Rust bisa jadi callback JS lewat Closure. Ini butuh feature HtmlElement dan EventTarget:

#[wasm_bindgen(js_name = pasangTombol)]
pub fn pasang_tombol(tombol: &web_sys::HtmlElement) -> Result<(), JsValue> {
    let klik = Closure::<dyn FnMut()>::new(|| console::log_1(&"Tombol pesan diklik".into()));
    tombol.add_event_listener_with_callback("click", klik.as_ref().unchecked_ref())?;
    klik.forget(); // biarkan closure hidup selama halaman hidup
    Ok(())
}

Ongkos ukuran glue

Diukur dengan wasm-pack build --target web (release), menambah satu fungsi tiap baris:

Isi crate .wasm glue .js
hargaInap saja 13.590 byte 4.122 byte
+ sapa (String masuk dan keluar) 15.497 byte 6.994 byte
+ catat (web-sys console) 19.022 byte 7.701 byte
+ gambarHarga (web-sys canvas) 22.102 byte 11.776 byte
+ pasangTombol (Closure) 23.049 byte 14.091 byte

Jawaban tebakannya: sekitar 22 KB (22.102 byte, 9.894 byte setelah gzip). Dasarnya sudah 13 KB untuk satu perkalian: allocator, tabel externref, dan penanganan panic. format! untuk teks menambah mesin format Rust. web-sys sendiri berisi ribuan API, tapi yang ikut cuma yang dipanggil. Dengan opt-level = "z", lto = true, dan codegen-units = 1 di [profile.release], versi gambarHarga turun ke 20.569 byte. Lab di halaman ini menjalankan build itu.

📌 Siapa yang merawat

Organisasi GitHub rustwasm diarsipkan pada 2025. wasm-bindgen, js-sys, dan web-sys sekarang dirawat di organisasi wasm-bindgen dan tetap rilis; ceritanya ada di pelajaran "Wasm dari Rust (wasm-pack)".

⚠️ Jebakan yang sering ketemu

TL;DR: wasm-bindgen membuat glue di dua sisi: &str/String disalin sebagai byte UTF-8 (passStringToWasm0, getStringFromWasm0, lalu __wbindgen_free), objek JS seperti ctx menyeberang apa adanya sebagai externref, dan tiap method Web API yang dipanggil Rust jadi impor JS (__wbg_fillRect_...). js-sys untuk objek bawaan JS (Array, Date, Math, Promise) tanpa feature; web-sys untuk Web API (console, document, canvas, fetch), satu feature Cargo per tipe, dan lupa feature berarti error[E0432]. console::log_1 untuk console.log, CanvasRenderingContext2d dengan fill_rect dan set_fill_style_str untuk menggambar. Closure::new membuat callback JS; drop terlalu cepat berarti closure invoked recursively or after being dropped, jadi forget() atau simpan. Ongkosnya: hargaInap saja 13.590 byte, dengan string, console, dan canvas 22.102 byte (20.569 dengan opt-level = "z" dan LTO). wasm-bindgen dirawat organisasi wasm-bindgen sejak 2025.

Yang akan kamu pelajari