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 é:
- 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).
- 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âmetroi32.(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:
- Carregamos o arquivo wasm (com
fetch). - Preparamos os imports que o WASM espera (namespace
"env"com a função"log"). - Instanciamos o módulo com
WebAssembly.instantiate(...). - Chamamos a função exportada (
call_log), que internamente usa a funçãologque 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:
- Declarar no código WASM a função como um import com a assinatura correta.
- Passar essa função real do lado do JS na hora de instanciar.