Che cos'è WebAssembly e da dove viene?

Da quando il web è diventato una piattaforma non solo per i documenti ma anche per le app, alcune delle app più avanzate hanno spinto i browser web al proprio limite. L'approccio "più vicino al metallo" interfacciandosi con linguaggi di livello inferiore per migliorare le prestazioni si riscontra in molte lingue di livello superiore. Ad esempio, Java dispone dell'interfaccia nativa Java. Per JavaScript, questo linguaggio di livello inferiore è WebAssembly. In questo articolo scoprirai cos'è il linguaggio assembly e perché può essere utile sul web, quindi scoprirai come è stato creato WebAssembly tramite la soluzione provvisoria di asm.js.

Linguaggio assemblativo

Hai mai programmato in linguaggio assembly? Nella programmazione informatica, il linguaggio assembly, spesso indicato semplicemente come Assembly e comunemente abbreviato come ASM o asm, è qualsiasi linguaggio di programmazione di basso livello con una corrispondenza molto forte tra le istruzioni del linguaggio e le istruzioni del codice macchina dell'architettura.

Ad esempio, esaminando Intel® 64 and IA-32 Architectures (PDF), l'istruzione MUL (per la multiplicazione) esegue una moltiplicazione non firmata del primo operando (operando di destinazione) e del secondo (operando di origine) e archivia il risultato nell'operando di destinazione. Molto semplificato, l'operando di destinazione è un operando implicito situato nel registro AX, mentre l'operando di origine si trova in un registro generico come CX. Il risultato viene nuovamente memorizzato nel registro AX. Considera il seguente esempio di codice x86:

mov ax, 5  ; Set the value of register AX to 5.
mov cx, 10 ; Set the value of register CX to 10.
mul cx     ; Multiply the value of register AX (5)
           ; and the value of register CX (10), and
           ; store the result in register AX.

Per fare un confronto, se il tuo compito è moltiplicare 5 e 10, probabilmente scriveresti un codice simile al seguente in JavaScript:

const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;

Il vantaggio di seguire il percorso di assemblaggio è che questo codice di basso livello e ottimizzato per la macchina è molto più efficiente di un codice di alto livello e ottimizzato per l'uomo. Nel caso precedente non importa, ma si può immaginare che, per operazioni più complesse, la differenza possa essere significativa.

Come suggerisce il nome, il codice x86 dipende dall'architettura x86. Cosa accadrebbe se esistesse un modo di scrivere codice Assembly che non dipendesse da un'architettura specifica, ma che erediterebbe i vantaggi in termini di prestazioni dell'assemblaggio?

asm.js

Il primo passaggio per scrivere codice assembly senza dipendenze dell'architettura è stato asm.js, un sottoinsieme rigoroso di JavaScript che può essere utilizzato come linguaggio di destinazione efficiente e di basso livello per i compilatori. Questo sottolinguaggio descriveva efficacemente una macchina virtuale con sandbox per linguaggi non sicuri per la memoria come C o C++. Una combinazione di convalida statica e dinamica ha consentito ai motori JavaScript di impiegare una strategia di compilazione per l'ottimizzazione anticipata (AOT) per un codice asm.js valido. Il codice scritto in lingue digitate in modo statico con gestione manuale della memoria (come C) è stato tradotto da un compilatore source-to-source come early Emscripten (basato su LLVM).

Il rendimento è stato migliorato limitando le funzionalità del linguaggio a quelle adatte all'AOT. Firefox 22 è stato il primo browser a supportare asm.js, rilasciato con il nome OdinMonkey. Nella versione 61, Chrome ha aggiunto il supporto per asm.js. Asm.js funziona ancora nei browser, ma è stato sostituito da WebAssembly. Il motivo per utilizzare asm.js a questo punto è un'alternativa per i browser che non supportano WebAssembly.

WebAssembly

WebAssembly è un linguaggio simile all'assemblaggio di basso livello con un formato binario compatto che viene eseguito con prestazioni quasi native e fornisce linguaggi come C/C++ e Rust e molti altri con una destinazione di compilazione in modo che vengano eseguiti sul web. Il supporto per linguaggi gestiti dalla memoria come Java e Dart è in fase di sviluppo e dovrebbe essere disponibile a breve oppure è già disponibile come nel caso di Kotlin/Wasm. WebAssembly è progettato per essere eseguito insieme a JavaScript, consentendo a entrambi di funzionare insieme.

