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
na potrzeby zaawansowanych scenariuszy, w których chcesz zadeklarować wiele funkcji JavaScriptu razem jako jedną bibliotekę.
Z tego artykułu dowiesz się, jak używać 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 JavaScript z instancjami C++ oraz konwertować wartości między typami C++ i JavaScript.
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.
- Wykonaj funkcję
- Przekształć wynik z JavaScriptu na format pośredni.
- Zwróć przekonwertowany wynik na język C++, który następnie odczyta go w języku C++.
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
Element EM_JS macro
umożliwia przeniesienie witryny fetch_json
do JavaScriptu. 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 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 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, możesz przepisać przykład fetch_json
, 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 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 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 jest to tak powszechne zastosowanie, że istnieje teraz specjalne makro EM_ASYNC_JS
, które łączy te 2 wartości.
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
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ż 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 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 tagu 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ć 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 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 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 zadeklarować odpowiednie prototypy ręcznie 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 dokładnego 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.
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.