Entendendo o formato textual do WebAssembly
Para permitir que o WebAssembly seja lido e editado por humanos, existe uma representação textual do formato binário Wasm. Este é um formulário intermediário projetado para ser exposto em editores de texto, ferramentas de desenvolvedor de navegador etc. Este artigo explica como esse formato de texto funciona, em termos de sintaxe bruta, e como ele está relacionado ao bytecode subjacente que ele representa — e os objetos wrapper que representam Wasm em JavaScript.
Nota: Isso pode ser um exagero se você for um desenvolvedor da Web que deseja apenas carregar um módulo Wasm em uma página e usá-lo em seu código (consulte Usando a API JavaScript WebAssembly), mas é mais útil se, por exemplo, você deseja escrever módulos Wasm para otimizar o desempenho de sua biblioteca JavaScript ou criar seu próprio compilador WebAssembly.
S-expressions
Nos formatos binário e textual, a unidade fundamental de código no WebAssembly é um módulo. No formato de texto, um módulo é representado como uma grande S-expression. As S-expressions são um formato textual muito antigo e muito simples para representar árvores e, portanto, podemos pensar em um módulo como uma árvore de nós que descreve a estrutura do módulo e seu código. Ao contrário da Abstract Syntax Tree de uma linguagem de programação, porém, a árvore do WebAssembly é bastante plana, consistindo principalmente em listas de instruções.
Primeiro, vamos ver como é uma S-expression. Cada nó na árvore vai dentro de um par de parênteses — ( ... )
. O primeiro rótulo dentro dos parênteses informa que tipo de nó é e, depois disso, há uma lista separada por espaços de atributos ou nós filhos. Isso significa a S-expression do WebAssembly:
(module (memory 1) (func))
representa uma árvore com o nó raiz "module" e dois nós filhos, um nó "memory" com o atributo "1" e um nó "func". Veremos em breve o que esses nós realmente significam.
O módulo mais simples
Vamos começar com o módulo Wasm mais simples e mais curto possível.
(module)
Este módulo está totalmente vazio, mas ainda é um módulo válido.
Se convertermos nosso módulo para binário agora (consulte Convertendo o formato de texto WebAssembly para Wasm), veremos apenas o cabeçalho do módulo de 8 bytes descrito no formato binário:
0000000: 0061 736d ; WASM_BINARY_MAGIC
0000004: 0100 0000 ; WASM_BINARY_VERSION
Adicionando funcionalidade ao seu módulo
Ok, isso não é muito interessante, vamos adicionar algum código executável a este módulo.
Todo o código em um módulo webassembly é agrupado em funções, que possuem a seguinte estrutura de pseudocódigo:
( func <assinatura> <locais> <corpo> )
- A assinatura declara o que a função recebe (parâmetros) e retorna (valores de retorno).
- Os locais são como variáveis em JavaScript, mas com tipos explícitos declarados.
- O corpo é apenas uma lista linear de instruções de baixo nível.
Portanto, isso é semelhante a funções em outras linguagens, mesmo que pareça diferente porque é uma S-expression.
Assinaturas e parâmetros
A assinatura é uma sequência de declarações de tipo de parâmetro seguida por uma lista de declarações de tipo de retorno. Vale ressaltar aqui que:
- A ausência de um
(result)
significa que a função não retorna nada. - Na iteração atual, pode haver no máximo 1 tipo de retorno, mas posteriormente isso será flexibilizado para qualquer número.
Cada parâmetro tem um tipo explicitamente declarado; Wasm Tipos de número, Tipos de referência, Tipos de vetor. Os tipos de números são:
i32
: inteiro de 32 bitsi64
: inteiro de 64 bitsf32
: flutuante de 32 bitsf64
: flutuante de 64 bits
Um único parâmetro é escrito (param i32)
e o tipo de retorno é escrito (result i32)
, portanto, uma função binária que recebe dois inteiros de 32 bits e retorna um float de 64 bits seria escrita assim:
(func (param i32) (param i32) (result f64) ...)
Após a assinatura, os locais são listados com seu tipo, por exemplo (local i32)
. Parâmetros são basicamente apenas locais que são inicializados com o valor do argumento correspondente passado pelo chamador.
Obtendo e configurando locais e parâmetros
Locais/parâmetros podem ser lidos e escritos pelo corpo da função com as instruções local.get
e local.set
.
Os comandos local.get
/local.set
referem-se ao item a ser obtido/definido por seu índice numérico: os parâmetros são referidos primeiro, na ordem de sua declaração, seguidos pelos locais na ordem de sua declaração. Assim, dada a seguinte função:
(func (param i32) (param f32) (local f64)
local.get 0
local.get 1
local.get 2)
A instrução local.get 0
obteria o parâmetro i32, local.get 1
obteria o parâmetro f32 e local.get 2
obteria o f64 local.
Há outro problema aqui - usar índices numéricos para se referir a itens pode ser confuso e irritante, então o formato de texto permite que você nomeie parâmetros, locais e a maioria dos outros itens simplesmente incluindo um nome prefixado por um cifrão ($
) logo antes da declaração de tipo.
Assim, você poderia reescrever nossa assinatura anterior assim:
(func (param $p1 i32) (param $p2 f32) (local $loc f64) …)
E então poderia escrever local.get $p1
em vez de local.get 0
, etc. (Observe que, quando este texto for convertido em binário, o binário conterá apenas o número inteiro).
Máquinas de pilha
Antes de podermos escrever um corpo de função, temos que falar sobre mais uma coisa: máquinas de pilha. Embora o navegador o compile para algo mais eficiente, a execução do Wasm é definida em termos de uma máquina de pilha onde a ideia básica é que todo tipo de instrução empurra e/ou retira um certo número de valores i32
/i64
/f32
/f64
para/de uma pilha.
Por exemplo, local.get
é definido para colocar o valor do local lido na pilha e i32.add
empurra dois valores i32
(ele implicitamente pega os dois valores anteriores colocados na pilha), calcula sua soma (módulo 2^32) e empurra o valor i32
resultante.
Quando uma função é chamada, ela começa com uma pilha vazia que é gradualmente preenchida e esvaziada conforme as instruções do corpo são executadas. Por exemplo, depois de executar a seguinte função:
(func (param $p i32)
(result i32)
local.get $p
local.get $p
i32.add)
A pilha contém exatamente um valor i32
— o resultado da expressão ($p + $p
), que é tratado por i32.add
. O valor de retorno de uma função é apenas o valor final deixado na pilha.
As regras de validação do WebAssembly garantem que a pilha corresponda exatamente: se você declarar um (result f32)
, a pilha deve conter exatamente um f32
no final. Se não houver nenhum tipo de resultado, a pilha deverá estar vazia.
Nosso primeiro corpo funcional
Como mencionado anteriormente, o corpo da função é uma lista de instruções que são seguidas conforme a função é chamada. Juntando isso com o que já aprendemos, podemos finalmente definir um módulo contendo nossa própria função simples:
(module
(func (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add))
Esta função obtém dois parâmetros, soma-os e retorna o resultado.
Há muito mais coisas que podem ser colocadas dentro dos corpos das funções, mas começaremos simples por enquanto, e você verá muito mais exemplos à medida que avança. Para obter uma lista completa dos opcodes disponíveis, consulte a referência de semântica webassembly.org.
Chamando a função
Nossa função não fará muito sozinha — agora precisamos chamá-la. Como fazemos isso? Como em um módulo ES, as funções Wasm devem ser explicitamente exportadas por uma instrução export
dentro do módulo.
Como os locais, as funções são identificadas por um índice por padrão, mas por conveniência, elas podem ser nomeadas. Vamos começar fazendo isso — primeiro, adicionaremos um nome precedido por um cifrão, logo após a palavra-chave func
:
(func $add …)
Agora precisamos adicionar uma declaração de exportação — fica assim:
(export "add" (func $add))
Aqui, add
é o nome pelo qual a função será identificada em JavaScript, enquanto $add
seleciona qual função WebAssembly dentro do módulo está sendo exportada.
Portanto, nosso módulo final (por enquanto) fica assim:
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
(export "add" (func $add))
)
Se você quiser seguir o exemplo, salve nosso módulo acima em um arquivo chamado add.wat
, então converta-o em um arquivo binário chamado add.wasm
usando wabt (consulte Convertendo o formato de texto WebAssembly para Wasm para detalhes).
Em seguida, instanciaremos nosso binário de forma assíncrona (consulte Carregando e executando o código WebAssembly) e executaremos nossa função add
em JavaScript (agora podemos encontrar add()
na propriedade exports
da instância):
WebAssembly.instantiateStreaming(fetch("add.wasm")).then((obj) => {
console.log(obj.instance.exports.add(1, 2)); // "3"
});
Nota:
Você pode encontrar este exemplo no GitHub como add.html (veja ao vivo também). Consulte também WebAssembly.instantiateStreaming()
para obter mais detalhes sobre a função instanciar.
Explorando fundamentos
Agora que cobrimos o básico real, vamos ver alguns recursos mais avançados.
Chamando funções de outras funções no mesmo módulo
A instrução call
chama uma única função, dado seu índice ou nome. Por exemplo, o seguinte módulo contém duas funções — uma apenas retorna o valor 42, a outra retorna o resultado da chamada do primeiro mais um:
(module
(func $getAnswer (result i32)
i32.const 42)
(func (export "getAnswerPlus1") (result i32)
call $getAnswer
i32.const 1
i32.add))
Nota: i32.const
apenas define um inteiro de 32 bits e o coloca na pilha. Você pode trocar o i32
por qualquer um dos outros tipos disponíveis e alterar o valor do const para o que quiser (aqui definimos o valor para 42
).
Neste exemplo, você notará uma seção (export "getAnswerPlus1")
, declarada logo após a instrução func
na segunda função — esta é uma forma abreviada de declarar que queremos exportar esta função e definir o nome como queremos exportá-la.
Isso é funcionalmente equivalente a incluir uma instrução de função separada fora da função, em outro lugar no módulo da mesma maneira que fizemos antes, por exemplo:
(export "getAnswerPlus1" (func $functionName))
O código JavaScript para chamar nosso módulo acima se parece com:
WebAssembly.instantiateStreaming(fetch("call.wasm")).then((obj) => {
console.log(obj.instance.exports.getAnswerPlus1()); // "43"
});
Importando funções do JavaScript
Já vimos JavaScript chamando funções WebAssembly, mas e o WebAssembly chamando funções JavaScript? O WebAssembly não possui nenhum conhecimento interno de JavaScript, mas possui uma maneira geral de importar funções que podem aceitar funções JavaScript ou Wasm. Vejamos um exemplo:
(module
(import "console" "log" (func $log (param i32)))
(func (export "logIt")
i32.const 13
call $log))
WebAssembly tem um namespace de dois níveis, então a declaração de importação aqui está dizendo que estamos pedindo para importar a função log
do módulo console
. Você também pode ver que a função logIt
exportada chama a função importada usando a instrução call
que introduzimos acima.
As funções importadas são como funções normais: elas têm uma assinatura que a validação do WebAssembly verifica estaticamente e recebem um índice e podem ser nomeadas e chamadas.
As funções JavaScript não têm noção de assinatura, portanto qualquer função JavaScript pode ser passada, independentemente da assinatura declarada na importação. Depois que um módulo declara uma importação, o chamador de WebAssembly.instantiate()
deve passar um objeto de importação que tenha as propriedades correspondentes.
Para o exposto acima, precisamos de um objeto (vamos chamá-lo de importObject
) de modo que importObject.console.log
seja uma função JavaScript.
Isso ficaria parecido com o seguinte:
const importObject = {
console: {
log(arg) {
console.log(arg);
},
},
};
WebAssembly.instantiateStreaming(fetch("logger.wasm"), importObject).then(
(obj) => {
obj.instance.exports.logIt();
},
);
Nota: Você pode encontrar este exemplo no GitHub como logger.html (veja ao vivo também).
Declarando globais no WebAssembly
O WebAssembly tem a capacidade de criar instâncias de variáveis globais, acessíveis a partir de JavaScript e importáveis/exportáveis em uma ou mais instâncias WebAssembly.Module
. Isso é muito útil, pois permite a vinculação dinâmica de vários módulos.
No formato de texto WebAssembly, é mais ou menos assim (consulte global.wat em nosso repositório GitHub; consulte também global.html para uma transmissão ao vivo Exemplo de JavaScript):
(module
(global $g (import "js" "global") (mut i32))
(func (export "getGlobal") (result i32)
(global.get $g))
(func (export "incGlobal")
(global.set $g
(i32.add (global.get $g) (i32.const 1))))
)
Isso é semelhante ao que vimos antes, exceto que especificamos um valor global usando a palavra-chave global
e também especificamos a palavra-chave mut
junto com o tipo de dados do valor, se quisermos que ele seja mutável.
Para criar um valor equivalente usando JavaScript, você usaria o construtor WebAssembly.Global()
:
const global = new WebAssembly.Global({ value: "i32", mutable: true }, 0);
Memória WebAssembly
O exemplo acima é uma função de registro terrível: ela imprime apenas um único inteiro! E se quiséssemos registrar uma string de texto? Para lidar com strings e outros tipos de dados mais complexos, o WebAssembly fornece memory (embora também tenhamos tipos de referência na implementação mais recente do WebAssembly). De acordo com o WebAssembly, a memória é apenas uma grande matriz de bytes que pode crescer com o tempo. O WebAssembly contém instruções como i32.load
e i32.store
para leitura e gravação de memória linear.
Do ponto de vista do JavaScript, é como se a memória estivesse toda dentro de um grande ArrayBuffer
(redimensionável). Isso é literalmente tudo o que o asm.js tem para brincar (exceto que não é redimensionável; veja o asm.js modelo de programação).
Portanto, uma string é apenas uma sequência de bytes em algum lugar dentro dessa memória linear. Vamos supor que escrevemos uma string adequada de bytes na memória; como passamos essa string para JavaScript?
A chave é que o JavaScript pode criar instâncias de memória linear WebAssembly por meio da interface WebAssembly.Memory()
e acessar uma instância de memória existente (atualmente, você só pode ter uma por instância de módulo) usando os métodos de instância associados. As instâncias de memória têm um getter buffer
, que retorna um ArrayBuffer
que aponta para toda a memória linear.
As instâncias de memória também podem aumentar, por exemplo, por meio do método Memory.grow()
em JavaScript. Quando o crescimento ocorre, já que ArrayBuffer
s não podem mudar de tamanho, o ArrayBuffer
atual é separado e um novo ArrayBuffer
é criado para apontar para a memória maior e mais nova. Isso significa que tudo o que precisamos fazer para passar uma string para JavaScript é passar o deslocamento da string na memória linear junto com alguma forma de indicar o comprimento.
Embora existam muitas maneiras de codificar o comprimento de uma string na própria string (por exemplo, strings C); para simplificar aqui, apenas passamos o deslocamento e o comprimento como parâmetros:
(import "console" "log" (func $log (param i32) (param i32)))
No lado do JavaScript, podemos usar a API TextDecoder para decodificar facilmente nossos bytes em uma string JavaScript. (Especificamos utf8
aqui, mas muitas outras codificações são suportadas.)
function consoleLogString(offset, length) {
const bytes = new Uint8Array(memory.buffer, offset, length);
const string = new TextDecoder("utf8").decode(bytes);
console.log(string);
}
A última peça que falta do quebra-cabeça é onde consoleLogString
obtém acesso à memory
do WebAssembly. WebAssembly nos dá muita flexibilidade aqui: podemos criar um objeto Memory
em JavaScript e fazer com que o módulo WebAssembly importe a memória, ou podemos fazer com que o módulo WebAssembly crie a memória e exporte-a para JavaScript.
Para simplificar, vamos criá-lo em JavaScript e depois importá-lo para o WebAssembly. Nossa instrução import
é escrita da seguinte forma:
(import "js" "mem" (memory 1))
O 1
indica que a memória importada deve ter pelo menos 1 página de memória (o WebAssembly define uma página como 64KB).
Então vamos ver um módulo completo que imprime a string "Hi". Em um programa C compilado normal, você chamaria uma função para alocar um pouco de memória para a string, mas como estamos apenas escrevendo nosso próprio assembly aqui e possuímos toda a memória linear, podemos apenas escrever o conteúdo da string na memória global usando uma seção data
. As seções de dados permitem que uma sequência de bytes seja escrita em um determinado deslocamento no tempo de instanciação e são semelhantes às seções .data
em formatos executáveis nativos.
Nosso módulo Wasm final se parece com isso:
(module
(import "console" "log" (func $log (param i32 i32)))
(import "js" "mem" (memory 1))
(data (i32.const 0) "Hi")
(func (export "writeHi")
i32.const 0 ;; pass offset 0 to log
i32.const 2 ;; pass length 2 to log
call $log))
Nota:
Acima, observe a sintaxe de ponto e vírgula dupla (;;
) para permitir comentários em arquivos WebAssembly.
Agora, a partir do JavaScript, podemos criar uma memória com 1 página e passá-la. Isso resulta em "Hi" sendo impresso no console:
const memory = new WebAssembly.Memory({ initial: 1 });
const importObject = {
console: { log: consoleLogString },
js: { mem: memory },
};
WebAssembly.instantiateStreaming(fetch("logger2.wasm"), importObject).then(
(obj) => {
obj.instance.exports.writeHi();
},
);
Nota: Você pode encontrar a fonte completa no GitHub como logger2.html (também veja ao vivo).
Tabelas WebAssembly
Para terminar este tour pelo formato de texto do WebAssembly, vamos dar uma olhada na parte mais complexa e muitas vezes confusa do WebAssembly: tables. As tabelas são basicamente matrizes redimensionáveis de referências que podem ser acessadas por índice do código WebAssembly.
Para ver por que as tabelas são necessárias, precisamos primeiro observar que a instrução call
que vimos anteriormente (consulte Chamando funções de outras funções no mesmo módulo) usa um índice de função estático e, portanto, só pode chamar uma função - mas e se o callee for um valor de tempo de execução?
- Em JavaScript, vemos isso o tempo todo: funções são valores de primeira classe.
- Em C/C++, vemos isso com ponteiros de função.
- Em C++, vemos isso com funções virtuais.
O WebAssembly precisava de um tipo de instrução de chamada para conseguir isso, então demos a ela call_indirect
, que recebe um operando de função dinâmica. O problema é que os únicos tipos que temos para dar operandos no WebAssembly são (atualmente) i32
/i64
/f32
/f64
.
WebAssembly poderia adicionar um tipo anyfunc
("qualquer" porque o tipo poderia conter funções de qualquer assinatura), mas infelizmente este tipo anyfunc
não poderia ser armazenado na memória linear por razões de segurança. A memória linear expõe o conteúdo bruto de valores armazenados como bytes e isso permitiria que o conteúdo do Wasm observasse e corrompesse arbitrariamente os endereços de funções brutas, algo que não pode ser permitido na web.
A solução foi armazenar as referências de função em uma tabela e, em vez disso, passar os índices da tabela, que são apenas valores i32. O operando de call_indirect
pode, portanto, ser um valor de índice i32.
Definindo uma tabela no Wasm
Então, como colocamos as funções Wasm em nossa tabela? Assim como as seções data
podem ser usadas para inicializar regiões de memória linear com bytes, as seções elem
podem ser usadas para inicializar regiões de tabelas com funções:
(module
(table 2 funcref)
(elem (i32.const 0) $f1 $f2)
(func $f1 (result i32)
i32.const 42)
(func $f2 (result i32)
i32.const 13)
...
)
- Em
(table 2 funcref)
, o 2 é o tamanho inicial da tabela (ou seja, irá armazenar duas referências) efuncref
declara que o tipo de elemento dessas referências é referência de função. - As seções de funções (
func
) são como quaisquer outras funções declaradas do Wasm. Estas são as funções às quais vamos nos referir em nossa tabela (por exemplo, cada uma apenas retorna um valor constante). Observe que a ordem em que as seções são declaradas não importa aqui — você pode declarar suas funções em qualquer lugar e ainda se referir a elas em sua seçãoelem
. - A seção
elem
pode listar qualquer subconjunto das funções em um módulo, em qualquer ordem, permitindo duplicatas. Esta é uma lista das funções que devem ser referenciadas pela tabela, na ordem em que devem ser referenciadas. - O valor
(i32.const 0)
dentro da seçãoelem
é um deslocamento — isso precisa ser declarado no início da seção e especifica em qual índice na tabela as referências de função começam a ser preenchidas. Aqui especificamos 0 e um tamanho de 2 (veja acima), para que possamos preencher duas referências nos índices 0 e 1. Se quiséssemos começar a escrever nossas referências no deslocamento 1, teríamos que escrever(i32.const 1)
, e o tamanho da tabela teria que ser 3.
Nota: Os elementos não inicializados recebem um valor padrão de ativação.
Em JavaScript, as chamadas equivalentes para criar tal instância de tabela seriam mais ou menos assim:
function () {
// seção da tabela
const tbl = new WebAssembly.Table({initial: 2, element: "anyfunc"});
// seções de função:
const f1 = ... /* alguma função WebAssembly importada */
const f2 = ... /* alguma função WebAssembly importada */
// seção de elementos
tbl.set(0, f1);
tbl.set(1, f2);
};
Usando a tabela
Seguindo em frente, agora que definimos a tabela, precisamos usá-la de alguma forma. Vamos usar esta seção de código para fazer isso:
(type $return_i32 (func (result i32))) ;; se fosse f32, a verificação de tipo falharia
(func (export "callByIndex") (param $i i32) (result i32)
local.get $i
call_indirect (type $return_i32))
- O bloco
(type $return_i32 (func (result i32)))
especifica um tipo, com um nome de referência. Esse tipo é usado ao executar a verificação de tipo das chamadas de referência de função de tabela posteriormente. Aqui estamos dizendo que as referências precisam ser funções que retornam umi32
como resultado. - Em seguida, definimos uma função que será exportada com o nome
callByIndex
. Isso levará umi32
como parâmetro, que recebe o nome de argumento$i
. - Dentro da função, adicionamos um valor à pilha — qualquer valor que seja passado como parâmetro
$i
. - Por fim, usamos
call_indirect
para chamar uma função da tabela — ela extrai implicitamente o valor de$i
da pilha. O resultado líquido disso é que a funçãocallByIndex
invoca a função$i
'th na tabela.
Você também pode declarar o parâmetro call_indirect
explicitamente durante a chamada do comando em vez de antes dele, assim:
(call_indirect (type $return_i32) (local.get $i))
Em uma linguagem mais expressiva e de nível superior, como JavaScript, você pode imaginar fazer a mesma coisa com uma matriz (ou provavelmente, mais provavelmente, um objeto) contendo funções. O pseudocódigo seria algo como tbl[i]()
.
Então, de volta à verificação de tipos. Como o WebAssembly é verificado quanto ao tipo e o funcref
pode ser potencialmente qualquer assinatura de função, temos que fornecer a assinatura presumida do callee no callsite, portanto, incluímos o tipo $return_i32
, para informar ao programa que uma função que retorna um i32
é esperada. Se o receptor não tiver uma assinatura correspondente (digamos que um f32
seja retornado), um WebAssembly.RuntimeError
será lançado.
Então, o que liga call_indirect
à tabela que estamos chamando? A resposta é que há apenas uma tabela permitida agora por instância do módulo, e é isso que call_indirect
está chamando implicitamente. No futuro, quando várias tabelas forem permitidas, também precisaremos especificar um identificador de tabela de algum tipo, nos moldes de
call_indirect $my_spicy_table (type $i32_to_void)
O módulo completo se parece com isso e pode ser encontrado em nosso arquivo de exemplo wasm-table.wat:
(module
(table 2 funcref)
(func $f1 (result i32)
i32.const 42)
(func $f2 (result i32)
i32.const 13)
(elem (i32.const 0) $f1 $f2)
(type $return_i32 (func (result i32)))
(func (export "callByIndex") (param $i i32) (result i32)
local.get $i
call_indirect (type $return_i32))
)
Nós o carregamos em uma página da Web usando o seguinte JavaScript:
WebAssembly.instantiateStreaming(fetch("wasm-table.wasm")).then((obj) => {
console.log(obj.instance.exports.callByIndex(0)); // retorna 42
console.log(obj.instance.exports.callByIndex(1)); // retorna 13
console.log(obj.instance.exports.callByIndex(2)); // retorna um erro, porque não há posição de índice 2 na tabela
});
Nota: Você pode encontrar este exemplo no GitHub como wasm-table.html (veja ao vivo também).
Nota:
Assim como a memória, as tabelas também podem ser criadas a partir de JavaScript (consulte WebAssembly.Table()
) bem como importadas de/para outro módulo Wasm.
Tabelas mutantes e links dinâmicos
Como o JavaScript tem acesso total às referências de função, o objeto Tabela pode sofrer mutação do JavaScript usando os métodos grow()
, get()
e set()
. E o próprio código WebAssembly é capaz de manipular tabelas usando instruções adicionadas como parte de Tipos de referência, como table.get
e table.set
.
Como as tabelas são mutáveis, elas podem ser usadas para implementar [esquemas de vinculação dinâmica] sofisticados de tempo de carregamento e tempo de execução (https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md). Quando um programa é vinculado dinamicamente, várias instâncias compartilham a mesma memória e tabela. Isso é simétrico para um aplicativo nativo onde vários .dll
s compilados compartilham o espaço de endereço de um único processo.
Para ver isso em ação, criaremos um único objeto de importação contendo um objeto de memória e um objeto de tabela e passaremos esse mesmo objeto de importação para várias chamadas instantiate()
.
Nossos exemplos de .wat
são assim:
shared0.wat
:
(module
(import "js" "memory" (memory 1))
(import "js" "table" (table 1 funcref))
(elem (i32.const 0) $shared0func)
(func $shared0func (result i32)
i32.const 0
i32.load)
)
shared1.wat
:
(module
(import "js" "memory" (memory 1))
(import "js" "table" (table 1 funcref))
(type $void_to_i32 (func (result i32)))
(func (export "doIt") (result i32)
i32.const 0
i32.const 42
i32.store ;; store 42 at address 0
i32.const 0
call_indirect (type $void_to_i32))
)
Estes funcionam da seguinte forma:
- A função
shared0func
é definida emshared0.wat
e armazenada em nossa tabela importada. - Esta função cria uma constante contendo o valor
0
, e então usa o comandoi32.load
para carregar o valor contido no índice de memória fornecido. O índice fornecido é0
— novamente, ele extrai implicitamente o valor anterior da pilha. Entãoshared0func
carrega e retorna o valor armazenado no índice de memória0
. - Em
shared1.wat
, exportamos uma função chamadadoIt
— esta função cria duas constantes contendo os valores0
e42
, então chamai32.store
para armazenar um valor fornecido em um índice fornecido da memória importada. Novamente, ele extrai implicitamente esses valores da pilha, então o resultado é que ele armazena o valor42
no índice de memória0
, - Na última parte da função, criamos uma constante com valor
0
, então chamamos a função neste índice 0 da tabela, que éshared0func
, armazenado lá anteriormente pelo blocoelem
emshared0.wat
. - Quando chamado,
shared0func
carrega o42
que armazenamos na memória usando o comandoi32.store
emshared1.wat
.
Nota: As expressões acima novamente extraem valores da pilha implicitamente, mas você pode declará-los explicitamente dentro das chamadas de comando, por exemplo:
(i32.store (i32.const 0) (i32.const 42))
(call_indirect (type $void_to_i32) (i32.const 0))
Depois de converter para assembly, usamos shared0.wasm
e shared1.wasm
em JavaScript por meio do seguinte código:
const importObj = {
js: {
memory: new WebAssembly.Memory({ initial: 1 }),
table: new WebAssembly.Table({ initial: 1, element: "anyfunc" }),
},
};
Promise.all([
WebAssembly.instantiateStreaming(fetch("shared0.wasm"), importObj),
WebAssembly.instantiateStreaming(fetch("shared1.wasm"), importObj),
]).then((results) => {
console.log(results[1].instance.exports.doIt()); // prints 42
});
Cada um dos módulos que está sendo compilado pode importar a mesma memória e objetos de tabela e, assim, compartilhar a mesma memória linear e "espaço de endereçamento" de tabela.
Nota: Você pode encontrar este exemplo no GitHub como shared-address-space.html (veja ao vivo também).
Operações de memória em massa
As operações de memória em massa são uma adição mais recente à linguagem (por exemplo, no Firefox 79) — sete novas operações incorporadas são fornecidas para operações de memória em massa, como cópia e inicialização, para permitir que o WebAssembly modele funções nativas como memcpy
e memmove
de maneira mais eficiente e com melhor desempenho.
As novas operações são:
data.drop
: Descarta os dados em um segmento de dados.elem.drop
: Descarta os dados em um segmento de elemento.memory.copy
: Copia de uma região da memória linear para outra.memory.fill
: Preenche uma região de memória linear com um determinado valor de byte.memory.init
: Copia uma região de um segmento de dados.table.copy
: Copia de uma região de uma tabela para outra.table.init
: Copia uma região de um segmento de elemento.
Nota: Você pode encontrar mais informações na proposta Operações de memória em massa e inicialização de segmento condicional.
Tipos
Tipos de números
Atualmente, o WebAssembly tem quatro number types disponíveis:
i32
: inteiro de 32 bitsi64
: inteiro de 64 bitsf32
: flutuante de 32 bitsf64
: flutuante de 64 bits
Tipos de vetores
v128
: vetor de 128 bits de inteiro compactado, dados de ponto flutuante ou um único tipo de 128 bits.
Tipos de referência
A proposta de tipos de referência (compatível com Firefox 79) fornece dois recursos principais:
- Um novo tipo,
externref
, que pode conter qualquer valor JavaScript, por exemplo, strings, referências DOM, objetos, etc.externref
é opaco do ponto de vista do WebAssembly — um módulo Wasm não pode acessar e manipular esses valores e, em vez disso, pode apenas recebê-los e passá-los de volta. Mas isso é muito útil para permitir que os módulos Wasm chamem funções JavaScript, APIs DOM, etc., e geralmente para preparar o caminho para uma interoperabilidade mais fácil com o ambiente host.externref
pode ser usado para tipos de valor e elementos de tabela. - Uma série de novas instruções que permitem aos módulos Wasm manipular diretamente tabelas WebAssembly, em vez de ter que fazer isso por meio da API JavaScript.
Nota:
A documentação wasm-bindgen contém algumas informações úteis sobre como aproveitar externref
do Rust.
WebAssembly de vários valores
Outra adição mais recente à linguagem (por exemplo, no Firefox 78) é o valor múltiplo do WebAssembly, o que significa que as funções do WebAssembly agora podem retornar vários valores e as sequências de instruções podem consumir e produzir vários valores de pilha.
No momento da redação deste artigo (junho de 2020), isso está em um estágio inicial e as únicas instruções de vários valores disponíveis são chamadas para funções que retornam vários valores. Por exemplo:
(module
(func $get_two_numbers (result i32 i32)
i32.const 1
i32.const 2
)
(func (export "add_two_numbers") (result i32)
call $get_two_numbers
i32.add
)
)
Mas isso abrirá caminho para tipos de instruções mais úteis e outras coisas além. Para uma descrição útil do progresso até agora e como isso funciona, consulte Multi-Value All The Wasm! por Nick Fitzgerald.
Threads WebAssembly
WebAssembly Threads (compatível com Firefox 79 em diante) permite que objetos WebAssembly Memory sejam compartilhados entre várias instâncias WebAssembly em execução em Web Workers separados, da mesma forma que SharedArrayBuffer
s em JavaScript. Isso permite uma comunicação muito rápida entre os trabalhadores e ganhos significativos de desempenho em aplicativos da web.
A proposta de threads tem duas partes, memórias compartilhadas e acessos à memória atômica.
Memórias compartilhadas
Conforme descrito acima, você pode criar objetos WebAssembly Memory
compartilhados, que podem ser transferidos entre os contextos Window e Worker usando postMessage()
, da mesma forma que um [SharedArrayBuffer
](/pt-BR/docs/Web/JavaScript/ Referência/Global_Objects/SharedArrayBuffer).
No lado da API JavaScript, o objeto de inicialização do construtor WebAssembly.Memory()
agora tem uma propriedade shared
, que quando definida como true
criará uma memória compartilhada:
const memory = new WebAssembly.Memory({
initial: 10,
maximum: 100,
shared: true,
});
a propriedade buffer
da memória agora retornará um SharedArrayBuffer
, em vez do ArrayBuffer
usual:
memory.buffer; // retorna SharedArrayBuffer
No formato de texto, você pode criar uma memória compartilhada usando a palavra-chave shared
, assim:
(memory 1 2 shared)
Ao contrário das memórias não compartilhadas, as memórias compartilhadas devem especificar um tamanho "máximo", tanto no construtor da API JavaScript quanto no formato de texto Wasm.
Nota: Você pode encontrar muito mais detalhes na Proposta de encadeamento para WebAssembly.
Acessos à memória atômica
Várias novas instruções Wasm foram adicionadas e podem ser usadas para implementar recursos de nível superior, como mutexes, variáveis de condição etc. Estas instruções são permitidas em memórias não compartilhadas a partir do Firefox 80.
Nota: A página de suporte do Emscripten Pthreads mostra como aproveitar essa nova funcionalidade do Emscripten.
Resumo
Isso encerra nosso tour de alto nível pelos principais componentes do formato de texto WebAssembly e como eles são refletidos na API WebAssembly JS.
Veja também
- A principal coisa que não foi incluída é uma lista abrangente de todas as instruções que podem ocorrer em corpos de função. Veja a semântica do WebAssembly para um tratamento de cada instrução.
- Veja também a gramática do formato de texto que é implementada pelo interpretador de especificações.