WEBASSEMBLY WASM JAVASCRIPT MEMORY-MANAGEMENT DATA-STRUCTURES HIGH-PERFORMANCE FRONTEND WEB-DEVELOPMENT RUST C++ EMSCRIPTEN BROWSER OPTIMIZATION INTEROP

Mengelola Memori dan Data Kompleks di WebAssembly: Jembatan Antara JavaScript dan Kode Native

⏱️ 8 menit baca
👨‍💻

Mengelola Memori dan Data Kompleks di WebAssembly: Jembatan Antara JavaScript dan Kode Native

WebAssembly (Wasm) telah membuka gerbang performa native di browser, memungkinkan kita menjalankan kode yang sangat cepat yang ditulis dalam bahasa seperti Rust, C++, atau Go. Ini adalah game-changer untuk aplikasi yang membutuhkan komputasi intensif, mulai dari pengolahan gambar, video, game, hingga simulasi fisika.

Namun, ada satu area yang seringkali menjadi tantangan dan berpotensi menjadi bottleneck performa jika tidak ditangani dengan benar: interaksi data antara JavaScript dan WebAssembly. Terutama ketika kita perlu bertukar data kompleks seperti string panjang, array objek, atau struktur data kustom.

Artikel ini akan membawa Anda menyelami lebih dalam bagaimana memori Wasm bekerja dan strategi praktis untuk mengelola dan mentransfer data kompleks secara efisien. Tujuannya? Agar aplikasi Wasm Anda tidak hanya cepat di sisi komputasi, tetapi juga mulus dalam berkomunikasi dengan dunia JavaScript.

1. Pendahuluan: Mengapa Interop Data itu Penting?

Bayangkan Anda memiliki modul WebAssembly yang dirancang untuk melakukan pengolahan teks super cepat, misalnya memfilter ribuan komentar atau melakukan pencarian pola yang kompleks. Modul ini menerima input berupa string dari JavaScript dan mengembalikan hasil juga berupa string.

Masalahnya, JavaScript dan WebAssembly hidup di “dunia” yang berbeda dengan cara pengelolaan memori yang berbeda pula:

Jika setiap kali kita mengirim data dari JS ke Wasm atau sebaliknya, kita hanya menyalin seluruh data, ini akan menimbulkan overhead yang signifikan. Untuk data kecil, mungkin tidak terasa. Tapi untuk data berukuran megabyte atau gigabyte, performa aplikasi Anda bisa anjlok drastis.

📌 Intinya: Memahami dan mengoptimalkan bagaimana data berpindah antara JS dan Wasm adalah kunci untuk memaksimalkan potensi performa WebAssembly.

2. Memahami Memori Linier WebAssembly

Setiap instance WebAssembly memiliki akses ke blok memori sendiri, yang disebut memori linier. Ini adalah ArrayBuffer yang dapat diakses dan dimodifikasi oleh JavaScript.

Ketika Anda mengkompilasi kode C/C++ atau Rust ke Wasm, variabel-variabel global, heap, dan stack dari kode native tersebut akan ditempatkan di dalam ArrayBuffer ini.

💡 Analogi: Bayangkan memori Wasm seperti selembar kanvas kosong yang sangat besar. JavaScript bisa “melihat” dan “menggambar” di kanvas itu (mengakses ArrayBuffer), dan kode Wasm juga “menggambar” di sana. Keduanya harus setuju di mana “gambar” (data) itu akan diletakkan dan bagaimana bentuknya.

JavaScript dapat mengakses memori Wasm melalui objek WebAssembly.Memory. Setelah modul Wasm di-instantiate, Anda bisa mendapatkan ArrayBuffer yang mendasarinya dan membuat TypedArray (seperti Uint8Array, Int32Array, Float64Array) untuk membaca atau menulis data.

// Contoh mendapatkan memori Wasm dan membuat TypedArray
const memory = wasmInstance.exports.memory; // memory adalah WebAssembly.Memory object
const uint8Array = new Uint8Array(memory.buffer); // Memori Wasm sebagai Uint8Array
const int32Array = new Int32Array(memory.buffer); // Memori Wasm sebagai Int32Array

⚠️ Penting: Saat memori Wasm diubah oleh Wasm atau JavaScript, ArrayBuffer yang sama akan merefleksikan perubahan tersebut. Namun, jika memori Wasm di-grow (ukurannya bertambah), ArrayBuffer yang lama akan menjadi tidak valid. Anda perlu membuat TypedArray baru dari memory.buffer yang diperbarui.

3. Mengirim String dari JavaScript ke WebAssembly

Mengirim string adalah salah satu kasus interop yang paling umum dan seringkali membingungkan. JavaScript menggunakan string UTF-16, sementara bahasa native seperti C/C++ atau Rust biasanya bekerja dengan string UTF-8 (atau C-string yang diakhiri null).

Prosesnya melibatkan beberapa langkah:

  1. Alokasi Memori di Wasm: JavaScript perlu meminta WebAssembly untuk mengalokasikan ruang memori yang cukup untuk string yang akan dikirim.
  2. Encoding String: String JavaScript (UTF-16) harus di-encode ke format yang diharapkan oleh Wasm (misalnya, UTF-8).
  3. Menulis ke Memori Wasm: Byte-byte hasil encoding kemudian ditulis ke lokasi memori yang dialokasikan di Wasm.
  4. Memanggil Fungsi Wasm: JavaScript memanggil fungsi Wasm, meneruskan pointer (alamat memori) dan panjang string.

Berikut contoh sederhana menggunakan Rust (dikompilasi ke Wasm) dan JavaScript:

