Membedah linear memory secara mendalam: memory.grow dan memory.size, akses dari JavaScript lewat Uint8Array/DataView, segment data, serta tables dan call_indirect untuk function pointers dan virtual dispatch

Setelah di episode 4 kalian menguasai sintaks WAT — fungsi, instruksi stack, import, export, dan memory dasar — pada episode ini kita masuk ke dua komponen yang paling sering disalahpahami: memory dan tables. Keduanya adalah jembatan antara modul Wasm dan dunia luar, dan memahami keduanya adalah kunci untuk menulis kode yang tidak hanya berjalan, tetapi juga efisien.
Mengapa topik ini penting? Karena hampir semua interop yang kalian lakukan — mengirim string dari JavaScript, memanggil fungsi secara dinamis, atau membangun plugin systems di episode 19 — pada akhirnya berputar di sekitar memory dan tables. Di sinilah arsitektur Wasm yang abstrak mulai terasa nyata.
Memory Wasm bersifat linear: sebuah blok byte berurutan yang diindeks dari 0. Ukurannya dinamis dan diukur dalam page — 1 page = 64 KB. Ada dua instruksi yang mengatur ukuran:
memory.size — mengembalikan jumlah page saat ini.memory.grow <n> — menambah memory sebanyak n page, mengembalikan page lama (atau -1 jika gagal).(module
(memory 1 4)
(func $saya (result i32)
memory.size
)
(func $grow (param $pages i32) (result i32)
local.get $pages
memory.grow
)
(export "saya" (func $saya))
(export "grow" (func $grow))
)Jika dipanggil berurutan: saya() mengembalikan 1 (satu page awal), lalu grow(2) mengembalikan 1 (ukuran lama) dan membuat ukuran baru 3 page, dan saya() berikutnya mengembalikan 3.
Note
memory.grow hanya bisa menambah, bukan mengurangi. Jika kalian butuh mengecilkan memory, satu-satunya cara adalah membuat instance baru — inilah alasan pola snapshot/rehydrate di episode 18 memakai instansiasi ulang.
Segment data mengisi memory dengan byte tertentu saat modul diinstansiasi — berguna untuk menaruh string atau konstanta binary:
(module
(memory 1)
(data (i32.const 0) "hello wasm")
(func $ambil (param $offset i32) (result i32)
local.get $offset
i32.load8_u
)
(export "memory" (memory 0))
(export "ambil" (func $ambil))
)Instruksi i32.load8_u membaca satu byte unsigned pada address di stack. Dengan (data (i32.const 0) "hello wasm"), byte ke-0 berisi kode ASCII h (104), byte ke-1 e (101), dan seterusnya.
Dari sisi JavaScript, memory Wasm diekspos sebagai WebAssembly.Memory, yang bisa diakses sebagai ArrayBuffer. Untuk membaca/menulis data, kalian memakai views:
const { instance } = await WebAssembly.instantiate(bytes)
const memory = instance.exports.memory
const bytes = new Uint8Array(memory.buffer) // akses byte per byte
const view = new DataView(memory.buffer) // akses multi-byte
console.log(bytes[0], bytes[1], bytes[2]) // 104, 101, 108
console.log(view.getUint32(0, true)) // baca 4 byte little-endianPerbedaan keduanya: Uint8Array untuk akses byte per byte (efisien untuk text/binary stream), sedangkan DataView untuk membaca/menulis integer 16/32/64-bit atau float dengan endianness eksplisit — pasangan alami instruksi i32.store/i64.load di sisi Wasm.
Memory menyimpan data; tables menyimpan referensi — khususnya referensi fungsi. Ini mekanisme Wasm untuk function pointer dan virtual dispatch.
(module
(type $t (func (param i32) (result i32)))
(table 2 funcref)
(elem (i32.const 0) $tambah $kali)
(func $tambah (type $t) (param $a i32) (result i32)
local.get $a
i32.const 1
i32.add
)
(func $kali (type $t) (param $a i32) (result i32)
local.get $a
i32.const 2
i32.mul
)
(func $panggil (param $idx i32) (param $x i32) (result i32)
local.get $idx
local.get $x
call_indirect (type $t)
)
(export "panggil" (func $panggil))
)(table 2 funcref) — table berukuran 2 yang menyimpan referensi fungsi.(elem (i32.const 0) $tambah $kali) — isi elemen 0 dengan $tambah, elemen 1 dengan $kali.call_indirect (type $t) — panggil fungsi pada index yang ada di stack, dengan verifikasi tipe saat runtime.Sekarang panggil(0, 5) mengembalikan 6 (tambah 1), sedangkan panggil(1, 5) mengembalikan 10 (kali 2). Fungsi mana yang dipanggil ditentukan secara dinamis oleh index — inilah yang memungkinkan polimorfisme, callback, dan dispatch.
call_indirect memverifikasi bahwa referensi pada index memiliki tipe yang sesuai ((type $t)). Jika tidak cocok — atau index melebihi ukuran table — eksekusi dihentikan dengan trap. Ini menghindari kesalahan yang di C/C++ dikenal sebagai function pointer type confusion, sekaligus menjaga integritas sandbox.
Kombinasi table + call_indirect adalah fondasi mekanisme callback di banyak binding (seperti Emscripten dan wasm-bindgen). Host bisa:
funcref).call_indirect — seolah-olah memanggil fungsi native.Kita akan memakai pola ini lagi saat membahas JS interop di episode 6 dan plugin systems di episode 19.
Tip
Aturan praktis: memory untuk data, table untuk kode. Jika kalian perlu mengirim data besar antar bahasa, gunakan memory + views. Jika kalian perlu memanggil fungsi secara dinamis (callback, dispatch), gunakan table + call_indirect.
memory.grow dulu jika perlu.grow mengembalikan ukuran lama, bukan baru — jangan keliru membaca hasilnya.elem harus dalam batas table — index di (elem (i32.const n) ...) tidak boleh melebihi ukuran table.true pada DataView untuk konsistensi.call_indirect butuh tipe — tanpa (type ...) yang sesuai, verifikasi tipe tidak bisa dilakukan dan modul ditolak.Pada episode 5 ini, kalian telah memahami dua komponen terpenting interop Wasm: linear memory dengan views-nya, dan tables dengan call_indirect.
Inti yang harus dibawa pulang:
memory.size/memory.grow mengontrolnya.Uint8Array (byte) atau DataView (multi-byte + endianness).(data) mengisi memory saat instansiasi; segment (elem) mengisi table.call_indirect menyediakan function pointers yang aman dan terverifikasi tipe.Di episode 6 selanjutnya kita akan membahas JS Interop secara penuh — WebAssembly.instantiate/compile, Instance.exports, passing primitives, import host functions, serta peran glue code Emscripten dan wasm-bindgen. Sampai jumpa di episode 6!