Oltre al browser, i programmi WebAssembly vengono eseguiti anche in altri runtime grazie a WASI, l'interfaccia di sistema WebAssembly, un'interfaccia di sistema modulare per WebAssembly. WASI è stato creato per essere portabile su più sistemi operativi, con l'obiettivo di essere sicuro e di essere eseguito in un ambiente sandbox.

Il codice WebAssembly (codice binario, ossia bytecode) deve essere eseguito su una macchina virtuale (VM) portatile nello stack. Il bytecode è progettato per essere più veloce da analizzare ed eseguire rispetto a JavaScript e per avere una rappresentazione compatta del codice.

L'esecuzione concettuale delle istruzioni procede per mezzo di un contatore di programma tradizionale che avanza attraverso le istruzioni. In pratica, la maggior parte dei motori Wasm compila il bytecode Wasm in codice della macchina e poi lo esegue. Le istruzioni rientrano in due categorie:

  • Le istruzioni di controllo che costruiscono il controllo modulo e inseriscono i relativi valori di argomento fuori dallo stack, possono modificare il contatore del programma ed eseguire il push dei valori dei risultati nello stack.
  • Semplici istruzioni che estraggono i valori di argomento dallo stack, applicano un operatore ai valori e quindi eseguono il push dei valori dei risultati nello stack, seguiti da un avanzamento implicito del contatore del programma.

Tornando all'esempio precedente, il seguente codice WebAssembly sarebbe equivalente al codice x86 dell'inizio dell'articolo:

i32.const 5  ; Push the integer value 5 onto the stack.
i32.const 10 ; Push the integer value 10 onto the stack.
i32.mul      ; Pop the two most recent items on the stack,
             ; multiply them, and push the result onto the stack.

Sebbene asm.js sia implementato interamente nel software, ovvero il suo codice può essere eseguito in qualsiasi motore JavaScript (anche se non ottimizzato), WebAssembly richiedeva una nuova funzionalità concordata da tutti i fornitori di browser. Annunciato nel 2015 e rilasciato per la prima volta a marzo 2017, WebAssembly è diventato un consiglio di W3C il 5 dicembre 2019. W3C mantiene gli standard con il contributo di tutti i principali fornitori di browser e di altre parti interessate. Dal 2017, il supporto dei browser è universale.

WebAssembly ha due rappresentazioni: testuale e binaria. Sopra è riportata la rappresentazione testuale.

Rappresentazione testuale

La rappresentazione testuale si basa sulle espressioni S e di solito utilizza l'estensione del file .wat (per il formato WebAssembly tess). Se davvero volessi, puoi scriverlo a mano. Utilizzando l'esempio di moltiplicazione sopra riportato e rendendolo più utile non eseguendo più l'hardcoded dei fattori, probabilmente puoi interpretare il seguente codice:

(module
  (func $mul (param $factor1 i32) (param $factor2 i32) (result i32)
    local.get $factor1
    local.get $factor2
    i32.mul)
  (export "mul" (func $mul))
)

Rappresentazione binaria

Il formato binario che utilizza l'estensione del file .wasm non è pensato per il consumo umano, tanto meno per la creazione umana. Con uno strumento come wat2wasm, puoi convertire il codice riportato sopra nella seguente rappresentazione binaria. (Di solito i commenti non fanno parte della rappresentazione binaria, ma sono aggiunti dallo strumento wat2wasm per una migliore comprensibilità).

