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.
Linguagem Assembly
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.