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
hargaInapsaja, di-build dengan wasm-bindgen, jadi 13.590 byte. Lalu ditambah tiga fungsi:sapa(string masuk dan keluar),catat(console.log), dangambarHarga(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
&strdanStringdi Rust jadistringdi JS. Penyalinan byte UTF-8 dari pelajaran jembatan sekarang dikerjakan glue-nya.console::log_1adalah fungsi web-sys untukconsole.logdengan satu argumen (log_2untuk dua, dan seterusnya).JsValueadalah nilai JS apa saja yang dipegang Rust.&CanvasRenderingContext2dmenerimactxdari JS. Method-nya ditulis gaya Rust:fillRectjadifill_rect, propertifillStylediisi denganset_fill_style_str.- Method yang bisa gagal di JS (seperti
fillText) mengembalikanResult<(), JsValue>.unwrap()menghentikan program kalau gagal; di kode sungguhan, kembalikan error-nya ke JS dengan?. - Canvas juga bisa diambil dari Rust lewat
web_sys::window()dandocument(), asal featureWindow,Document, danHtmlCanvasElementikut dinyalakan.
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);
},
- String masuk:
passStringToWasm0memesan tempat dengan__wbindgen_malloc, menulis byte UTF-8, lalu mengirim pointer dan panjang. Untuk"Renée", glue memesan 5 byte (panjang string), ketemué, memperbesar jadi 9, lalu memotong ke 6. - String keluar:
wasm.sapamengembalikan dua angka sekaligus (pointer dan panjang). Glue men-decode byte-nya jadi string JS, lalu__wbindgen_freemelepas tempatnya difinally. - Objek JS:
ctxdikirim apa adanya sebagaiexternref, tanpa disalin. Saat Rust memanggilfill_rect, Wasm memanggil impor__wbg_fillRect_..., dan impor itu yang menjalankanarg0.fillRect(...). SatugambarHarga(ctx, 2)= 16 panggilan dari Wasm ke JS: 14 ke canvas, dan 2 untuk console.log (mengubah teks jadi string JS, lalulog). - Akhiran seperti
_0ef59adb9acb7d06dibuat wasm-bindgen. Glue dan.wasmharus dari build yang sama; jangan campur file dari dua build.
js-sys dan web-sys
- js-sys: binding ke objek bawaan JavaScript yang ada di semua lingkungan JS:
Array,Object,Date,Math,JSON,Promise,Uint8Array. Gak butuh feature. Contoh:js_sys::Date::now(). - web-sys: binding ke Web API yang disediakan browser:
Window,Document,HtmlCanvasElement,CanvasRenderingContext2d,console,Request,Response. Tiap tipe adalah satu feature Cargo, dan cuma yang dinyalakan yang di-compile. Lupa feature-nya dan rustc menolak:
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(())
}
Closure::newmembungkus closure Rust jadi fungsi JS.as_ref().unchecked_ref()mengubahnya ke tipeFunctionyang dimintaadd_event_listener_with_callback.- Kalau
klikdi-drop saat fungsi selesai, listener-nya tetap terpasang, dan klik berikutnya gagal:Error: closure invoked recursively or after being dropped. forget()membuat closure hidup selamanya. Untuk listener yang dipasang sekali, itu wajar. Untuk listener yang dipasang dan dilepas berkali-kali, simpanClosure-nya, lalu drop setelahremove_event_listener, supaya memorinya gak bocor.
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
- Lupa feature web-sys.
error[E0432]: unresolved importpadahal crate-nya sudah ada. Tambahkan nama tipenya kefeatures. - Closure yang di-drop. Listener tetap terpasang tapi isinya sudah dilepas:
Error: closure invoked recursively or after being dropped. Pakaiforget()atau simpanClosure-nya. - Memanggil fungsi sebelum
await init(). Glue belum punya modul Wasm. - Mengira web-sys gratis. Satu tipe API bisa menambah beberapa KB. Nyalakan feature yang benar-benar dipakai saja.
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.