JavaScript-Wasm Bridge — WebAssembly

JavaScript ↔ Wasm Bridge WebAssembly dan JavaScript berkomunikasi melalui mekanisme imports dan exports. Memahami bridge ini krusial karena ini adalah…

JavaScript dan Wasm berbicara lewat satu jembatan: export (fungsi Wasm yang dipanggil JS), impor (fungsi JS yang dipanggil Wasm), dan linear memory yang bisa dibaca dan ditulis keduanya. Bayangin loket resepsionis Homestay Kabut yang dipisah kaca dengan celah kecil. Lewat celah itu cuma bisa diselipkan kertas berisi angka: jumlah malam, harga per malam. Koper tamu gak muat, jadi ditaruh di gudang bersama, dan yang diselipkan cuma nomor rak dan berapa banyak barangnya. Kunci kamar tamu boleh dititipkan, tapi resepsionis gak boleh membuka isinya. Celahnya adalah perbatasan JS dan Wasm, kertas angkanya i32/i64/f32/f64, gudangnya linear memory, nomor rak dan jumlahnya pointer dan panjang, dan kunci yang dititipkan itu externref.

🧪 Tebakan cepat: nama tamu "Renée" punya 5 huruf. Untuk dikirim ke Wasm, berapa byte yang harus ditulis JS ke memori: 5, 6, atau 10? Tebak dulu, jawabannya ada di bagian "String: tulis byte-nya, kirim pointer dan panjang".

Export dan impor

Semua contoh di pelajaran ini memakai satu modul, jembatan.wat (272 byte setelah wat2wasm 1.0.42). Ini bagian yang dipakai di sini:

(module
  ;; impor: fungsi JS yang boleh dipanggil Wasm
  (import "halaman" "tampilkan" (func $tampilkan (param i32)))
  (memory (export "memory") 1)

  (func $hargaInap (export "hargaInap") (param $malam i32) (param $hargaPerMalam i32) (result i32)
    local.get $malam
    local.get $hargaPerMalam
    i32.mul)

  ;; Wasm menghitung, lalu memanggil balik JS
  (func (export "pesan") (param $malam i32) (param $hargaPerMalam i32)
    local.get $malam
    local.get $hargaPerMalam
    call $hargaInap
    call $tampilkan))
const imports = {
  halaman: { tampilkan: (total) => console.log('Total: Rp ' + total) },
};
WebAssembly.instantiate(bytes, imports).then(({ instance }) => {
  const { hargaInap, pesan } = instance.exports;
  console.log(hargaInap(2, 350000)); // 700000
  pesan(2, 350000);                  // Total: Rp 700000
});

Nilai yang bisa menyeberang

Yang benar-benar lewat celah cuma angka dengan empat tipe Wasm, plus referensi:

Tipe Wasm Di JS Contoh di jembatan.wasm
i32 Number, dipotong ke bilangan bulat 32-bit hargaInap(7000, 350000) jadi -1844967296; hargaInap(2.9, "350000") jadi 700000
i64 BigInt hargaInap64(7000n, 350000n) jadi 2450000000n
f32, f64 Number pecahan, misalnya harga per meter
externref nilai JS apa saja, utuh tapi gak bisa dibaca Wasm simpanTamu(tamu) lalu ambilTamu() === tamu hasilnya true

String: tulis byte-nya, kirim pointer dan panjang

jembatan.wat punya besarkan(ptr, len): mengubah huruf a sampai z jadi huruf besar, byte demi byte, di memori. JS yang menulis dan membaca teksnya:

const { memory, besarkan } = instance.exports;
const nama = 'Renée';
const byteNama = new TextEncoder().encode(nama); // 6 byte: 52 65 6e c3 a9 65 (hex)
const ptr = 0;                                   // alamat di memori Wasm
new Uint8Array(memory.buffer).set(byteNama, ptr);
besarkan(ptr, byteNama.length);                  // yang menyeberang: dua i32, 0 dan 6
const hasil = new Uint8Array(memory.buffer, ptr, byteNama.length);
console.log(new TextDecoder().decode(hasil));    // RENéE
R e n é (byte 1) é (byte 2) e
Sebelum 52 65 6e c3 a9 65
Sesudah besarkan(0, 6) 52 45 4e c3 a9 45

Ongkos menyeberang

Tiap penyeberangan mengubah argumen ke tipe Wasm lalu hasilnya balik. Sekali cuma beberapa nanodetik, tapi angkanya menumpuk (diukur di pelajaran sebelumnya, Chromium 147):

Cara memanggil Waktu
grayscale(ptr, 3840000), sekali untuk foto 1200 × 800 0,7 ms + 0,6 ms salin masuk dan keluar
JavaScript biasa 1,8 ms
abuPiksel(r, g, b), 960.000 kali 8 ms
fungsi Go lewat js.FuncOf (Node 22) sekitar 7,8 µs per panggilan

Aturannya: kumpulkan datanya, tulis sekali ke memori, panggil sekali, dan biarkan loop-nya jalan di dalam Wasm. Hal yang sama berlaku ke arah sebaliknya: impor JS yang dipanggil per piksel sama mahalnya.

📌 JS String Builtins

Bahasa yang memakai linear memory (C, C++, Rust, Go) tetap memakai cara pointer dan panjang. Tapi sekarang ada jalan lain: JS String Builtins (Chrome 130, Firefox 134, Safari 26.2). Modul bisa memegang string JS sebagai externref dan memanggil operasi string (panjang, potong, gabung) lewat impor khusus wasm:js-string, tanpa menyalin byte. Ini dipakai compiler yang menargetkan Wasm GC, bukan kode yang kamu tulis dengan tangan.

⚠️ Jebakan yang sering ketemu

TL;DR: JS memanggil export Wasm (instance.exports.hargaInap), Wasm memanggil impor JS ({ halaman: { tampilkan } }, dicek saat instantiate: LinkError kalau gak ada), dan keduanya berbagi linear memory. Yang bisa menyeberang langsung cuma angka: i32 (ToInt32: 2.9 jadi 2, string bukan angka jadi 0 diam-diam), i64 sebagai BigInt (Chrome 85, Firefox 78, Safari 14.1; Number ditolak dengan TypeError), f32/f64, plus externref untuk memegang objek JS tanpa membacanya. String dikirim sebagai byte: TextEncoder ke memori, lalu pointer dan panjang ("Renée" 5 huruf tapi 6 byte), dan TextDecoder untuk membaca balik. Tiap penyeberangan ada ongkosnya (960.000 panggilan abuPiksel 8 ms, lawan 1,8 ms JavaScript), jadi kirim data sekaligus dan panggil sekali.

Yang akan kamu pelajari