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 apps mais avançados têm levado os navegadores da Web ao limite. A abordagem de se aproximar do metal usando interfaces com linguagens de nível inferior para melhorar o desempenho é encontrada em muitas linguagens de nível superior. Por exemplo, o Java tem a Java Native Interface. No JavaScript, essa linguagem de nível inferior é o WebAssembly. Neste artigo, você vai descobrir o que é a linguagem assembly e por que ela pode ser útil na Web. Em seguida, vai aprender como o WebAssembly foi criado usando a solução temporária do asm.js.

Você já programou em linguagem Assembly? Na programação de computadores, a linguagem assembly, frequentemente chamada 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 da linguagem e as instruções de código de máquina da arquitetura.

Por exemplo, no Manual do desenvolvedor de software para arquiteturas IA-32 e Intel® 64 (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ê tiver a tarefa de multiplicar 5 e 10, provavelmente vai escrever um código semelhante ao seguinte em JavaScript:

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

A vantagem de usar o assembly é que esse código de baixo nível e otimizado para máquina é muito mais eficiente do que o código de alto nível e otimizado para humanos. No caso anterior, isso não importa, mas você pode imaginar 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 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 um código Assembly sem dependências de arquitetura foi asm.js, um subconjunto restrito de JavaScript que poderia ser usado como uma linguagem de destino eficiente e de baixo nível para compiladores. Essa sublinguagem descreveu de forma eficaz uma máquina virtual em modo sandbox para linguagens sem proteção de memória, como C ou C++. Uma combinação de validação estática e dinâmica permitiu que os mecanismos do JavaScript empregassem uma estratégia de compilação otimizada com antecedência (AOT) para um código válido de asm.js. O código escrito em linguagens com tipagem estática e gerenciamento de memória manual (como C) foi traduzido por um compilador de origem para origem, como o Emscripten (baseado no LLVM).

A performance foi melhorada ao limitar os recursos de idioma aos que são compatíveis com o AOT. O Firefox 22 foi o primeiro navegador a oferecer suporte a asm.js, lançado com o nome OdinMonkey. O Chrome adicionou suporte a asm.js na versão 61. O asm.js ainda funciona em navegadores, mas 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 funciona com desempenho quase nativo e oferece linguagens como C/C++, Rust e muitas outras opções com um destino de compilação para execução na Web. O suporte para linguagens gerenciadas pela memória, como Java e Dart, está em desenvolvimento e deve estar disponível 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 funcionem juntos.

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

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

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

  • Instruções de controle que formam construções de controle e retiram os valores de argumento da pilha podem mudar o contador do programa e enviar valores de resultado para a pilha.
  • Instruções simples que retiram os valores de argumento da pilha, aplicam um operador aos valores e em seguida injetam os valores de resultado na pilha, seguidos por um avanço implícito do contador de programa.

Voltando ao exemplo anterior, o código WebAssembly a seguir 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, seu código pode ser executado em qualquer mecanismo JavaScript (mesmo que não otimizado), o WebAssembly exigia uma nova funcionalidade que todos os fornecedores de navegadores concordavam. Anunciado em 2015 e lançado pela primeira vez 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 aos navegadores é universal.

A 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 expressões S e geralmente usa a extensão de arquivo .wat (para o formato WebAssembly text). Se você quiser, pode escrever à mão. Usando o exemplo de multiplicação acima e tornando-o mais útil sem 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 melhor 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

Como compilar para o WebAssembly

Como mostrado, nem .wat nem .wasm são muito compatíveis com humanos. É aí que entra em cena um compilador como o Emscripten. Ele permite compilar usando linguagens de nível mais alto, como C e C++. Existem outros compiladores para outras linguagens, como Rust e muito mais. Considere o seguinte código C:

#include <stdio.h>

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

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

$ gcc hello.c -o hello

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

$ emcc hello.c -o hello.html

Isso criará um arquivo hello.wasm e o arquivo wrapper HTML hello.html. Quando você serve o arquivo hello.html de um servidor da Web, a "Hello World" é impressa no console do DevTools.

Também há uma maneira de compilar para o WebAssembly sem o wrapper de 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, o Node.js:

$ node hello.js
Hello World

Saiba mais

Esta breve introdução à WebAssembly é apenas a ponta do iceberg. Saiba mais sobre o WebAssembly na documentação do WebAssembly no MDN e consulte a documentação do Emscripten. Para ser sincero, trabalhar com o WebAssembly pode parecer um pouco como o meme "Como desenhar uma coruja", especialmente porque os desenvolvedores da Web que conhecem HTML, CSS e JavaScript não são necessariamente versados em linguagens que precisam ser compiladas, como C. Felizmente, existem canais como a tag webassembly do StackOverflow, em que especialistas estão sempre dispostos a ajudar.

Agradecimentos

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