Aprenda a incorporar o código JavaScript na sua biblioteca WebAssembly para se comunicar com o mundo exterior.
Ao trabalhar na integração do WebAssembly com a Web, você precisa de uma forma de chamar APIs externas, como APIs da Web e bibliotecas de terceiros. Em seguida, você precisa de uma maneira de armazenar os valores e as instâncias de objeto que essas APIs retornam e uma maneira de passar esses valores armazenados para outras APIs posteriormente. Para APIs assíncronas, talvez seja necessário aguardar promessas no código C/C++ síncrono com Asyncify e ler o resultado quando a operação for concluída.
A Emscripten fornece várias ferramentas para essas interações:
emscripten::val
para armazenar e operar valores JavaScript em C++.EM_JS
para incorporar snippets de JavaScript e vinculá-los como funções C/C++.EM_ASYNC_JS
, que é semelhante aEM_JS
, mas facilita a incorporação de snippets JavaScript assíncronos.EM_ASM
para incorporar snippets curtos e executá-los in-line, sem declarar uma função.--js-library
para cenários avançados em que você quer declarar muitas funções JavaScript juntas como uma única biblioteca.
Nesta postagem, você vai aprender a usar todos eles para tarefas semelhantes.
emscripten::classe de valor
A classe emcripten::val
é fornecida pelo Embind. Ele pode invocar APIs globais, vincular valores JavaScript a instâncias C++ e converter valores entre tipos C++ e JavaScript.
Confira como usá-lo com o .await()
do Asyncify para buscar e analisar um código JSON:
#include <emscripten/val.h>
using namespace emscripten;
val fetch_json(const char *url) {
// Get and cache a binding to the global `fetch` API in each thread.
thread_local const val fetch = val::global("fetch");
// Invoke fetch and await the returned `Promise<Response>`.
val response = fetch(url).await();
// Ask to read the response body as JSON and await the returned `Promise<any>`.
val json = response.call<val>("json").await();
// Return the JSON object.
return json;
}
// Example URL.
val example_json = fetch_json("https://httpbin.org/json");
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
Esse código funciona bem, mas executa muitas etapas intermediárias. Cada operação no val
precisa executar as seguintes etapas:
- Converta valores C++ transmitidos como argumentos em algum formato intermediário.
- Vá para JavaScript, leia e converta argumentos em valores JavaScript.
- Executar a função
- Converta o resultado de JavaScript para o formato intermediário.
- Retorne o resultado convertido para o C++, e o C++ finalmente vai lê-lo de volta.
Cada await()
também precisa pausar o lado C++ desenrolando toda a pilha de chamadas do módulo WebAssembly, retornando ao JavaScript, aguardando e restaurando a pilha WebAssembly quando a operação for concluída.
Esse código não precisa de nada do C++. O código C++ atua apenas como driver de uma série de operações JavaScript. E se você pudesse migrar a fetch_json
para JavaScript e reduzir a sobrecarga de etapas intermediárias ao mesmo tempo?
Macro EM_JS
O EM_JS macro
permite mover o fetch_json
para JavaScript. EM_JS
no Emscripten permite que você declare uma função C/C++ implementada por um snippet de JavaScript.
Como o próprio WebAssembly, ele tem a limitação de suporte apenas a argumentos numéricos e valores de retorno. Para transmitir outros valores, é necessário convertê-los manualmente usando APIs correspondentes. Veja alguns exemplos.
A transmissão de números não requer conversão:
// Passing numbers, doesn't need any conversion.
EM_JS(int, add_one, (int x), {
return x + 1;
});
int x = add_one(41);
Ao transmitir strings de e para JavaScript, é preciso usar as funções de conversão e alocação correspondentes do preamble.js:
EM_JS(void, log_string, (const char *msg), {
console.log(UTF8ToString(msg));
});
EM_JS(const char *, get_input, (), {
let str = document.getElementById('myinput').value;
// Returns heap-allocated string.
// C/C++ code is responsible for calling `free` once unused.
return allocate(intArrayFromString(str), 'i8', ALLOC_NORMAL);
});
Por fim, para tipos de valor mais complexos e arbitrários, é possível usar a API JavaScript para a classe val
mencionada anteriormente. Ao usá-lo, você pode converter valores JavaScript e classes C++ em identificadores intermediários e retorno:
EM_JS(void, log_value, (EM_VAL val_handle), {
let value = Emval.toValue(val_handle);
console.log(value);
});
EM_JS(EM_VAL, find_myinput, (), {
let input = document.getElementById('myinput');
return Emval.toHandle(input);
});
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
log_value(obj.as_handle()); // logs { x: 1, y: 2 }
val myinput = val::take_ownership(find_input());
// Now you can store the `find_myinput` DOM element for as long as you like, and access it later like:
std::string value = input["value"].as<std::string>();
Com essas APIs em mente, o exemplo fetch_json
poderia ser reescrito para fazer a maior parte do trabalho sem sair do JavaScript:
EM_JS(EM_VAL, fetch_json, (const char *url), {
return Asyncify.handleAsync(async () => {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
Ainda temos algumas conversões explícitas nos pontos de entrada e saída da função, mas o restante é agora um código JavaScript normal. Ao contrário do equivalente ao val
, agora ele pode ser otimizado pelo mecanismo JavaScript e exige apenas que você pause o lado C++ uma vez para todas as operações assíncronas.
Macro EM_ASYNC_JS
O único bit restante que não está bonito é o wrapper Asyncify.handleAsync
. O único objetivo dele é permitir a execução de funções JavaScript async
com o Async. Na verdade, esse caso de uso é tão comum que agora existe uma macro EM_ASYNC_JS
especializada que os combina.
Veja como usá-lo para produzir a versão final do exemplo de fetch
:
EM_ASYNC_JS(EM_VAL, fetch_json, (const char *url), {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
EM_ASM
EM_JS
é a maneira recomendada de declarar snippets de JavaScript. Ela é eficiente porque vincula os snippets declarados diretamente como qualquer outra importação de função JavaScript. Ele também oferece uma boa ergonomia, permitindo que você declare explicitamente todos os tipos e nomes de parâmetros.
Em alguns casos, no entanto, você quer inserir um snippet rápido para a chamada console.log
, uma instrução debugger;
ou algo semelhante e não quer declarar uma função totalmente separada. Nesses casos raros, um EM_ASM macros family
(EM_ASM
, EM_ASM_INT
e EM_ASM_DOUBLE
) pode ser uma opção mais simples. Essas macros são semelhantes à macro EM_JS
, mas executam o código inline no local em que são inseridas, em vez de definir uma função.
Como eles não declaram um protótipo de função, precisam de uma maneira diferente de especificar o tipo de retorno e acessar argumentos.
É preciso usar o nome correto da macro para escolher o tipo de retorno. Espera-se que os blocos EM_ASM
atuem como funções void
, os blocos EM_ASM_INT
podem retornar um valor inteiro e os blocos EM_ASM_DOUBLE
retornam números de ponto flutuante correspondentes.
Todos os argumentos transmitidos vão ficar disponíveis com os nomes $0
, $1
e assim por diante no corpo do JavaScript. Assim como acontece com EM_JS
ou WebAssembly, em geral, os argumentos são limitados apenas a valores numéricos: inteiros, números de ponto flutuante, ponteiros e identificadores.
Confira um exemplo de como usar uma macro EM_ASM
para registrar um valor arbitrário de JS no console:
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
// executes inline immediately
EM_ASM({
// convert handle passed under $0 into a JavaScript value
let obj = Emval.fromHandle($0);
console.log(obj); // logs { x: 1, y: 2 }
}, obj.as_handle());
--js-library
Por fim, o Emscripten permite a declaração do código JavaScript em um arquivo separado em um formato de biblioteca próprio:
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
Em seguida, é necessário declarar os protótipos correspondentes manualmente no lado do C++:
extern "C" void log_value(EM_VAL val_handle);
Depois de declarada em ambos os lados, a biblioteca JavaScript pode ser vinculada ao código principal por meio de --js-library option
, conectando protótipos às implementações JavaScript correspondentes.
No entanto, o formato desse módulo não é padrão e requer anotações de dependência cuidadosas. Por isso, ela é reservada para cenários avançados.
Conclusão
Nesta postagem, vimos várias maneiras de integrar o código JavaScript em C++ ao trabalhar com o WebAssembly.
A inclusão desses snippets permite que você expresse longas sequências de operações de maneira mais limpa e eficiente, além de explorar bibliotecas de terceiros, novas APIs JavaScript e até mesmo recursos de sintaxe JavaScript que ainda não são expressas por C++ ou Embind.