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 aplicativos, alguns dos aplicativos mais avançados levaram os navegadores a seus limites. Em muitos idiomas de nível superior, a abordagem de se "aproximar do metal" fazendo interface com idiomas de nível inferior para melhorar o desempenho é encontrada em muitos idiomas. Por exemplo, o Java tem a Interface nativa do Java. Para JavaScript, essa linguagem de nível inferior é a WebAssembly. Neste artigo, você vai descobrir o que é a linguagem Assembly e por que ela pode ser útil na Web. Depois você vai aprender como o WebAssembly foi criado com a solução provisória de asm.js.

Linguagem Assembly

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

Por exemplo, analisando as arquiteturas Intel® 64 e IA-32 (PDF), a instrução MUL (para múltiplos) realiza uma multiplicação não assinada do primeiro operando (de destino) e do segundo (de origem) e armazena o resultado no de destino. Muito simplificado, 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. Confira 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 tivesse o objetivo de multiplicar 5 e 10, você provavelmente escreveria um código semelhante ao seguinte em JavaScript:

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

A vantagem de seguir a rota de montagem é que esse código de baixo nível e otimizado para máquinas é muito mais eficiente do que um código de alto nível 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 é dependente da arquitetura x86. E se houvesse uma maneira de escrever código Assembly que não dependente de uma arquitetura específica, mas que herdasse os benefícios de desempenho do assembly?

asm.js

A primeira etapa para escrever 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 efetivamente uma máquina virtual em sandbox para linguagens sem segurança de memória, como C ou C++. Uma combinação de validação estática e dinâmica permitiu que os mecanismos JavaScript empregassem uma estratégia de compilação otimizada antecipadamente (AOT, na sigla em inglês) para um código asm.js válido. Código escrito em linguagens tipadas estaticamente com gerenciamento manual de memória (como C) era traduzido por um compilador de origem para fonte, como o Emscripten (baseado em LLVM).

O desempenho foi melhorado com a limitação dos recursos de linguagem aos compatíveis com a 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. Embora o asm.js ainda funcione em navegadores, ele foi substituído pelo WebAssembly. O motivo para usar o asm.js neste momento seria uma alternativa para navegadores que não têm suporte para WebAssembly.

WebAssembly

WebAssembly é uma linguagem assembly de baixo nível com um formato binário compacto que é executada com desempenho quase nativo e fornece linguagens como C/C++ e Rust e muitas outras com um destino de compilação para execução 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 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 de sistema WebAssembly, uma interface de sistema modular para o WebAssembly. O WASI foi criado para ser portátil entre 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 projetado para ser executado em uma máquina de pilha virtual (VM) portátil. O bytecode foi projetado para ser mais rápido de analisar e executar do que JavaScript e para ter uma representação de código compacta.

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

  • As instruções de controle que formam construções de controle e exibem os valores de argumento delas da pilha. Elas podem alterar o contador do programa e enviar os valores dos resultados para a pilha.
  • Instruções simples que exibem os valores de argumento da pilha, aplicam um operador a eles e enviam os valores de resultado para a pilha, seguidas por um avanço implícito do contador de programas.

Voltando ao exemplo anterior, o código WebAssembly abaixo 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 possa ser executado em qualquer mecanismo JavaScript (mesmo se não estiver otimizado), o WebAssembly exigia novas funcionalidades com as quais todos os fornecedores de navegadores concordaram. 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, a compatibilidade com 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 você realmente quisesse, poderia escrevê-lo à mão. Com base no exemplo da multiplicação acima e o tornamos mais útil ao não fixar os fatores no código, 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 se destina ao consumo humano, muito menos à criação humana. Usando uma ferramenta como o wat2wasm, é possível converter o código acima na representação binária a seguir. Os comentários geralmente não fazem parte da representação binária, mas são adicionados pela ferramenta wat2Wasm para uma 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 no WebAssembly

Como você pode notar, nem .wat nem .wasm são especialmente muito humanos. É aqui que um compilador como o Emscripten entra em cena. Ele permite compilar usando linguagens de nível superior, como C e C++. Existem outros compiladores para outras linguagens, como Rust e muitas outras. Considere o seguinte código C:

#include <stdio.h>

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

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

$ gcc hello.c -o hello

Com o Emscripten instalado, faça a compilação para o 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 wrapper HTML hello.html. Ao disponibilizar o arquivo hello.html de um servidor da Web, você verá "Hello World" impresso no console do DevTools.

Há também uma maneira de compilar para o 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 resultante hello.js com, por exemplo, Node.js:

$ node hello.js
Hello World

Saiba mais

Esta breve introdução ao 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 (links em inglês). A verdade é que trabalhar com o WebAssembly pode ser um pouco parecido com Como desenhar um meme de coruja, especialmente porque desenvolvedores da Web familiarizados com HTML, CSS e JavaScript não são necessariamente experientes em linguagens a serem compiladas em C. Felizmente, existem canais como a tag webassembly do StackOverflow, em que os especialistas geralmente ficam felizes em ajudar quando você pede bem.

Agradecimentos

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