Lewati ke konten

Rust dan WebAssembly

WebAssembly (sering disingkat Wasm) memungkinkan kamu menjalankan kode yang ditulis dalam bahasa seperti Rust di dalam browser, berdampingan dengan JavaScript. Bayangkan Wasm sebagai mesin kedua di browser: JavaScript tetap mengendalikan halaman, tapi kalau ada pekerjaan berat (filter gambar, simulasi fisika, membaca file besar) kamu bisa menyerahkannya ke kode Rust yang sudah di-compile dan cepat.

Di halaman ini kamu akan menulis library Rust kecil, meng-compile-nya ke Wasm, lalu memanggilnya dari halaman web. Tidak ada yang bisa dijalankan di Rust Playground di sini (butuh browser dan tool tambahan), jadi sebagian besar contoh berupa blok kode biasa untuk dijalankan di komputermu sendiri. Tapi bagian yang murni Rust bisa langsung kamu coba.

Apa itu WebAssembly, dan kenapa Rust cocok

Section titled “Apa itu WebAssembly, dan kenapa Rust cocok”

WebAssembly adalah format biner yang ringkas untuk program. Semua browser utama bisa menjalankannya, dengan kecepatan mendekati kode native, di dalam sandbox keamanan yang sama dengan JavaScript. Wasm juga tidak hanya untuk browser: server dan platform edge pun menjalankan Wasm.

Rust adalah salah satu bahasa paling populer untuk Wasm, karena beberapa alasan:

  • Tanpa garbage collector atau runtime besar. Rust tidak perlu membawa mesin tambahan bersama kodemu, jadi file .wasm bisa tetap kecil.
  • Tool yang bagus. wasm-bindgen menuliskan kode perekat (glue code) antara Rust dan JavaScript untukmu, dan wasm-pack membangun semuanya dengan satu perintah.
  • Aman dan cepat. Kamu mendapat keamanan memori dan performa yang sama seperti di desktop.

Pertama, beri tahu Rust bahwa ia boleh meng-compile untuk target Wasm. Target artinya “jenis mesin tempat kode akan berjalan”; wasm32-unknown-unknown berarti “Wasm 32-bit, tanpa sistem operasi tertentu”:

Terminal window
rustup target add wasm32-unknown-unknown

Berikutnya, instal wasm-pack. Tool ini meng-compile crate kamu, menjalankan wasm-bindgen, dan mengumpulkan semua yang kamu butuhkan ke dalam satu folder. Kamu bisa memakai installer resminya, atau membangunnya dengan Cargo:

Terminal window
# Installer Linux / macOS dari situs wasm-pack
curl https://wasm-bindgen.github.io/wasm-pack/installer/init.sh -sSf | sh
# atau, di sistem apa pun yang sudah terpasang Rust
cargo install wasm-pack --locked

Modul Wasm adalah sebuah library, bukan program dengan main. Buat satu:

Terminal window
cargo new --lib hello-wasm
cd hello-wasm

Lalu edit Cargo.toml. Baris crate-type memberi tahu Cargo untuk menghasilkan library yang cocok untuk .wasm (cdylib), dan wasm-bindgen adalah crate (library Rust) yang menghubungkan Rust dengan JavaScript:

[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"

Sekarang tulis dua fungsi di src/lib.rs. Atribut #[wasm_bindgen] menandai masing-masing sebagai “bisa dipanggil dari JavaScript”:

use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Halo, {name}! String ini dibuat di Rust.")
}

Hanya itu kode Rust yang kamu perlukan. Perhatikan bahwa keduanya fungsi biasa: wasm-bindgen yang mengurus perubahan string JavaScript menjadi &str, dan String yang dikembalikan menjadi string JavaScript lagi.

Ini kebiasaan yang sangat menghemat waktu: taruh pekerjaan utamanya di fungsi Rust biasa, lalu uji di komputermu dengan cargo test atau main sederhana. Debugging di browser lebih lambat daripada di terminal. Logika yang sama, tanpa atribut, bisa berjalan di mana saja:

fn add(a: i32, b: i32) -> i32 {
a + b
}
fn greet(name: &str) -> String {
format!("Halo, {name}! String ini dibuat di Rust.")
}
fn main() {
println!("{}", greet("Ferris"));
println!("2 + 3 = {}", add(2, 3));
}

Output:

Halo, Ferris! String ini dibuat di Rust.
2 + 3 = 5
▶ Coba sendiri

Setelah logikanya benar, pembungkus #[wasm_bindgen] hanyalah pintu tipis menuju browser. (Test yang ditandai #[test] juga berjalan secara native dengan cargo test; lihat Testing.)

Dari folder crate, jalankan:

Terminal window
wasm-pack build --target web

