Dowiedz się, jak osadzić 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 interfejsy API internetowe i biblioteki innych firm. Następnie musisz znaleźć sposób na przechowywanie wartości i instancji obiektów zwracanych przez te interfejsy API oraz sposób na przekazanie tych wartości do innych interfejsów API. W przypadku interfejsów API asynchronicznych może być też konieczne oczekiwanie na obietnice w kodzie synchronicznym C/C++ za pomocą funkcji Asyncify oraz odczytanie wyniku po zakończeniu operacji.
Emscripten udostępnia kilka narzędzi do takich interakcji:
emscripten::val
do przechowywania wartości JavaScript i działania na nich w C++.EM_JS
do umieszczania fragmentów kodu JavaScript i wiązania ich jako funkcji C/C++.EM_ASYNC_JS
, który jest podobny doEM_JS
, ale ułatwia umieszczanie asynchronicznych fragmentów kodu JavaScript.EM_ASM
do umieszczania krótkich fragmentów kodu i ich wykonywania w miejscu, bez deklarowania funkcji.--js-library
w zaawansowanych scenariuszach, gdy chcesz zadeklarować wiele funkcji JavaScript jako jedną bibliotekę.
Z tego artykułu dowiesz się, jak używać wszystkich tych narzędzi do podobnych zadań.
Klasa emscripten::val
Klasa emcripten::val
jest udostępniana przez Embind. Może wywoływać globalne interfejsy API, wiązać wartości JavaScriptu z wystąpieniami C++ oraz konwertować wartości między typami C++ i JavaScriptu.
Oto jak używać go w połączeniu z funkcją .await()
biblioteki Asyncify, aby pobrać i przeanalizować dane 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>();
Ten kod działa dobrze, ale wykonuje wiele pośrednich kroków. Każda operacja na val
wymaga wykonania tych czynności:
- Konwertuje wartości C++ przekazane jako argumenty na format pośredni.
- Otwórz JavaScript, przeczytaj argumenty i przekształć je w wartości JavaScriptu.
- Wykonywanie funkcji
- Przekształć wynik z JavaScriptu na format pośredni.
- Zwracanie przekonwertowanego wyniku do C++, a C++ odczytuje go.
Każda funkcja await()
musi też wstrzymać stronę C++, odwijając cały stos wywołań modułu WebAssembly, wracając do JavaScriptu, czekając i odtwarzając stos WebAssembly po zakończeniu operacji.
Taki kod nie wymaga niczego z C++. Kod C++ działa tylko jako sterownik dla serii operacji JavaScript. Co, jeśli można by przenieść fetch_json
do JavaScriptu i jednocześnie ograniczyć koszty pośrednie?
Makro EM_JS
EM_JS macro
umożliwia przeniesienie fetch_json
do kodu JavaScript. EM_JS
w Emscripten pozwala zadeklarować funkcję C/C++, która jest implementowana przez fragment kodu JavaScript.
Podobnie jak WebAssembly, ma ono ograniczenie polegające na tym, że obsługuje tylko argumenty i wartości zwracane o typie liczbowym. Aby przekazać inne wartości, musisz je przekonwertować ręcznie za pomocą odpowiednich interfejsów API. Oto kilka przykładów.
Przekazywanie liczb nie wymaga 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 JavaScriptu i z niego musisz używać odpowiednich funkcji konwersji i przydziału 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, dowolnych typów wartości możesz użyć interfejsu JavaScript API dla wspomnianej wcześniej klasy val
. Dzięki temu możesz konwertować wartości JavaScriptu 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, przykład fetch_json
można przerobić tak, aby większość pracy wykonywał on 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 na początku i na końcu funkcji, ale reszta to już zwykły kod JavaScriptu. W przeciwieństwie do odpowiednika w val
może być teraz optymalizowany przez silnik JavaScriptu i wymaga tylko jednorazowego wstrzymania działania kodu C++ na potrzeby wszystkich operacji asynchronicznych.
Makro EM_ASYNC_JS
Jedynym elementem, który nie wygląda zbyt ładnie, jest obudowa Asyncify.handleAsync
. Jej jedynym celem jest umożliwienie wykonywania funkcji async
JavaScript za pomocą Asyncify. W zasadzie ten przypadek użycia jest tak powszechny, że istnieje teraz specjalne makro EM_ASYNC_JS
, które łączy te 2 wartości.
Oto, jak można użyć tego atrybutu 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
EM_JS
to zalecany sposób deklarowania fragmentów kodu JavaScript. Jest to skuteczne rozwiązanie, ponieważ łączy zadeklarowane fragmenty kodu bezpośrednio, tak jak inne importowane funkcje JavaScript. Zapewnia też dobrą ergonomię, ponieważ umożliwia jawne zadeklarowanie wszystkich typów i nazwy parametrów.
W niektórych przypadkach możesz jednak chcieć wstawić krótki fragment kodu wywołania funkcji console.log
, instrukcji debugger;
lub czegoś podobnego i nie chcesz deklarować osobnej funkcji. W takich rzadkich przypadkach prostszym rozwiązaniem może być użycie EM_ASM macros family
(EM_ASM
, EM_ASM_INT
i EM_ASM_DOUBLE
). Te makra są podobne do makra EM_JS
, ale zamiast definiowania funkcji wykonują kod w miejscu wstawienia.
Ponieważ nie deklarują prototypu funkcji, muszą używać innego sposobu określania typu zwracanego i dostępu do argumentów.
Aby wybrać typ zwracanej wartości, musisz użyć odpowiedniej nazwy makra. Bloki EM_ASM
mają działać jak funkcje void
, bloki EM_ASM_INT
mogą zwracać wartość całkowitą, a bloki EM_ASM_DOUBLE
zwracają liczby zmiennoprzecinkowe.
Wszystkie przekazane argumenty będą dostępne w ciele skryptu JavaScript pod nazwami $0
, $1
itd. 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 zapisania 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
Emscripten obsługuje też deklarowanie kodu JavaScriptu w osobnym pliku w niestandardowym formacie biblioteki:
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
Następnie musisz ręcznie zadeklarować odpowiednie prototypy po stronie C++:
extern "C" void log_value(EM_VAL val_handle);
Po zadeklarowaniu po obu stronach bibliotekę JavaScript można połączyć z głównym kodem za pomocą --js-library option
, łącząc prototypy z odpowiednimi implementacjami JavaScript.
Format tego modułu jest jednak niestandardowy i wymaga starannego oznaczenia zależności. Z tego powodu jest ona przeznaczona głównie do zaawansowanych scenariuszy.
Podsumowanie
W tym poście omówiliśmy różne sposoby integrowania kodu JavaScript z językiem C++ podczas pracy z WebAssembly.
Użycie takich fragmentów kodu umożliwia tworzenie długich sekwencji operacji w bardziej przejrzysty i wydajny sposób oraz korzystanie z bibliotek zewnętrznych, nowych interfejsów API JavaScriptu, a nawet funkcji składni JavaScriptu, których nie można jeszcze wyrazić za pomocą C++ ani Embind.