Dowiedz się, jak umieszczać kod JavaScript w bibliotece WebAssembly, aby komunikować się ze światem zewnętrznym.
Pracując nad integracją WebAssembly z internetem, potrzebujesz sposobu na wywoływanie zewnętrznych interfejsów API, takich jak internetowe interfejsy API i biblioteki innych firm. Musisz następnie znaleźć sposób na przechowywanie wartości i instancji obiektów zwracanych przez te interfejsy API, a także sposób na przekazanie tych przechowywanych wartości do innych interfejsów API. W przypadku asynchronicznych interfejsów API konieczne może być też czekanie na obietnice w synchronicznym kodzie C/C++ za pomocą narzędzia Asyncify i odczytywanie wyniku po zakończeniu operacji.
Emscripten udostępnia kilka narzędzi do takich interakcji:
emscripten::val
do przechowywania i używania wartości JavaScript w C++.EM_JS
do umieszczania fragmentów kodu JavaScript i wiązania ich jako funkcji C/C++.EM_ASYNC_JS
podobny doEM_JS
, ale ułatwia umieszczanie asynchronicznych fragmentów kodu JavaScript.EM_ASM
do umieszczania krótkich fragmentów i wykonywania ich w tekście bez deklarowania funkcji.--js-library
na potrzeby zaawansowanych scenariuszy, w których chcesz zadeklarować wiele funkcji JavaScriptu razem jako jedną bibliotekę.
Z tego posta dowiesz się, jak używać ich wszystkich do podobnych zadań.
emscripten::val klasa
Zajęcia emcripten::val
są dostarczane przez Embind. Może wywoływać globalne interfejsy API, wiązać wartości JavaScript z instancjami C++ oraz konwertować wartości między typami C++ i JavaScript.
Aby użyć go razem z .await()
Asyncify do pobrania i przeanalizowania kodu 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>();
Taki kod działa dobrze, ale wykonuje wiele kroków pośrednich. Każda operacja na val
musi wykonać te czynności:
- Konwertowanie wartości C++ przekazywanych jako argumenty na format pośredni.
- Przejdź do JavaScriptu, odczytuj i konwertuj argumenty na wartości JavaScript.
- Wykonaj funkcję
- Przekonwertuj wynik z kodu JavaScript na format pośredni.
- Zwróć przekonwertowany wynik na język C++, który następnie odczyta go w języku C++.
Każdy element await()
musi też wstrzymać działanie strony C++ przez rozwinięcie całego stosu wywołań modułu WebAssembly, powrót do JavaScriptu, oczekiwanie i przywrócenie stosu WebAssembly po zakończeniu operacji.
Taki kod nie wymaga niczego z C++. Kod w C++ działa tylko jako napęd dla serii operacji JavaScript. A gdyby można było jednocześnie przenieść fetch_json
do JavaScriptu i jednocześnie ograniczyć nakład pracy związany z krokami pośrednimi?
Makro EM_JS
Element EM_JS macro
umożliwia przeniesienie witryny fetch_json
do JavaScriptu. EM_JS
w Emscripten umożliwia zadeklarowanie funkcji C/C++ zaimplementowanej przez fragment kodu JavaScript.
Podobnie jak w przypadku rozwiązania WebAssembly, ma on ograniczenie dotyczące obsługi argumentów liczbowych i zwracanych wartości. Aby przekazywać inne wartości, musisz je przekonwertować ręcznie za pomocą odpowiednich interfejsów API. Oto kilka przykładów.
Przekazywanie numerów nie wymaga żadnej konwersji:
// Passing numbers, doesn't need any conversion.
EM_JS(int, add_one, (int x), {
return x + 1;
});
int x = add_one(41);
Podczas przekazywania ciągów znaków do i z JavaScriptu musisz używać odpowiednich funkcji konwersji i alokacji z pliku 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);
});
W przypadku bardziej złożonych, arbitralnych typów wartości możesz też użyć interfejsu JavaScript API dla wcześniej wspomnianej klasy val
. Za jego pomocą możesz konwertować wartości JavaScript i klasy C++ na uchwyty pośrednie i z powrotem:
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>();
Mając na uwadze te interfejsy API, można przepisać przykład fetch_json
, aby obsługiwał większość zadań bez opuszczania JavaScriptu:
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>();
Nadal mamy kilka jawnych konwersji w punktach wejścia i wyjścia funkcji, ale reszta to zwykły kod JavaScript. W przeciwieństwie do odpowiednika val
można ją teraz optymalizować przez mechanizm JavaScriptu. Wszystkie operacje asynchroniczne wymagają tylko jednego wstrzymania strony C++.
Makro EM_ASYNC_JS
Jedynym, który nie wygląda dobrze, jest otoka Asyncify.handleAsync
– jej jedynym przeznaczeniem jest umożliwienie wykonywania funkcji JavaScriptu async
przy użyciu Asyncify. Ten przypadek użycia jest tak powszechny, że powstało specjalne makro EM_ASYNC_JS
, które łączy je ze sobą.
Oto w jaki sposób można użyć go do wygenerowania ostatecznej wersji przykładu 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
Zalecanym sposobem deklarowania fragmentów kodu JavaScript jest EM_JS
. Jest wydajny, ponieważ wiąże zadeklarowane fragmenty bezpośrednio tak jak każdy inny import funkcji JavaScript. Zapewnia też wysoką ergonomię, ponieważ umożliwia jawne zadeklarowanie wszystkich typów i nazw parametrów.
W niektórych przypadkach jednak chcesz wstawić krótki fragment kodu dla wywołania console.log
, instrukcji debugger;
lub podobnej, i nie chcesz zaprzątać sobie głowy deklarowaniem zupełnie osobnej funkcji. W tych rzadkich przypadkach prostszym rozwiązaniem może być EM_ASM macros family
(EM_ASM
, EM_ASM_INT
i EM_ASM_DOUBLE
). Te makra są podobne do makra EM_JS
, ale uruchamiają kod bezpośrednio w miejscu wstawienia, zamiast definiować funkcję.
Ponieważ nie deklarują prototypu funkcji, potrzebują innego sposobu określania typu zwracanego i uzyskiwania dostępu do argumentów.
Aby wybrać zwracany typ makra, musisz użyć odpowiedniej nazwy. Bloki EM_ASM
powinny działać jak funkcje void
, bloki (EM_ASM_INT
) mogą zwracać wartość całkowitą, a bloki EM_ASM_DOUBLE
– odpowiednio liczby zmiennoprzecinkowe.
Wszystkie przekazane argumenty będą dostępne pod nazwami $0
, $1
itd. w treści JavaScriptu. Podobnie jak w przypadku EM_JS
lub WebAssembly w ogóle argumenty są ograniczone do wartości liczbowych, takich jak liczby całkowite, liczby zmiennoprzecinkowe, wskaźniki i nick.
Oto przykład użycia makra EM_ASM
do zarejestrowania w konsoli dowolnej wartości JavaScriptu:
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
Dodatkowo Emscripten obsługuje deklarowanie kodu JavaScript w osobnym pliku w własnym formacie biblioteki:
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
Następnie musisz zadeklarować odpowiednie prototypy ręcznie po stronie C++:
extern "C" void log_value(EM_VAL val_handle);
Po zadeklarowaniu tej biblioteki po obu stronach możesz połączyć bibliotekę JavaScript z kodem głównym za pomocą interfejsu --js-library option
, łącząc prototypy z odpowiednimi implementacjami JavaScript.
Jest to jednak niestandardowy format modułu i wymaga ostrożnych adnotacji dotyczących zależności. Dlatego jest on zarezerwowany głównie na potrzeby zaawansowanych scenariuszy.
Podsumowanie
W tym poście pokazaliśmy różne sposoby integracji kodu JavaScript z C++ podczas pracy z WebAssembly.
Dołączenie takich fragmentów pozwala wyrażać długie sekwencje działań w czystszy i wydajniejszy sposób oraz korzystać z bibliotek innych firm, nowych interfejsów API JavaScript, a nawet funkcji składni JavaScript, których nie można jeszcze wyrazić w C++ lub Embind.