00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

Interazione con il DOM

Wasm e il DOM

WebAssembly non ha accesso diretto al DOM. Ogni interazione con l’albero HTML deve passare attraverso un bridge JavaScript. Questo è un vincolo architetturale: Wasm opera in una sandbox con accesso solo alla propria memoria lineare e alle funzioni importate.

Il Bridge JavaScript

Il pattern fondamentale prevede che Wasm chiami funzioni JavaScript importate, le quali a loro volta manipolano il DOM.

(module
  ;; Importare funzioni JS per il DOM
  (import "dom" "createElement" (func $createElement (param i32 i32) (result i32)))
  (import "dom" "setText" (func $setText (param i32 i32 i32)))
  (import "dom" "appendChild" (func $appendChild (param i32 i32)))

  (memory (export "memory") 1)

  (func (export "buildUI")
    ;; Logica Wasm che chiama le funzioni bridge
    ;; I parametri sono puntatori alla memoria per le stringhe
    ;; e handle numerici per i nodi DOM
    nop)
)
// Lato JavaScript: implementare il bridge
let nextHandle = 1;
const handleMap = new Map();

function getHandle(element) {
  const h = nextHandle++;
  handleMap.set(h, element);
  return h;
}

function readString(memory, ptr, len) {
  return new TextDecoder().decode(new Uint8Array(memory.buffer, ptr, len));
}

const importObject = {
  dom: {
    createElement(tagPtr, tagLen) {
      const tag = readString(instance.exports.memory, tagPtr, tagLen);
      const el = document.createElement(tag);
      return getHandle(el);
    },
    setText(handle, textPtr, textLen) {
      const el = handleMap.get(handle);
      const text = readString(instance.exports.memory, textPtr, textLen);
      el.textContent = text;
    },
    appendChild(parentHandle, childHandle) {
      const parent = handleMap.get(parentHandle);
      const child = handleMap.get(childHandle);
      parent.appendChild(child);
    },
  },
};

wasm-bindgen e web-sys (Rust)

In Rust, wasm-bindgen e il crate web-sys eliminano la necessità di scrivere il bridge manualmente.

# Cargo.toml
[dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = [
    "Document", "Element", "HtmlElement",
    "Window", "Node", "Event", "HtmlInputElement"
]}
use wasm_bindgen::prelude::*;
use web_sys::{Document, Element, HtmlElement, HtmlInputElement};

#[wasm_bindgen(start)]
pub fn main() -> Result<(), JsValue> {
    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();
    let body = document.body().unwrap();

    // Creare un elemento
    let heading = document.create_element("h1")?;
    heading.set_text_content(Some("Creato da Rust/Wasm!"));
    body.append_child(&heading)?;

    // Creare un input
    let input = document.create_element("input")?;
    input.set_attribute("type", "text")?;
    input.set_attribute("placeholder", "Scrivi qui...")?;
    body.append_child(&input)?;

    // Creare un pulsante con event listener
    let button = document.create_element("button")?;
    button.set_text_content(Some("Saluta"));

    let input_clone = input.clone();
    let closure = Closure::wrap(Box::new(move |_event: web_sys::Event| {
        let input_el: HtmlInputElement = input_clone.clone().dyn_into().unwrap();
        let name = input_el.value();
        web_sys::window()
            .unwrap()
            .alert_with_message(&format!("Ciao, {}!", name))
            .unwrap();
    }) as Box<dyn FnMut(_)>);

    button.add_event_listener_with_callback(
        "click",
        closure.as_ref().unchecked_ref(),
    )?;
    closure.forget(); // Evitare il drop

    body.append_child(&button)?;
    Ok(())
}

Pattern di Comunicazione Wasm <-> DOM

1. Render Loop (Canvas/Giochi)

Wasm calcola lo stato, JS disegna sul Canvas:

const { instance } = await WebAssembly.instantiateStreaming(
  fetch("game.wasm"), imports
);
const ctx = canvas.getContext("2d");

function gameLoop() {
  // Wasm aggiorna lo stato del gioco
  instance.exports.update();

  // JS legge lo stato dalla memoria condivisa e disegna
  const statePtr = instance.exports.getState();
  const state = new Float32Array(
    instance.exports.memory.buffer, statePtr, 100
  );

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  for (let i = 0; i < state.length; i += 4) {
    ctx.fillRect(state[i], state[i+1], state[i+2], state[i+3]);
  }

  requestAnimationFrame(gameLoop);
}
gameLoop();

2. Virtual DOM con Patch

Wasm genera un diff, JS applica le modifiche:

// Wasm produce un array di operazioni DOM
instance.exports.computeDiff(oldStatePtr, newStatePtr);
const opsPtr = instance.exports.getDiffOps();
const opsLen = instance.exports.getDiffOpsLen();
const ops = new Uint32Array(
  instance.exports.memory.buffer, opsPtr, opsLen
);

// JS interpreta le operazioni
for (let i = 0; i < ops.length; i += 3) {
  const op = ops[i];       // 0=create, 1=update, 2=remove
  const nodeId = ops[i+1];
  const value = ops[i+2];
  // Applicare l'operazione al DOM reale
}

3. Message Passing

Comunicazione asincrona tramite coda di messaggi:

// Coda condivisa in SharedArrayBuffer
const queue = new Int32Array(sharedMemory.buffer, 0, 256);

// JS invia un messaggio a Wasm
function sendToWasm(msgType, payload) {
  const idx = Atomics.load(queue, 0); // indice di scrittura
  queue[idx + 1] = msgType;
  queue[idx + 2] = payload;
  Atomics.add(queue, 0, 2);
  Atomics.notify(queue, 0);
}

Worker Threads

Per evitare di bloccare il thread principale, si può eseguire Wasm in un Web Worker:

// main.js
const worker = new Worker("wasm-worker.js");

worker.postMessage({ type: "compute", data: largeArray });
worker.onmessage = (e) => {
  // Aggiornare il DOM con i risultati
  document.getElementById("result").textContent = e.data.result;
};
// wasm-worker.js
let instance;

self.onmessage = async (e) => {
  if (!instance) {
    const { instance: inst } = await WebAssembly.instantiateStreaming(
      fetch("heavy_compute.wasm"), {}
    );
    instance = inst;
  }

  if (e.data.type === "compute") {
    // Scrivere i dati nella memoria Wasm
    const ptr = instance.exports.alloc(e.data.data.length * 4);
    new Float32Array(instance.exports.memory.buffer, ptr, e.data.data.length)
      .set(e.data.data);

    // Eseguire il calcolo pesante
    const result = instance.exports.process(ptr, e.data.data.length);

    // Restituire il risultato al thread principale
    self.postMessage({ result });
  }
};

Questo pattern separa il calcolo pesante (Wasm nel Worker) dall’aggiornamento dell’interfaccia (DOM nel thread principale), mantenendo l’UI sempre reattiva.