Dowiedz się, jak umieścić kod JavaScript w bibliotece WebAssembly, aby komunikować się ze światem zewnętrznym.
Podczas pracy nad integracją WebAssembly z internetem potrzebujesz sposobu na wywoływanie zewnętrznych interfejsów API, takich jak internetowe interfejsy API i biblioteki innych firm. Potrzebny jest też sposób przechowywania wartości i instancji obiektów zwróconych przez te interfejsy API oraz sposób na późniejsze przekazanie tych wartości do innych interfejsów API. W przypadku asynchronicznych interfejsów API może też być konieczne czekanie na obietnice w synchronicznym kodzie C/C++ z użyciem funkcji Asyncify i odczytanie wyniku po zakończeniu operacji.
Emscripten udostępnia kilka narzędzi do takich interakcji:
emscripten::val
do przechowywania wartości JavaScriptu i operowania nimi w C++.EM_JS
do umieszczania fragmentów kodu JavaScript i powią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 JavaScript jako jedną bibliotekę.
Z tego posta dowiesz się, jak używać ich wszystkich do podobnych zadań.
emscripten::val klasa
Klasa emcripten::val
jest świadczona 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 pobrać i przeanalizować zawartość pliku JSON razem z .await()
usługi Asyncify:
#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>();
Ten kod działa dobrze, ale wykonuje wiele kroków średniozaawansowanych. Każda operacja na zasobie val
musi obejmować następujące kroki:
- Przekonwertuj wartości C++ przekazywane jako argumenty na format pośredni.
- Otwórz JavaScript, a potem odczytaj i przekształć argumenty w wartości JavaScript.
- Wykonaj funkcję
- Przekonwertuj wynik z JavaScriptu na format pośredni.
- Zwróć przekonwertowany wynik do C++, a C++ na koniec odczyta go z powrotem.
Każdy obiekt await()
musi też wstrzymać stronę C++ przez cofnię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 C++ działa jedynie jako sterownik serii operacji JavaScript. A gdyby można było przenieść fetch_json
do JavaScriptu i jednocześnie zmniejszyć nakład pracy związany z krokami pośrednimi?
Makro EM_JS
EM_JS macro
umożliwia przeniesienie fetch_json
do JavaScriptu. Tag EM_JS
w Emscripten pozwala zadeklarować funkcję w języku C/C++ zaimplementowaną przez fragment kodu JavaScript.
Podobnie jak ona WebAssembly, ma ona ograniczenie dotyczące obsługi tylko argumentów liczbowych i zwracanych wartości. Jeśli chcesz przekazać inne wartości, musisz przekonwertować je ręcznie za pomocą odpowiednich interfejsów API. Oto kilka przykładów.
Przekazane numery nie wymagają 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);
});
Na koniec, aby uzyskać bardziej złożone, dowolne typy wartości, możesz użyć interfejsu JavaScript API dla wspomnianej wcześniej klasy val
. Umożliwia on konwersję wartości JavaScript i klas 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 zmodyfikować przykład fetch_json
w taki sposób, aby wykorzystywał 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>();
W punktach wejścia i wyjścia funkcji nadal mamy kilka jawnych konwersji, ale reszta to zwykły kod JavaScript. W przeciwieństwie do odpowiednika val
można ją teraz optymalizować przez mechanizm JavaScript. Wymaga tylko jednego wstrzymania strony C++ w przypadku wszystkich operacji asynchronicznych.
Makro EM_ASYNC_JS
Jedynym fragmentem, który nie wygląda dobrze, jest otoka Asyncify.handleAsync
– jej jedynym przeznaczeniem jest umożliwienie wykonywania funkcji JavaScript async
za pomocą Asyncify. Ten przypadek użycia jest tak powszechny, że teraz istnieje specjalne makro EM_ASYNC_JS
, które łączy je ze sobą.
Aby utworzyć ostateczną wersję przykładu fetch
, możesz go użyć w ten sposób:
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 to wydajne, bo wiąże zadeklarowane fragmenty bezpośrednio w taki sam sposób jak każda inna funkcja JavaScriptu importowana. Zapewnia też dobrą ergonomię, umożliwiając jawne deklarowanie wszystkich typów i nazw parametrów.
W niektórych przypadkach możesz jednak chcieć wstawić krótki fragment kodu dla wywołania console.log
, instrukcji debugger;
lub podobnej i nie chcesz zadeklarować całej oddzielnej funkcji. W tych rzadkich przypadkach prostszym wyborem 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 wykonują kod w miejscu, w którym zostały wstawione, ale nie definiują funkcji.
Ponieważ nie deklarują prototypu funkcji, potrzebują innego sposobu określania typu zwracanego i argumentów dostępu.
Aby wybrać zwracany typ, musisz użyć odpowiedniej nazwy makra. Bloki EM_ASM
powinny działać jak funkcje void
, bloki EM_ASM_INT
mogą zwracać wartość całkowitą, a bloki EM_ASM_DOUBLE
odpowiadają liczbom zmiennoprzecinkowym.
Wszystkie przekazane argumenty będą dostępne pod nazwami $0
, $1
itd. w treści JavaScriptu. Podobnie jak w przypadku EM_JS
lub WebAssembly argumenty są ograniczone tylko do wartości liczbowych – liczb całkowitych, liczb zmiennoprzecinkowych, wskaźników i uchwytów.
Oto przykład użycia makra EM_ASM
do zarejestrowania dowolnej wartości JS w konsoli:
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
Na koniec Emscripten obsługuje deklarowanie kodu JavaScript w osobnym pliku we własnym formacie biblioteki:
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
Następnie zadeklaruj odpowiednie prototypy ręcznie po stronie C++:
extern "C" void log_value(EM_VAL val_handle);
Po zadeklarowaniu po obu stronach biblioteki JavaScript można połączyć z kodem głównym za pomocą --js-library option
, łącząc prototypy z odpowiadającymi im implementacjami JavaScript.
Ten format modułu jest jednak niestandardowy i wymaga szczegółowych adnotacji dotyczących zależności. Dlatego jest on zwykle zarezerwowany dla scenariuszy zaawansowanych.
Podsumowanie
W tym poście omówiliśmy różne sposoby integracji kodu JavaScript z C++ podczas pracy z WebAssembly.
Uwzględnienie takich fragmentów kodu pozwala wyrazić długie sekwencje operacji w czytelniejszy i efektywniejszy sposób oraz korzystać z bibliotek innych firm, nowych interfejsów API JavaScript, a nawet funkcji składni JavaScript, których jeszcze nie da się wyrazić w C++ lub Embind.