Aprenda a incorporar 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 maneira de chamar APIs externas, como APIs da Web e bibliotecas de terceiros. Você precisa de uma maneira de armazenar os valores e as instâncias de objeto retornados por essas APIs e uma maneira de transmitir esses valores armazenados para outras APIs mais tarde. Para APIs assíncronas, talvez você também precise aguardar promessas no código C/C++ síncrono com o Asyncify e ler o resultado quando a operação for concluída.
O Emscripten oferece várias ferramentas para essas interações:
emscripten::val
para armazenar e operar em valores JavaScript em C++.EM_JS
para incorporar snippets JavaScript e vincular como funções C/C++.EM_ASYNC_JS
semelhante aEM_JS
, mas que facilita a incorporação de snippets de JavaScript assíncronos.EM_ASM
para incorporar snippets curtos e executá-los inline, 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.
Neste post, você vai aprender a usar todos eles para tarefas semelhantes.
Classe emscripten::val
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 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 em val
precisa realizar as seguintes etapas:
- Converta valores C++ transmitidos como argumentos em algum formato intermediário.
- Acesse o JavaScript, leia e converta argumentos em valores JavaScript.
- Executar a função
- Converte o resultado do JavaScript para o formato intermediário.
- O resultado convertido é retornado ao C++, e o C++ o lê.
Cada await()
também precisa pausar o lado do C++ desdobrando toda a pilha de chamadas do módulo do WebAssembly, retornando ao JavaScript, aguardando e restaurando a pilha do 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 um driver para uma série de operações JavaScript. E se você pudesse mover fetch_json
para JavaScript e reduzir a sobrecarga das etapas intermediárias ao mesmo tempo?
Macro EM_JS
O EM_JS macro
permite mover fetch_json
para o JavaScript. EM_JS
no Emscripten permite declarar uma função C/C++ implementada por um snippet de JavaScript.
Assim como a WebAssembly, ela tem a limitação de oferecer suporte apenas a argumentos numéricos e valores de retorno. Para transmitir outros valores, é necessário convertê-los manualmente usando as APIs correspondentes. Veja alguns exemplos.
Não é necessário converter os números:
// 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 para e do JavaScript, é necessário usar as funções de conversão e alocação correspondentes de 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 valores mais complexos e arbitrários, use a API JavaScript para a classe val
mencionada anteriormente. Com ele, é possível converter valores do JavaScript e classes C++ em identificadores intermediários e vice-versa:
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
pode 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 é código JavaScript normal. Ao contrário do equivalente val
, agora ele pode ser otimizado pelo mecanismo JavaScript e só requer pausar o lado C++ uma vez para todas as operações assíncronas.
Macro EM_ASYNC_JS
O único pedaço que não parece muito bom é o wrapper Asyncify.handleAsync
. O único objetivo dele é permitir a execução de funções JavaScript async
com o Asyncify. Na verdade, esse caso de uso é tão comum que agora há uma macro EM_ASYNC_JS
especializada que os combina.
Confira como usá-lo para produzir a versão final do exemplo 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 JavaScript. Ele é eficiente porque vincula os snippets declarados diretamente, como qualquer outra importação de função JavaScript. Ele também oferece boa ergonomia, permitindo que você declare explicitamente todos os tipos e nomes de parâmetros.
No entanto, em alguns casos, você quer inserir um snippet rápido para a chamada console.log
, uma instrução debugger;
ou algo semelhante e não quer se preocupar em declarar uma função 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 à EM_JS
, mas executam o código inline onde são inseridas, em vez de definir uma função.
Como eles não declaram um protótipo de função, eles precisam de uma maneira diferente de especificar o tipo de retorno e acessar argumentos.
É necessário usar o nome correto da macro para escolher o tipo de retorno. Os blocos EM_ASM
devem funcionar 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.
Todos os argumentos transmitidos vão estar disponíveis com os nomes $0
, $1
e assim por diante no corpo do JavaScript. Assim como no EM_JS
ou na WebAssembly em geral, os argumentos são limitados apenas a valores numéricos, como números inteiros, de ponto flutuante, ponteiros e identificadores.
Confira um exemplo de como usar uma macro EM_ASM
para registrar um valor arbitrário do 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 oferece suporte à declaração de código JavaScript em um arquivo separado em um formato de biblioteca personalizado:
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
Em seguida, é necessário declarar manualmente os protótipos correspondentes 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 pelo --js-library option
, conectando protótipos às implementações JavaScript correspondentes.
No entanto, esse formato de módulo não é padrão e requer anotações de dependência cuidadosas. Por isso, ele é reservado principalmente para cenários avançados.
Conclusão
Neste post, analisamos várias maneiras de integrar o código JavaScript ao C++ ao trabalhar com o WebAssembly.
A inclusão desses snippets permite expressar longas sequências de operações de uma maneira mais limpa e eficiente, além de usar bibliotecas de terceiros, novas APIs JavaScript e até mesmo recursos de sintaxe JavaScript que ainda não podem ser expressos em C++ ou Embind.