// lib.rs (Rust code)
#[no_mangle]
pub extern "C" fn process_string(ptr: *mut u8, len: usize) -> usize {
    let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
    let s = std::str::from_utf8(slice).unwrap();
    // Lakukan sesuatu dengan string 's'
    // Misalnya, cetak atau modifikasi
    println!("String dari JS: {}", s);
    
    // Untuk contoh, kita asumsikan tidak ada perubahan panjang string
    // Dalam kasus nyata, jika string dimodifikasi dan panjang berubah,
    // kita perlu mengalokasikan memori baru atau mengembalikan panjang baru.
    s.len() // Mengembalikan panjang string (UTF-8)
}

#[no_mangle]
pub extern "C" fn allocate(size: usize) -> *mut u8 {
    let mut vec = Vec::<u8>::with_capacity(size);
    let ptr = vec.as_mut_ptr();
    std::mem::forget(vec); // Mencegah Rust membebaskan memori
    ptr
}

#[no_mangle]
pub extern "C" fn deallocate(ptr: *mut u8, capacity: usize) {
    unsafe {
        let _ = Vec::from_raw_parts(ptr, 0, capacity);
    }
}
// index.js (JavaScript code)
async function runWasm() {
    const wasmModule = await WebAssembly.instantiateStreaming(
        fetch('lib.wasm'), { /* importObject */ }
    );
    const { instance } = wasmModule;
    const { process_string, allocate, deallocate, memory } = instance.exports;

    const inputString = "Halo, dunia WebAssembly dari JavaScript!";
    const encoder = new TextEncoder(); // Untuk encoding UTF-8
    const encodedString = encoder.encode(inputString);
    const byteLength = encodedString.length;

    // 1. Alokasi memori di Wasm
    const ptr = allocate(byteLength);

    // 2. Tulis string ke memori Wasm
    const wasmByteView = new Uint8Array(memory.buffer, ptr, byteLength);
    wasmByteView.set(encodedString);

    // 3. Panggil fungsi Wasm
    const resultLen = process_string(ptr, byteLength);
    console.log(`Wasm memproses string dengan panjang ${resultLen} byte.`);

    // 4. Bebaskan memori di Wasm
    deallocate(ptr, byteLength); 
}

runWasm();

Tips: Gunakan TextEncoder untuk meng-encode string JS ke UTF-8. Pastikan Anda mengalokasikan memori yang cukup di Wasm dan membebaskannya setelah selesai, terutama jika Anda mengelola memori secara manual. Pustaka seperti wasm-bindgen untuk Rust sangat membantu mengotomatisasi proses ini.

4. Mengirim String dari WebAssembly ke JavaScript

Prosesnya adalah kebalikan dari sebelumnya:

  1. Wasm Mengalokasikan Memori & Menulis String: Fungsi Wasm mengalokasikan memori internal, menulis string (UTF-8) ke dalamnya, dan mengembalikan pointer ke awal string serta panjangnya.
  2. JavaScript Membaca & Mendekode: JavaScript menerima pointer dan panjang, kemudian membaca byte-byte dari memori Wasm dan mendekodekannya menjadi string JavaScript (UTF-16) menggunakan TextDecoder.
  3. Wasm Membebaskan Memori: JavaScript memberi sinyal kepada Wasm untuk membebaskan memori yang dialokasikan untuk string.

Mari kita tambahkan fungsi ke kode Rust dan JavaScript kita:

// lib.rs (Rust code - Lanjutan)
// ... (fungsi allocate dan deallocate dari sebelumnya) ...

#[no_mangle]
pub extern "C" fn get_greeting(ptr: *mut u8, len: usize) -> *mut u8 {
    let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
    let name = std::str::from_utf8(slice).unwrap();

    let greeting = format!("Halo, {} dari Rust!", name);
    let mut bytes = greeting.into_bytes();
    let ptr = bytes.as_mut_ptr();
    let len = bytes.len();
    std::mem::forget(bytes); // Mencegah Rust membebaskan memori

    // Mengembalikan pointer ke string baru dan panjangnya dalam format 64-bit
    // (pointer_32_bit << 32) | length_32_bit
    // Dalam contoh ini, kita akan mengembalikan pointer saja dan panjangnya secara terpisah
    // atau menggunakan cara yang lebih canggih (wasm-bindgen)
    // Untuk kesederhanaan, kita hanya mengembalikan ptr, dan panjang akan dibaca dari Wasm
    // dalam contoh JS berikutnya.
    ptr
}

// Untuk mendapatkan panjang string hasil dari get_greeting
#[no_mangle]
pub extern "C" fn get_greeting_len() -> usize {
    // Ini hanya contoh. Dalam praktik, Anda perlu menyimpan panjang string
    // yang terakhir kali dihasilkan oleh get_greeting.
    // wasm-bindgen menangani ini secara otomatis.
    // Untuk demo sederhana, kita akan menganggap panjangnya tetap.
    "Halo, <nama> dari Rust!".len() // Panjang placeholder
}
// index.js (JavaScript code - Lanjutan)
async function runWasm() {
    // ... (kode sebelumnya) ...

    const { get_greeting, get_greeting_len } = instance.exports;

    const name = "Developer";
    const nameEncoded = encoder.encode(name);
    const namePtr = allocate(nameEncoded.length);
    new Uint8Array(memory.buffer, namePtr, nameEncoded.length).set(nameEncoded);

    // 1. Panggil fungsi Wasm untuk mendapatkan string
    const greetingPtr = get_greeting(namePtr, nameEncoded.length);
    deallocate(namePtr, nameEncoded.length); // Bebaskan memori untuk nama

    // 2. Baca panjang string dari Wasm (ini adalah cara simplistik)
    // Dalam kasus nyata, Wasm akan mengembalikan pointer dan panjang secara bersamaan