O que é o WebAssembly e de onde ele veio?

Desde que a Web se tornou uma plataforma não apenas para documentos, mas também para apps, alguns dos aplicativos mais avançados têm levado os navegadores da Web ao limite. A abordagem de "chegar mais perto do hardware" ao interagir com linguagens de nível mais baixo para melhorar o desempenho é encontrada em muitas linguagens de nível mais alto. Por exemplo, o Java tem a Java Native Interface. Para JavaScript, essa linguagem de nível mais baixo é o WebAssembly. Neste artigo, você vai descobrir o que é a linguagem de assembly e por que ela pode ser útil na Web. Depois, vai aprender como o WebAssembly foi criado usando a solução provisória do asm.js.

Linguagem Assembly

Você já programou em linguagem assembly? Na programação de computadores, a linguagem de assembly, geralmente chamada apenas de Assembly e abreviada como ASM ou asm, é qualquer linguagem de programação de baixo nível com uma correspondência muito forte entre as instruções na linguagem e as instruções de código de máquina da arquitetura.

Por exemplo, no documento Arquiteturas Intel® 64 e IA-32 (PDF), a instrução MUL (para multiplicação) realiza uma multiplicação sem sinal do primeiro operando (operando de destino) e do segundo operando (operando de origem) e armazena o resultado no operando de destino. De forma muito simplificada, o operando de destino é um operando implícito localizado no registro AX, e o operando de origem está localizado em um registro de uso geral, como CX. O resultado é armazenado novamente no registro AX. Considere o exemplo de código x86 a seguir:

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.

Para fins de comparação, se você tivesse a tarefa de multiplicar 5 e 10, provavelmente escreveria um código semelhante ao seguinte em JavaScript:

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

A vantagem de usar a linguagem assembly é que esse código de baixo nível e otimizado para máquinas é muito mais eficiente do que o código de alto nível e otimizado para humanos. No caso anterior, isso não importa, mas imagine que, para operações mais complexas, a diferença pode ser significativa.

Como o nome sugere, o código x86 depende da arquitetura x86. E se houvesse uma maneira de escrever código de assembly que não dependesse de uma arquitetura específica, mas que herdasse os benefícios de desempenho do assembly?

asm.js

A primeira etapa para escrever código de assembly sem dependências de arquitetura foi o asm.js, um subconjunto estrito de JavaScript que podia ser usado como uma linguagem de destino eficiente e de baixo nível para compiladores. Essa sublinguagem descrevia efetivamente uma máquina virtual em sandbox para linguagens sem proteção de memória, como C ou C++. Uma combinação de validação estática e dinâmica permitia que os mecanismos JavaScript empregassem uma estratégia de compilação de otimização antecipada (AOT, na sigla em inglês) para código asm.js válido. O código escrito em linguagens estaticamente tipadas com gerenciamento manual de memória (como C) foi traduzido por um compilador de origem para origem, como o Emscripten inicial (baseado em LLVM).

A performance foi melhorada ao limitar os recursos de linguagem àqueles adequados para AOT. O Firefox 22 foi o primeiro navegador a oferecer suporte ao asm.js, lançado com o nome OdinMonkey. O Chrome adicionou suporte para asm.js na versão 61. Embora o asm.js ainda funcione em navegadores, ele foi substituído pelo WebAssembly. O motivo para usar asm.js neste momento seria como uma alternativa para navegadores que não têm suporte ao WebAssembly.

WebAssembly

O WebAssembly é uma linguagem de baixo nível semelhante a assembly com um formato binário compacto que é executado com desempenho quase nativo e oferece linguagens como C/C++ e Rust, além de muitas outras com uma meta de compilação para que sejam executadas na Web. O suporte para linguagens gerenciadas por memória, como Java e Dart, está em desenvolvimento e deve ser disponibilizado em breve ou já foi lançado, como no caso do Kotlin/Wasm. O WebAssembly foi projetado para ser executado com o JavaScript, permitindo que ambos trabalhem juntos.

Além do navegador, os programas WebAssembly também são executados em outros ambientes de execução graças ao WASI, a interface do sistema WebAssembly, uma interface modular para WebAssembly. O WASI foi criado para ser portátil em vários sistemas operacionais, com o objetivo de ser seguro e poder ser executado em um ambiente de sandbox.

