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
});
(import "halaman" "tampilkan" ...)artinya: modul ini butuh fungsitampilkandi objekhalaman. Objek impor di JS harus punya bentuk yang sama:{ halaman: { tampilkan } }.(export "hargaInap" ...)membuat fungsinya muncul diinstance.exports.memoryjuga bisa di-export.- Urutan
pesan(2, 350000): JS memanggil Wasm, Wasm memanggilhargaInapdi dalam dirinya sendiri (tanpa menyeberang), lalu Wasm memanggil JS lewattampilkan(700000), dan baru setelah itupesankembali ke JS. Panggilan balik ke JS juga menyeberang perbatasan. - Impornya dicek saat instantiate. Tanpa objek impor sama sekali, Chromium 147 menolak dengan
TypeError: WebAssembly.instantiate(): Imports argument must be present and must be an object. Kalautampilkangak ada, hasilnyaLinkError: WebAssembly.instantiate(): Import #0 "halaman" "tampilkan": function import requires a callable.
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 |
- Saat masuk ke
i32, JS menjalankan konversi ToInt32: angka diubah ke Number, pecahannya dibuang, lalu dipotong ke 32 bit.2.9jadi2, string"350000"jadi350000, dan string atau objek yang bukan angka jadi0tanpa error. i64menyeberang sebagaiBigIntsejak Chrome 85, Firefox 78, Safari 14.1, dan Node 15. Kirim Number biasa dan Chromium menolak:TypeError: Cannot convert 7000 to a BigInt. Sebaliknya, BigInt kei32juga ditolak:TypeError: Cannot convert a BigInt value to a number.externrefdatang dari reference types (Chrome 96, Firefox 79, Safari 15, Node 17.2). Wasm boleh menyimpan dan mengembalikan objek JS, tapi gak bisa membacatamu.nama. Untuk itu Wasm harus memanggil fungsi impor JS.- String, array, dan isi objek gak bisa lewat langsung. Semuanya harus jadi byte di linear memory.
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 |
TextEncodermengubah string jadi byte UTF-8. Huruf biasa 1 byte, tapié2 byte (c3 a9). Jadi"Renée"butuh 6 byte, padahalnama.lengthbilang 5. Itu jawaban tebakannya. Panjang yang dikirim harusbyteNama.length.besarkancuma melihat byte.c3dana9bukanasampaiz, jadiétetap. HasilnyaRENéE.TextDecodermengubah byte balik jadi string. Wasm yang mengembalikan teks juga memberi pointer dan panjang, dan JS yang men-decode.new TextEncoder().encodeInto(nama, view)menulis langsung ke memori Wasm tanpaUint8Arrayperantara, lalu mengembalikan{ read, written }.- Di sini alamatnya 0 supaya contohnya pendek. Modul sungguhan meminta alamat dari allocator di dalam Wasm; itu bahasan pelajaran berikutnya.
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
- Number ke parameter
i64.hargaInap64(7000, 350000)ditolak:TypeError: Cannot convert 7000 to a BigInt. Tulis7000n, atauBigInt(malam). - String atau objek langsung ke parameter
i32.besarkan('Renée', 5)gak error: Wasm menerimaptr0 dan memproses byte apa pun yang kebetulan ada di alamat 0.hargaInap(tamu, 350000)mengembalikan0. - Memakai
nama.lengthsebagai panjang byte.lengthmenghitung unit UTF-16, bukan byte UTF-8. Pakai panjang hasilencode. - Nama impor gak cocok.
{ halaman: {} }atau salah ketiktampilanberakhir denganLinkErrorsaat instantiate.
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.