0000000: 0061 736d                             ; WASM_BINARY_MAGIC
0000004: 0100 0000                             ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                    ; section code
0000009: 00                                    ; section size (guess)
000000a: 01                                    ; num types
; func type 0
000000b: 60                                    ; func
000000c: 02                                    ; num params
000000d: 7f                                    ; i32
000000e: 7f                                    ; i32
000000f: 01                                    ; num results
0000010: 7f                                    ; i32
0000009: 07                                    ; FIXUP section size
; section "Function" (3)
0000011: 03                                    ; section code
0000012: 00                                    ; section size (guess)
0000013: 01                                    ; num functions
0000014: 00                                    ; function 0 signature index
0000012: 02                                    ; FIXUP section size
; section "Export" (7)
0000015: 07                                    ; section code
0000016: 00                                    ; section size (guess)
0000017: 01                                    ; num exports
0000018: 03                                    ; string length
0000019: 6d75 6c                          mul  ; export name
000001c: 00                                    ; export kind
000001d: 00                                    ; export func index
0000016: 07                                    ; FIXUP section size
; section "Code" (10)
000001e: 0a                                    ; section code
000001f: 00                                    ; section size (guess)
0000020: 01                                    ; num functions
; function body 0
0000021: 00                                    ; func body size (guess)
0000022: 00                                    ; local decl count
0000023: 20                                    ; local.get
0000024: 00                                    ; local index
0000025: 20                                    ; local.get
0000026: 01                                    ; local index
0000027: 6c                                    ; i32.mul
0000028: 0b                                    ; end
0000021: 07                                    ; FIXUP func body size
000001f: 09                                    ; FIXUP section size
; section "name"
0000029: 00                                    ; section code
000002a: 00                                    ; section size (guess)
000002b: 04                                    ; string length
000002c: 6e61 6d65                       name  ; custom section name
0000030: 01                                    ; name subsection type
0000031: 00                                    ; subsection size (guess)
0000032: 01                                    ; num names
0000033: 00                                    ; elem index
0000034: 03                                    ; string length
0000035: 6d75 6c                          mul  ; elem name 0
0000031: 06                                    ; FIXUP subsection size
0000038: 02                                    ; local name type
0000039: 00                                    ; subsection size (guess)
000003a: 01                                    ; num functions
000003b: 00                                    ; function index
000003c: 02                                    ; num locals
000003d: 00                                    ; local index
000003e: 07                                    ; string length
000003f: 6661 6374 6f72 31            factor1  ; local name 0
0000046: 01                                    ; local index
0000047: 07                                    ; string length
0000048: 6661 6374 6f72 32            factor2  ; local name 1
0000039: 15                                    ; FIXUP subsection size
000002a: 24                                    ; FIXUP section size

Compilazione in WebAssembly

Come puoi vedere, né .wat.wasm sono particolarmente facili da usare per le persone. È qui che entra in gioco un compilatore come Emscripten. Consente di compilare da linguaggi di livello superiore come C e C++. Esistono altri compilatori per altri linguaggi, come Rust e molti altri. Considera il seguente codice C:

#include <stdio.h>

int main() {
  printf("Hello World\n");
  return 0;
}

Di solito, questo programma C viene compilato con il compilatore gcc.

$ gcc hello.c -o hello

Con Emscripten installato, lo compili in WebAssembly utilizzando il comando emcc e quasi gli stessi argomenti:

$ emcc hello.c -o hello.html

Verranno creati un file hello.wasm e il file wrapper HTML hello.html. Quando pubblichi il file hello.html da un server web, vedrai "Hello World" stampato nella console DevTools.

C'è anche un modo per eseguire la compilazione in WebAssembly senza il wrapper HTML:

$ emcc hello.c -o hello.js

Come in precedenza, verrà creato un file hello.wasm, ma questa volta un file hello.js al posto del wrapper HTML. Per il test, esegui il file JavaScript hello.js risultante con, ad esempio, Node.js:

$ node hello.js
Hello World

Scopri di più

Questa breve introduzione a WebAssembly è solo la punta dell'iceberg. Scopri di più su WebAssembly nella documentazione di WebAssembly su MDN e consulta la documentazione di Emscripten. A dire la verità, lavorare con WebAssembly può sembrare un po' come How to disegnare an owl meme, soprattutto perché gli sviluppatori web che hanno familiarità con HTML, CSS e JavaScript non sono necessariamente esperti nei linguaggi da compilare come C. Fortunatamente ci sono canali come il tag webassembly di StackOverflow, in cui gli esperti sono spesso felici di aiutarti se li chiedi in modo gentile.

Ringraziamenti

Questo articolo è stato scritto da Jakob Kummerow, Derek Schuff e Rachel Andrew.