O código WebAssembly (código binário, ou seja, bytecode) foi criado para ser executado em uma máquina virtual (VM) de pilha portátil. O bytecode foi projetado para ser mais rápido de analisar e executar do que o JavaScript e ter uma representação de código compacta.

A execução conceitual das instruções é feita por um contador de programa tradicional que avança pelas instruções. Na prática, a maioria dos mecanismos do Wasm compila o bytecode do Wasm em código de máquina e o executa. As instruções se dividem em duas categorias:

  • Instruções de controle que formam construções de controle e extraem os valores de argumento da pilha, podem mudar o contador de programa e inserir valores de resultado na pilha.
  • Instruções simples que extraem os valores de argumento da pilha, aplicam um operador aos valores e inserem os valores de resultado na pilha, seguidas por um avanço implícito do contador de programa.

Voltando ao exemplo anterior, o seguinte código WebAssembly seria equivalente ao código x86 do início do artigo:

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.

Embora o asm.js seja implementado totalmente em software, ou seja, o código dele pode ser executado em qualquer mecanismo JavaScript (mesmo que não esteja otimizado), o WebAssembly exigiu uma nova funcionalidade que todos os fornecedores de navegadores concordaram em implementar. Anunciado em 2015 e lançado em março de 2017, o WebAssembly se tornou uma recomendação do W3C em 5 de dezembro de 2019. O W3C mantém o padrão com contribuições de todos os principais fornecedores de navegadores e outras partes interessadas. Desde 2017, o suporte a navegadores é universal.

O WebAssembly tem duas representações: textual e binária. O que você vê acima é a representação textual.

Representação textual

A representação textual é baseada em S-expressions e geralmente usa a extensão de arquivo .wat (para o formato de texto WebAssembly). Se quiser, você pode escrever à mão. Pegando o exemplo de multiplicação acima e tornando-o mais útil ao não codificar os fatores, você provavelmente vai entender o seguinte código:

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

Representação binária

O formato binário que usa a extensão de arquivo .wasm não é destinado ao consumo humano, muito menos à criação humana. Usando uma ferramenta como wat2wasm, é possível converter o código acima na seguinte representação binária. Os comentários geralmente não fazem parte da representação binária, mas são adicionados pela ferramenta wat2wasm para facilitar a compreensão.

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

Compilar para WebAssembly

Como você pode ver, nem .wat nem .wasm são particularmente fáceis de usar para humanos. É aí que entra um compilador como o Emscripten. Ele permite compilar de linguagens de nível mais alto, como C e C++. Há outros compiladores para outras linguagens, como Rust e muitas outras. Considere o seguinte código em C:

#include <stdio.h>

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

Normalmente, você compila esse programa em C com o compilador gcc.

$ gcc hello.c -o hello

Com o Emscripten instalado, compile para WebAssembly usando o comando emcc e quase os mesmos argumentos:

$ emcc hello.c -o hello.html

Isso vai criar um arquivo hello.wasm e o arquivo de wrapper HTML hello.html. Quando você veicular o arquivo hello.html de um servidor da Web, "Hello World" será impresso no console do DevTools.

Também é possível compilar para WebAssembly sem o wrapper HTML:

$ emcc hello.c -o hello.js

Como antes, isso vai criar um arquivo hello.wasm, mas desta vez um arquivo hello.js em vez do wrapper HTML. Para testar, execute o arquivo JavaScript hello.js resultante com, por exemplo, Node.js:

$ node hello.js
Hello World

Saiba mais

Esta breve introdução ao WebAssembly é apenas uma pequena parte do que ele pode fazer. Saiba mais sobre o WebAssembly na documentação do WebAssembly no MDN e consulte a documentação do Emscripten. Na verdade, trabalhar com WebAssembly pode parecer um pouco com o meme de como desenhar uma coruja, principalmente porque os desenvolvedores da Web que conhecem HTML, CSS e JavaScript não necessariamente dominam linguagens a serem compiladas, como C. Felizmente, existem canais como a tag webassembly do StackOverflow, em que especialistas costumam ajudar se você pedir com educação.

Agradecimentos

Este artigo foi revisado por Jakob Kummerow, Derek Schuff e Rachel Andrew.