--target web artinya “buat file yang bisa dimuat langsung oleh browser lewat tag <script type="module">”, tanpa perlu bundler seperti webpack. Saat kami menjalankannya, folder pkg/ yang baru berisi:

pkg/
├── hello_wasm_bg.wasm kode Rust yang sudah di-compile
├── hello_wasm.js glue code JavaScript yang memuatnya dan membungkus tiap fungsi
├── hello_wasm.d.ts tipe TypeScript untuk fungsi-fungsimu
├── hello_wasm_bg.wasm.d.ts
└── package.json

Tanda hubung di hello-wasm berubah menjadi garis bawah di nama file. Secara default wasm-pack build membuat build release (yang dioptimalkan); tambahkan --dev untuk build yang lebih cepat selesai tapi tidak dioptimalkan selama kamu bereksperimen.

Buat index.html di samping Cargo.toml (supaya pkg/ berada di sebelahnya):

<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Halo dari Rust</title>
</head>
<body>
<p id="message">Memuat…</p>
<script type="module">
import init, { add, greet } from "./pkg/hello_wasm.js";
await init(); // unduh dan jalankan file .wasm
const text = greet("Ferris");
const sum = add(2, 3);
document.getElementById("message").textContent = `${text} 2 + 3 = ${sum}`;
</script>
</body>
</html>

Export default-nya, init(), mengunduh file .wasm lalu menjalankannya. Setelah itu, add dan greet menjadi fungsi JavaScript biasa.

Browser tidak mau memuat modul dari alamat file://, jadi kamu butuh web server lokal kecil. Kalau Python sudah terpasang, ini cara tercepat:

Terminal window
python3 -m http.server 8080

Buka http://localhost:8080 di browser. Saat kami mencobanya, halaman menampilkan:

Halo, Ferris! String ini dibuat di Rust. 2 + 3 = 5

Kalimat itu dibuat oleh Rust, yang berjalan di dalam browser. 🦀

Apa yang murah melewati batas (dan apa yang tidak)

Section titled “Apa yang murah melewati batas (dan apa yang tidak)”

Rust dan JavaScript tidak berbagi memori dengan cara yang sama. Modul Wasm punya blok memorinya sendiri, dan nilai harus berpindah antara kedua sisi. Sebagian perpindahan gratis; sebagian lagi butuh salinan:

Tipe di Rust Di JavaScript Biaya
i32, u32, f64, bool number, boolean Murah: dikirim langsung
&str, String string Disalin, ditambah konversi UTF-16 ke UTF-8 (atau sebaliknya)
&[u8], Vec<u8> Uint8Array Disalin ke (atau dari) memori Wasm
&mut [u8] Uint8Array Disalin masuk, lalu disalin balik setelah pemanggilan

Jadi aturan emasnya: buat sedikit pemanggilan, tapi masing-masing mengerjakan banyak hal. Memanggil Rust sekali untuk memproses satu gambar penuh itu cepat. Memanggil Rust sejuta kali, sekali per piksel, akan menghabiskan sebagian besar waktunya untuk menyeberangi batas itu.

Seperti ini contohnya. Fungsi berikut mencerahkan seluruh buffer nilai piksel dalam satu pemanggilan:

use wasm_bindgen::prelude::*;
/// Buat setiap piksel lebih terang. JS mengirim Uint8Array.
#[wasm_bindgen]
pub fn brighten(pixels: &mut [u8], amount: u8) {
for p in pixels.iter_mut() {
*p = p.saturating_add(amount);
}
}

Dari JavaScript, setelah await init(), kamu memanggil brighten(pixels, 20) dengan sebuah Uint8Array, dan array itu diperbarui langsung di tempat. Perulangannya sendiri murni Rust, jadi kamu bisa mengujinya secara native:

fn brighten(pixels: &mut [u8], amount: u8) {
for p in pixels.iter_mut() {
// saturating_add berhenti di 255, bukan overflow
*p = p.saturating_add(amount);
}
}
fn main() {
let mut pixels = [10, 100, 250];
brighten(&mut pixels, 20);
println!("{pixels:?}");
}

Output:

[30, 120, 255]
▶ Coba sendiri

Perhatikan bahwa 250 + 20 menjadi 255, bukan crash atau kembali ke nol: saturating_add berhenti di nilai maksimum. Itu persis yang kamu mau untuk warna.

Setiap byte dari file .wasm harus diunduh dulu sebelum halaman bisa memakainya. Beberapa tips:

  • Selalu kirim build release. Untuk contoh kecil kita (versi dengan teks bahasa Inggris), wasm-pack build --dev menghasilkan file .wasm sebesar 58.591 byte; build release default hanya 21.530 byte. (Itu hasil di mesin kami dengan wasm-opt dimatikan; angkamu bisa berbeda.)
  • Biarkan wasm-pack menjalankan wasm-opt. Dalam mode release, wasm-pack secara default menjalankan optimizer wasm-opt dari Binaryen pada hasilnya, yang biasanya membuatnya lebih kecil lagi.
  • Aktifkan kompresi di server. File kami yang 21.530 byte menjadi 8.464 byte setelah gzip.
  • Coba pengaturan yang fokus pada ukuran, lalu ukur:
