Scopri come incorporare il codice JavaScript nella libreria WebAssembly per comunicare con il mondo esterno.
Quando lavori all'integrazione di WebAssembly con il web, devi trovare un modo per richiamare le API esterne, come le API web e le librerie di terze parti. Devi quindi avere un modo per archiviare i valori e le istanze di oggetti restituiti da queste API e un modo per trasmettere in un secondo momento i valori archiviati ad altre API. Per le API asincrone, potrebbe anche essere necessario attendere le promesse nel codice C/C++ sincrono con Asyncify e leggere il risultato al termine dell'operazione.
Emscripten fornisce diversi strumenti per queste interazioni:
emscripten::val
per memorizzare e operare sui valori JavaScript in C++.EM_JS
per incorporare snippet JavaScript e associarli come funzioni C/C++.EM_ASYNC_JS
simile aEM_JS
, ma che semplifica l'inserimento di snippet JavaScript asincroni.EM_ASM
per incorporare snippet brevi ed eseguirli in linea, senza dichiarare una funzione.--js-library
per scenari avanzati in cui vuoi dichiarare molte funzioni JavaScript insieme come una singola libreria.
In questo post scoprirai come utilizzarli tutti per attività simili.
emscripten::val classe
La classe emcripten::val
è fornita da Embind. Può richiamare API globali, associare valori JavaScript a istanze C++ e convertire valori tra tipi C++ e JavaScript.
Ecco come utilizzarlo con .await()
di Asyncify per recuperare e analizzare alcuni file 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>();
Questo codice funziona bene, ma esegue molti passaggi intermedi. Ogni operazione su val
deve eseguire i seguenti passaggi:
- Converte i valori C++ passati come argomenti in un formato intermedio.
- Vai a JavaScript, leggi e converti gli argomenti in valori JavaScript.
- Esegui la funzione
- Converti il risultato da JavaScript in formato intermedio.
- Restituire il risultato convertito in C++ e C++ infine lo leggerà.
Ogni await()
deve inoltre mettere in pausa il lato C++ sbloccando l'intero stack di chiamate del modulo WebAssembly, tornando a JavaScript, attendendo e ripristinando lo stack WebAssembly al termine dell'operazione.
Questo codice non ha bisogno di nulla da C++. Il codice C++ funge solo da driver per una serie di operazioni JavaScript. E se potessi spostare fetch_json
in JavaScript e ridurre contemporaneamente l'overhead dei passaggi intermedi?
Macro EM_JS
EM_JS macro
ti consente di spostare fetch_json
in JavaScript. EM_JS
in Emscripten ti consente di dichiarare una funzione C/C++ implementata da uno snippet JavaScript.
Come WebAssembly stesso, presenta un limite nel supportare solo argomenti numerici e valori restituiti. Per trasmettere altri valori, devi convertirli manualmente tramite le API corrispondenti. Di seguito sono riportati alcuni esempi.
La trasmissione di numeri non richiede alcuna conversione:
// Passing numbers, doesn't need any conversion.
EM_JS(int, add_one, (int x), {
return x + 1;
});
int x = add_one(41);
Quando passi stringhe a e da JavaScript, devi utilizzare le funzioni di conversione e allocazione corrispondenti di 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);
});
Infine, per tipi di valori più complessi e arbitrari, puoi utilizzare l'API JavaScript per la classe val
menzionata in precedenza. Con questo metodo, puoi convertire i valori JavaScript e le classi C++ in handle intermedi e viceversa:
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>();
Tenendo presente queste API, l'esempio fetch_json
potrebbe essere riscritto per svolgere la maggior parte del lavoro senza uscire da 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>();
Abbiamo ancora un paio di conversioni esplicite nei punti di ingresso e di uscita della funzione, ma il resto è ora codice JavaScript normale. A differenza dell'equivalente val
, ora può essere ottimizzato dal motore JavaScript e richiede solo di mettere in pausa il lato C++ una volta per tutte le operazioni asincrone.
Macro EM_ASYNC_JS
L'unico pezzo che non sembra bello è il wrapper Asyncify.handleAsync
, il cui unico scopo è consentire l'esecuzione di funzioni JavaScript async
con Asyncify. In effetti, questo caso d'uso è così comune che ora esiste una macro EM_ASYNC_JS
specializzata che li combina.
Ecco come potresti utilizzarlo per produrre la versione finale dell'esempio 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
è il metodo consigliato per dichiarare gli snippet JavaScript. È efficiente perché associa direttamente gli snippet dichiarati come qualsiasi altra importazione di funzioni JavaScript. Offre inoltre una buona ergonomia consentendoti di dichiarare esplicitamente tutti i tipi e i nomi dei parametri.
In alcuni casi, tuttavia, potresti voler inserire un breve snippet per la chiamata console.log
, un'istruzione debugger;
o qualcosa di simile e non voler dichiarare una funzione completamente separata. In questi rari casi, un EM_ASM macros family
(EM_ASM
, EM_ASM_INT
e EM_ASM_DOUBLE
) potrebbe essere una scelta più semplice. Queste macro sono simili alla macro EM_JS
, ma eseguono il codice in linea dove vengono inserite, anziché definire una funzione.
Poiché non dichiarano un prototipo di funzione, hanno bisogno di un modo diverso per specificare il tipo di ritorno e accedere agli argomenti.
Per scegliere il tipo restituito, devi utilizzare il nome della macro corretto. I blocchi EM_ASM
dovrebbero comportarsi come funzioni void
, i blocchi EM_ASM_INT
possono restituire un valore intero e i blocchi EM_ASM_DOUBLE
restituiscono numeri in virgola mobile di conseguenza.
Gli eventuali argomenti passati saranno disponibili con i nomi $0
, $1
e così via nel corpo di JavaScript. Come per EM_JS
o WebAssembly in generale, gli argomenti sono limitati solo a valori numerici: numeri interi, numeri in virgola mobile, puntatori e handle.
Ecco un esempio di come utilizzare una macro EM_ASM
per registrare un valore JS arbitrario nella 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
Infine, Emscripten supporta la dichiarazione del codice JavaScript in un file separato in un formato libreria personalizzato:
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
Poi devi dichiarare manualmente i prototipi corrispondenti lato C++:
extern "C" void log_value(EM_VAL val_handle);
Una volta dichiarata su entrambi i lati, la libreria JavaScript può essere collegata al codice principale tramite --js-library option
, collegando prototipi con implementazioni JavaScript corrispondenti.
Tuttavia, questo formato di modulo non è standard e richiede attente annotazioni relative alle dipendenze. Di conseguenza, è principalmente riservato a scenari avanzati.
Conclusione
In questo post abbiamo esaminato diversi modi per integrare il codice JavaScript in C++ quando si lavora con WebAssembly.
L'inclusione di questi snippet ti consente di esprimere lunghe sequenze di operazioni in modo più pulito ed efficiente e di utilizzare librerie di terze parti, nuove API JavaScript e persino funzionalità di sintassi JavaScript che non sono ancora esprimibili tramite C++ o Embind.