Skip to main content

Javascript | Web Assembly

  • Javascript | Web Assembly

Importando funções

Para “passar” funções JavaScript para dentro do WebAssembly (WASM), basicamente precisamos importar essas funções no módulo WASM, de modo que o código em WASM possa chamá-las. A ideia geral é:

  1. Definir, no lado do WASM, as funções que serão esperadas como import (ou seja, funções “vindo de fora”, de outro módulo ou do JS).
  2. Fornecer, no lado do JavaScript, as implementações concretas das funções que o WASM vai chamar.

Abaixo vou mostrar um passo a passo simplificado usando o JavaScript puro (sem frameworks como Emscripten).


1. Criar (ou visualizar) o código WASM que espera uma função de import

Suponha que tenhamos um módulo WebAssembly (pode ser em formato texto (*.wat) ou binário (*.wasm)) que deseja chamar uma função log para imprimir um valor. Exemplo em WAT (WebAssembly Text Format):

(module
;; 1. Importamos uma função chamada `log` do namespace "env"
(import "env" "log" (func $log (param i32)))

;; 2. Exportamos uma função `call_log` para chamar a função importada `log`
(func $call_log (export "call_log") (param $value i32)
;; chamamos a função importada $log passando $value
local.get $value
call $log
)
)

Explicando:

  • (import "env" "log" (func $log (param i32))): diz que o WASM espera que, no namespace "env", exista uma função chamada "log" que receba um parâmetro i32.
  • (func $call_log (export "call_log") ...): cria uma função exportada para ser chamada externamente (pelo JS), que por sua vez chama a função importada $log.

Se você já tem o *.wasm compilado, basta ter certeza de que ele está configurado para esperar uma função de import (com os nomes corretos de namespace e função).


2. Carregar e instanciar o módulo WASM no JavaScript

Vamos supor que o arquivo *.wasm se chame module.wasm (compilado a partir do exemplo acima). No JavaScript, podemos usar fetch e WebAssembly.instantiateStreaming (no navegador) ou WebAssembly.instantiate (em Node.js com fs.readFile, por exemplo).

async function loadWasm() {
const response = await fetch('module.wasm');
const buffer = await response.arrayBuffer();

// Aqui criamos as "imports" que o WASM espera
const imports = {
// "env" é o namespace que definimos lá no WASM:
env: {
// A função "log" que o WASM espera
log: (value) => {
console.log("Valor vindo do WASM:", value);
},
},
};

// Instanciamos o módulo
const { instance } = await WebAssembly.instantiate(buffer, imports);

return instance;
}

// Exemplo de uso:
loadWasm().then((wasmInstance) => {
// Pega a função exportada pelo WASM: `call_log`
const callLog = wasmInstance.exports.call_log;

// Chamamos, passando algum valor
callLog(42);
});

O que acontece aqui:

  1. Carregamos o arquivo wasm (com fetch).
  2. Preparamos os imports que o WASM espera (namespace "env" com a função "log").
  3. Instanciamos o módulo com WebAssembly.instantiate(...).
  4. Chamamos a função exportada (call_log), que internamente usa a função log que passamos pelo import.

3. Chamando funções JS mais complexas

Para funções mais elaboradas, você precisa se lembrar das seguintes limitações e passos:

  • O WebAssembly lida basicamente com tipos primitivos (i32, i64, f32, f64). Então, se você quiser passar strings, arrays ou objetos JavaScript diretamente, é preciso criar uma camada de “marshalling” (conversão) entre os dados em memória WASM e os dados em JS.
  • Para “guardar” uma função JS e chamá-la como se fosse um ponteiro de função em C/C++, você pode usar artifícios (no Emscripten, por exemplo, existe addFunction, que converte uma função JS em um ponteiro que o WASM pode chamar).
  • Novos recursos de referência de função (func ref) no WebAssembly estão evoluindo, mas ainda não são largamente suportados em todos os browsers.

No geral, para a maioria dos casos, basta:

  1. Declarar no código WASM a função como um import com a assinatura correta.
  2. Passar essa função real do lado do JS na hora de instanciar.