[profile.release]
opt-level = "z" # optimalkan ukuran ("s" versi yang lebih ringan)
lto = true
codegen-units = 1

Jujurlah pada dirimu sendiri dan ukur hasilnya. Untuk contoh dua fungsi kita, pengaturan ini tidak membantu: filenya jadi 22.817 byte, sedikit lebih besar. Pengaturan ini biasanya baru terasa manfaatnya di crate yang lebih besar. Hal seperti format! dan pesan panic juga ikut menarik kode, jadi crate yang menghindarinya bisa jauh lebih kecil.

Beberapa contoh, masing-masing dengan tulisan dari perusahaannya sendiri (bahasa Inggris):

  • Figma meng-compile editor desainnya yang ditulis dalam C++ ke WebAssembly dan melaporkan waktu muatnya turun 3x. (Bukan Rust, tapi ini menunjukkan apa yang bisa dilakukan Wasm di aplikasi besar.)
  • Cloudflare Workers mendukung Rust lewat crate workers-rs: kode Rust kamu di-compile ke Wasm dan berjalan di server Cloudflare di seluruh dunia.
  • Shopify Functions adalah modul WebAssembly untuk mengustomisasi toko, dan Shopify merekomendasikan Rust sebagai bahasa dengan performa terbaik untuk itu.
  • Ekstensi browser 1Password memindahkan fitur pengisian halamannya ke WebAssembly dan melaporkan kecepatannya minimal dua kali lipat.

Lupa crate-type = ["cdylib"]. Tanpa baris ini, wasm-pack langsung berhenti. Untungnya, pesan error-nya memberi tahu persis apa yang harus ditambahkan:

Error: crate-type must be cdylib to compile to WebAssembly. Add the following to your Cargo.toml file:
[lib]
crate-type = ["cdylib", "rlib"]

Membuka index.html dengan klik dua kali. Halaman tetap menampilkan “Memuat…”, dan console browser (di Chrome) menulis:

Access to script at 'file:///…/pkg/hello_wasm.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: chrome, chrome-extension, chrome-untrusted, data, http, https, isolated-app.

Artinya, browser memblokir script yang dimuat dari file://. Solusinya: sajikan folder itu dengan web server lokal, misalnya python3 -m http.server, lalu buka alamat http://localhost.

Memanggil fungsi sebelum await init(). Modul Wasm belum dimuat, jadi glue code tidak punya apa pun untuk dipanggil. Saat kami menghapus baris init(), Chrome menampilkan Cannot read properties of undefined (reading '__wbindgen_free'), yaitu ada objek yang masih kosong (undefined). Selalu await init() dulu.

Langkah berikutnya: aplikasi web penuh dengan Rust

Section titled “Langkah berikutnya: aplikasi web penuh dengan Rust”

Memanggil beberapa fungsi Rust dari JavaScript adalah awal yang bagus. Kalau kamu ingin membangun seluruh aplikasi web dengan Rust, lihat framework berikut. Keduanya di-compile ke Wasm dan memungkinkan kamu menulis komponen dalam Rust, mirip React:

  • Leptos: framework modern yang cepat dan juga bisa me-render halaman di server.
  • Yew: framework berbasis komponen yang sudah matang.
  • WebAssembly adalah format biner yang cepat dan aman yang bisa dijalankan browser (dan server); Rust cocok di-compile ke sana karena tidak punya garbage collector.
  • Tambahkan target dengan rustup target add wasm32-unknown-unknown, lalu instal wasm-pack.
  • Tandai fungsi dengan #[wasm_bindgen], atur crate-type = ["cdylib", "rlib"], dan build dengan wasm-pack build --target web.
  • Di JavaScript, await init() dulu, baru panggil fungsimu; sajikan halaman lewat http://, bukan file://.
  • Angka murah melewati batas; string dan slice disalin. Jadi buat sedikit pemanggilan yang masing-masing mengerjakan banyak hal.
  • Kirim build release dengan kompresi, dan ukur dulu sebelum memercayai trik pengecilan ukuran.

Kuis singkat

1. Perintah mana yang menambahkan target Rust untuk WebAssembly di browser?

2. Apa fungsi atribut #[wasm_bindgen]?

3. Halamanmu tetap menampilkan "Memuat…" dan console menunjukkan error CORS untuk alamat file:///. Apa solusinya?

4. Mana yang paling murah dikirim dari JavaScript ke Rust?

Skor: 0 / 4