Memahami WebAssembly Text Format (WAT): Menggali Bahasa Mesin Web Anda
1. Pendahuluan
Sebagai developer web modern, kita sering berhadapan dengan JavaScript. Namun, pernahkah Anda merasa bahwa ada batasan performa yang tidak bisa ditembus oleh JavaScript, terutama untuk tugas-tugas komputasi berat seperti pemrosesan gambar, video, game, atau simulasi ilmiah? Di sinilah WebAssembly (Wasm) hadir sebagai game-changer. Wasm memungkinkan kita menjalankan kode yang mendekati performa native langsung di browser, membuka gerbang bagi aplikasi web yang jauh lebih powerful dan responsif.
Tapi, Wasm itu sendiri adalah format biner. Sulit dibaca dan dipahami manusia. Bayangkan Anda harus mendebug file .exe atau .dll secara langsung tanpa disassembler! Nah, di sinilah WebAssembly Text Format (WAT) berperan. WAT adalah representasi tekstual dari modul WebAssembly, semacam “bahasa assembly” yang bisa dibaca manusia.
🎯 Artikel ini akan membawa Anda menyelami dunia WAT. Kita akan belajar apa itu WAT, bagaimana strukturnya, tipe data yang digunakannya, instruksi dasar, dan bagaimana ia berinteraksi dengan JavaScript. Memahami WAT tidak hanya akan memperdalam pemahaman Anda tentang WebAssembly, tetapi juga memberikan wawasan berharga tentang bagaimana kode berperforma tinggi bekerja di web.
2. Apa Itu WebAssembly Text Format (WAT)?
Bayangkan Anda memiliki sebuah program yang ditulis dalam bahasa C++. Setelah dicompile, program tersebut akan menjadi file biner .wasm. File biner ini sangat efisien untuk dieksekusi oleh mesin, tetapi sama sekali tidak bisa dibaca oleh manusia. Di sinilah WAT datang sebagai penyelamat.
WAT adalah format teks yang merepresentasikan modul WebAssembly secara human-readable. Jadi, jika Anda punya file .wasm, Anda bisa mengonversinya kembali menjadi .wat untuk melihat instruksi-instruksi assembly di dalamnya. Ini mirip dengan cara Anda bisa melihat kode assembly dari sebuah program C++ yang sudah dicompile.
✅ Keuntungan Memahami WAT:
- Debugging Lebih Mudah: Ketika ada masalah performa atau bug di modul Wasm Anda, melihat representasi WAT bisa sangat membantu untuk memahami apa yang sebenarnya terjadi di level instruksi.
- Pembelajaran Mendalam: WAT adalah cara terbaik untuk benar-benar memahami bagaimana WebAssembly bekerja di balik layar, mulai dari pengelolaan memori hingga eksekusi fungsi.
- Optimasi Kode: Dengan memahami instruksi dasar, Anda bisa mengidentifikasi potensi optimasi yang mungkin tidak terlihat dari bahasa tingkat tinggi (seperti C++ atau Rust) yang Anda gunakan.
- Membangun Modul Sederhana: Untuk kasus penggunaan yang sangat spesifik dan kecil, Anda bahkan bisa menulis modul Wasm langsung dalam WAT!
Sintaksis WAT menggunakan apa yang disebut S-expressions (Symbolic Expressions), yang mungkin familiar bagi Anda yang pernah berinteraksi dengan bahasa seperti Lisp. Ini berarti kode dikelilingi oleh tanda kurung, seperti (module ...) atau (func ...).
3. Struktur Dasar Modul WAT
Setiap file WAT mendefinisikan sebuah module. module adalah unit dasar dalam WebAssembly yang berisi semua definisi fungsi, variabel global, memori, dan lain-lain.
Berikut adalah struktur dasar sebuah modul WAT:
(module
;; Bagian-bagian modul ada di sini
;; (tipe, fungsi, memori, tabel, global, import, export)
)
Mari kita bedah bagian-bagian pentingnya:
-
(module ...): Ini adalah blok utama yang membungkus seluruh kode WebAssembly Anda. Semua yang ada di dalam tanda kurung ini adalah bagian dari modul. -
type: Mendefinisikan function signature atau tanda tangan fungsi. Ini seperti antarmuka fungsi yang menentukan parameter dan nilai kembaliannya.(type $add_type (func (param i32 i32) (result i32))) ;; Mendefinisikan tipe fungsi bernama $add_type ;; yang menerima dua parameter i32 dan mengembalikan satu nilai i32.Nama dengan
$add_typeadalah opsional, tapi membantu untuk referensi. -
func: Mendefinisikan sebuah fungsi. Fungsi ini akan berisi instruksi-instruksi yang akan dieksekusi.(func $add (export "add") (param $a i32) (param $b i32) (result i32) ;; Instruksi-instruksi fungsi local.get $a local.get $b i32.add )$add: Nama internal fungsi (opsional, tapi disarankan).(export "add"): Membuat fungsi ini bisa diakses dari lingkungan host (misalnya JavaScript) dengan nama “add”.(param $a i32) (param $b i32): Mendefinisikan parameter fungsi.$adan$badalah nama lokal untuk parameter tersebut.i32adalah tipe data (integer 32-bit).(result i32): Mendefinisikan tipe data nilai kembalian fungsi.
-
import: Digunakan untuk mengimpor fungsi, memori, atau variabel global dari lingkungan host (JavaScript).(import "js" "log" (func $log_message (param i32))) ;; Mengimpor fungsi bernama "log" dari modul "js" di host. ;; Fungsi ini secara internal akan dinamai $log_message dan menerima satu i32. -
export: Digunakan untuk mengekspor fungsi, memori, atau variabel global agar bisa diakses oleh lingkungan host. Kita sudah melihat contohnya difunc. -
memory: Mendefinisikan memori linear yang bisa diakses oleh modul Wasm. Memori ini adalah array byte yang bisa dibaca dan ditulis oleh Wasm.(memory (export "mem") 1) ;; Mendefinisikan memori dan mengeksporkannya dengan nama "mem". ;; Angka 1 menunjukkan jumlah halaman memori awal (1 halaman = 64KB). -
data: Digunakan untuk menginisialisasi segmen memori dengan data tertentu.(data (i32.const 0) "Hello, WebAssembly!\00") ;; Menginisialisasi memori mulai dari offset 0 dengan string "Hello, WebAssembly!" ;; \00 adalah null terminator.
4. Tipe Data dan Instruksi Dasar
WebAssembly adalah mesin berbasis stack. Artinya, semua operasi dilakukan dengan mendorong nilai ke stack (push) dan mengambil nilai dari stack (pop).
Tipe Data Numerik
Wasm memiliki empat tipe data dasar untuk angka:
i32: Integer 32-bit (bilangan bulat).i64: Integer 64-bit (bilangan bulat).f32: Floating-point 32-bit (bilangan desimal presisi tunggal).f64: Floating-point 64-bit (bilangan desimal presisi ganda).
⚠️ Perhatikan bahwa tidak ada tipe data string atau boolean secara langsung di Wasm. String direpresentasikan sebagai array byte dalam memori linear, dan boolean biasanya direpresentasikan sebagai i32 (0 untuk false, 1 untuk true).
Instruksi Stack-Based
Instruksi Wasm umumnya mengikuti pola tipe.operasi.
-
tipe.const <nilai>: Mendorong nilai konstan ke stack.i32.const 10 ;; Mendorong integer 10 ke stack f64.const 3.14 ;; Mendorong float 3.14 ke stack -
tipe.add,tipe.sub,tipe.mul,tipe.div: Melakukan operasi aritmatika. Instruksi ini mengambil dua nilai teratas dari stack, melakukan operasi, dan mendorong hasilnya kembali ke stack.i32.const 5 i32.const 3 i32.add ;; Mengambil 3 dan 5, menambahkan, mendorong 8 ke stack -
local.get <indeks/nama>: Mengambil nilai dari variabel lokal atau parameter dan mendorongnya ke stack.local.get 0 ;; Mengambil nilai dari parameter/variabel lokal pertama local.get $a ;; Mengambil nilai dari parameter/variabel lokal bernama $a -
local.set <indeks/nama>: Mengambil nilai dari stack dan menyimpannya ke variabel lokal atau parameter.i32.const 10 local.set $x ;; Mengambil 10 dari stack, menyimpannya ke variabel lokal $x -
call <indeks/nama>: Memanggil fungsi lain. Fungsi yang dipanggil akan mengambil parameter dari stack, mengeksekusi, dan mendorong nilai kembaliannya ke stack.i32.const 5 i32.const 3 call $add ;; Memanggil fungsi $add, mengambil 3 dan 5 sebagai param, ;; mendorong hasilnya ke stack. -
drop: Menghapus nilai teratas dari stack. Berguna jika Anda tidak membutuhkan hasil dari suatu operasi.
📌 Contoh Sederhana: Fungsi Penambahan Dua Angka
Mari kita buat fungsi add dalam WAT yang menerima dua integer 32-bit dan mengembalikan hasil penjumlahannya.
(module
(func (export "add") (param $a i32) (param $b i32) (result i32)
local.get $a ;; Mendorong nilai parameter $a ke stack
local.get $b ;; Mendorong nilai parameter $b ke stack
i32.add ;; Mengambil $b dan $a dari stack, menjumlahkan, mendorong hasilnya ke stack
)
)
Bagaimana cara kerja i32.add?
local.get $a: Stack:[nilai_a]local.get $b: Stack:[nilai_a, nilai_b]i32.add: Mengambilnilai_bdannilai_adari stack, menjumlahkannya (nilai_a + nilai_b), lalu mendorong hasilnya kembali ke stack. Stack:[hasil]Karena ini adalah instruksi terakhir dan fungsi memiliki(result i32), nilai teratas di stack (hasil) akan menjadi nilai kembalian fungsi.
5. Mengelola Memori dengan WAT
Salah satu kekuatan WebAssembly adalah kemampuannya untuk berinteraksi dengan memori linear. Ini adalah array byte yang bisa dibagi antara Wasm dan JavaScript, memungkinkan transfer data yang efisien.
-
(memory (export "mem") <initial_pages> <max_pages>): Mendefinisikan memori linear.(export "mem"): Memori ini bisa diakses oleh JavaScript dengan nama “mem”.<initial_pages>: Jumlah halaman memori awal yang dialokasikan (1 halaman = 64KB).<max_pages>: Batas maksimum halaman memori yang bisa dialokasikan (opsional).
-
Instruksi
loaddanstore: Untuk membaca dan menulis ke memori.tipe.load: Mengambil offset (alamat) dari stack, membaca nilai dari memori pada offset tersebut, dan mendorong nilai yang dibaca ke stack.tipe.store: Mengambil nilai dan offset dari stack, lalu menulis nilai tersebut ke memori pada offset yang diberikan.
💡 Contoh: Menyimpan dan Memuat dari Memori
Mari kita buat fungsi yang menyimpan integer ke memori pada offset tertentu, lalu memuatnya kembali.
(module
(memory (export "mem") 1) ;; Export memori 1 halaman (64KB)
(func (export "storeAndLoad") (param $offset i32) (param $value i32) (result i32)
;; Simpan $value ke memori pada $offset
local.get $offset ;; Mendorong offset ke stack: [$offset]
local.get $value ;; Mendorong value ke stack: [$offset, $value]
i32.store ;; Mengambil $value dan $offset, menyimpan $value di memori[$offset]. Stack kosong.
;; Muat kembali nilai dari $offset
local.get $offset ;; Mendorong offset ke stack: [$offset]
i32.load ;; Mengambil $offset, membaca memori[$offset], mendorong hasilnya ke stack: [loaded_value]
)
)
6. Interaksi dengan JavaScript: Import dan Export
Modul WebAssembly tidak hidup dalam isolasi. Mereka perlu berinteraksi dengan lingkungan host (biasanya JavaScript di browser). Ini dilakukan melalui mekanisme import dan export.
-
export: Sudah kita lihat sebelumnya. Ini membuat fungsi atau memori dari Wasm bisa dipanggil/diakses oleh JavaScript. -
import: Memungkinkan modul Wasm memanggil fungsi yang didefinisikan di JavaScript.
📌 Contoh Interaksi Lengkap
Mari kita buat modul WAT yang mengekspor fungsi add dan storeAndLoad, serta mengimpor fungsi log dari JavaScript.
;; file: my_module.wat
(module
;; Import fungsi log dari JavaScript
(import "js" "log" (func $log_message (param i32)))
;; Definisikan memori
(memory (export "mem") 1)
;; Fungsi penambahan
(func (export "add") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add
call $log_message ;; Panggil fungsi log dari JS dengan hasil penjumlahan
)
;; Fungsi simpan dan muat memori
(func (export "storeAndLoad") (param $offset i32) (param $value i32) (result i32)
local.get $offset
local.get $value
i32.store
local.get $offset
i32.load
)
)
Sekarang, bagaimana kita memuat dan menjalankan ini di JavaScript?
// index.js
async function loadWasm() {
// Konversi WAT ke binary .wasm (biasanya ini dilakukan oleh toolchain)
// Untuk demo, kita bisa pakai wabt (WebAssembly Binary Toolkit) atau online converter
// Di sini kita asumsikan sudah ada my_module.wasm
const watCode = `
(module
(import "js" "log" (func $log_message (param i32)))
(memory (export "mem") 1)
(func (export "add") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add
call $log_message
)
(func (export "storeAndLoad") (param $offset i32) (param $value i32) (result i32)
local.get $offset
local.get $value
i32.store
local.get $offset
i32.load
)
)
`;
// Untuk menjalankan WAT langsung di browser tanpa kompilasi eksternal,
// kita perlu mengonversinya ke binary terlebih dahulu.
// Ini bisa dilakukan dengan tool seperti `wat2wasm` dari Wabt.
// Untuk contoh sederhana ini, kita akan menganggap `watCode` ini sudah dikompilasi
// menjadi array buffer atau bisa langsung di-instantiate oleh WebAssembly.instantiate.
// Namun, WebAssembly.instantiate tidak menerima string WAT, melainkan array buffer binary WASM.
// Jadi, kita perlu step kompilasi WAT ke WASM binary.
// Untuk tujuan edukasi, kita akan menyederhanakan dengan menganggap
// kita memiliki cara untuk mendapatkan binary dari WAT string.
// Realistisnya, Anda akan memuat file .wasm:
// const response = await fetch('my_module.wasm');
// const buffer = await response.arrayBuffer();
// const module = await WebAssembly.compile(buffer);
// Atau, untuk demo WAT string, kita bisa menggunakan TextEncoder dan WebAssembly.Module
// yang secara teknis membutuhkan format binary, jadi kita harus "menulis" binary-nya
// atau menggunakan library. Untuk tutorial ini, kita akan melewati detail kompilasi WAT-to-WASM
// dan fokus pada interaksi setelah modul siap.
// Jika Anda ingin mencoba, Anda bisa menggunakan online WAT to WASM converter,
// atau menginstal `wabt` dan menjalankan `wat2wasm my_module.wat -o my_module.wasm`.
// Anggap `wasmBytes` adalah ArrayBuffer dari `my_module.wasm`
// Untuk demo ini, mari kita gunakan contoh WAT yang lebih sederhana yang bisa dikompilasi on-the-fly
// (meskipun ini bukan cara standar di browser, tapi untuk ilustrasi)
// Sebenarnya WebAssembly.Module hanya menerima binary, bukan text format.
// Untuk demo langsung di JS, kita perlu library