Menjelaskan interop JavaScript-WebAssembly secara penuh: API WebAssembly.instantiate/compile, Instance.exports, passing primitives, dan import host functions, plus peran glue code Emscripten dan wasm-bindgen untuk bahasa tingkat tinggi

Setelah di episode 5 kita memahami memory dan tables sebagai jembatan data dan kode — pada episode ini kita menyusun semuanya menjadi interop yang lengkap antara JavaScript dan WebAssembly. Inilah momen ketika Wasm berhenti menjadi objek abstrak dan mulai bekerja di dalam aplikasi nyata.
Mengapa interop penting? Karena dalam praktik, hampir tidak ada modul Wasm yang berdiri sendiri. Ia selalu dipanggil dari JavaScript, menerima data dari user, dan mengembalikan hasil untuk dirender di layar. Menguasai interop berarti menguasai antar muka — dan di situlah 80% bug integrasi biasanya muncul.
JavaScript menyediakan empat fungsi utama:
| Fungsi | Deskripsi |
|---|---|
WebAssembly.compile | Kompilasi bytecode menjadi WebAssembly.Module (tanpa instance) |
WebAssembly.instantiate | Kompilasi + instansiasi sekaligus |
WebAssembly.compileStreaming | Versi streaming dari compile untuk fetch |
WebAssembly.instantiateStreaming | Versi streaming dari instantiate — paling efisien |
const { instance } = await WebAssembly.instantiateStreaming(
fetch("modul.wasm"),
{ env: { /* imports */ } },
)Fungsi streaming mengkompilasi bytecode sambil data diunduh — bukan menunggu seluruh file selesai. Untuk modul besar, ini menghemat waktu nyata. WebAssembly.Module yang dihasilkan compile juga bisa di-cache ulang dengan murah, topik yang kembali kita bahas di episode 13.
Setelah instansiasi, semua yang diexport modul bisa diakses lewat instance.exports:
const { instance } = await WebAssembly.instantiateStreaming(
fetch("math.wasm"),
)
console.log(instance.exports.tambah(2, 3)) // 5
console.log(instance.exports.kali(4, 5)) // 20
const d = instance.exports.lingkaran(2.5) // f64
console.log(d)Aturan passing primitives sederhana namun ketat:
i32/i64 JS ↔ integer Wasm; f32/f64 JS ↔ float Wasm.(param)/(result) — JS memakai konversi longgar, tapi hasil ekstrem bisa mengejutkan.i64 tidak didukung penuh oleh JS (presisi BigInt); BigInt diperlukan mulai sekarang di semua engine modern.const hasil = instance.exports.fungsiI64(10n, 20n)
console.log(typeof hasil, hasil) // bigint 30Arah sebaliknya: modul Wasm bisa memanggil fungsi JavaScript yang disediakan sebagai imports. Pola ini dipakai modul untuk logging, DOM, jaringan, atau apa pun yang tidak bisa ia lakukan sendiri:
(module
(import "env" "logAngka" (func $log (param i32)))
(func $proses (param $x i32) (result i32)
local.get $x
call $log
local.get $x
i32.const 2
i32.mul
)
(export "proses" (func $proses))
)const { instance } = await WebAssembly.instantiateStreaming(
fetch("impor-host.wasm"),
{
env: {
logAngka: (n) => console.log("dari wasm:", n),
},
},
)
instance.exports.proses(21) // log "21", return 42Pola ini adalah dasar dari WASI (host menyediakan filesystem, clock, jaringan) yang kita bahas di episode 10.
String dan objek tidak bisa langsung dilewatkan — kalian harus melewati linear memory. Alurnya:
Uint8Array.const memory = instance.exports.memory
const encoder = new TextEncoder()
const bytes = encoder.encode("halo dari js")
const buf = new Uint8Array(memory.buffer)
buf.set(bytes, 0) // tulis ke offset 0
instance.exports.sapaan(0, bytes.length) // address + panjang
const out = new TextDecoder().decode(
new Uint8Array(memory.buffer, 0, 16),
)
console.log(out)Manual, rawan error, dan membosankan — inilah alasan lahirnya glue code di bagian berikutnya.
Menulis interop manual seperti di atas untuk proyek nyata tidak akan bertahan. Di sinilah peran bindings:
Emscripten menghasilkan .wasm plus file JavaScript glue yang mengurus memory management, loading, dan marshalling secara otomatis. Kalian menulis C/C++ biasa; JS glue-nya mengurus sisanya. Detail lengkap di episode 8.
wasm-bindgen menganalisis kode Rust yang diberi anotasi dan menghasilkan:
import * as wasm from "./pkg").Vec<u8> sebagai Uint8Array, dan objek JS.use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet(nama: &str) -> String {
format!("Halo, {nama}!")
}Kode Rust di atas otomatis menghasilkan greet yang bisa dipanggil dari JS dengan string biasa — tanpa manual marshalling. Pipeline ini kita bangun langkah demi langkah di episode 7.
Tip
Aturan praktis memilih jalur: Rust + wasm-bindgen untuk proyek baru (toolchain modern, aman, ergonomis), C/C++ + Emscripten untuk porting kode legacy atau workload yang butuh full POSIX emulation.
memory.buffer bisa di-detach setelah memory.grow; ambil ulang view setelah setiap grow.Uint8Array yang sama saat Wasm sedang membacanya menyebabkan data race di sisi JS; gunakan salinan jika perlu.TextEncoder/TextDecoder (UTF-8), jangan mengasumsikan ASCII.LinkError.Pada episode 6 ini, kalian telah menguasai seluruh permukaan interop JS–Wasm: API instansiasi, exports, passing primitives, host functions, dan marshalling string lewat memory.
Inti yang harus dibawa pulang:
instantiateStreaming adalah cara paling efisien memuat modul; compile memisahkan kompilasi dari instansiasi.i32/f32/f64) langsung cocok; i64 memakai BigInt; string butuh memory + encoder.Di episode 7 selanjutnya kita akan membangun pipeline Rust + wasm-pack dari nol — cargo new --lib, menambah target wasm32-unknown-unknown, build dengan wasm-pack, dan memahami struktur pkg/ yang dihasilkan. Sampai jumpa di episode 7!