Interfejsy API wejścia/wyjścia w internecie są asynchroniczne, ale w większości języków systemowych są synchroniczne. Podczas kompilowania kodu do WebAssembly musisz połączyć jeden rodzaj interfejsów API z innym, a tym połączeniem jest Asyncify. Z tego posta dowiesz się, kiedy i jak używać Asyncify oraz jak działa ta funkcja.
Wejście-wyjście w językach systemowych
Zacznę od prostego przykładu w C. Załóżmy, że chcesz odczytać imię użytkownika z pliku i przywitać go komunikatem „Hello, (username)!”:
#include <stdio.h>
int main() {
FILE *stream = fopen("name.txt", "r");
char name[20+1];
size_t len = fread(&name, 1, 20, stream);
name[len] = '\0';
fclose(stream);
printf("Hello, %s!\n", name);
return 0;
}
Ten przykład nie robi zbyt wiele, ale pokazuje coś, co znajdziesz w aplikacji dowolnej wielkości: odczytuje dane wejściowe ze świata zewnętrznego, przetwarza je wewnętrznie i zapisuje dane wyjściowe z powrotem w świecie zewnętrznym. Cała interakcja ze światem zewnętrznym odbywa się za pomocą kilku funkcji, które są powszechnie nazywane funkcjami wejścia-wyjścia, w skrócie I/O.
Aby odczytać nazwę z C, musisz wykonać co najmniej 2 kluczowe wywołania wejścia-wyjścia: fopen, aby otworzyć plik, i fread, aby odczytać z niego dane. Po pobraniu danych możesz użyć innej funkcji wejścia/wyjścia printf, aby wydrukować wynik w konsoli.
Na pierwszy rzut oka te funkcje wydają się dość proste i nie musisz się zastanawiać nad mechanizmem odczytywania lub zapisywania danych. W zależności od środowiska w środku może się jednak dziać całkiem sporo:
- Jeśli plik wejściowy znajduje się na dysku lokalnym, aplikacja musi wykonać szereg operacji dostępu do pamięci i dysku, aby zlokalizować plik, sprawdzić uprawnienia, otworzyć go do odczytu, a następnie odczytywać blok po bloku, aż do pobrania żądanej liczby bajtów. Może to potrwać dość długo w zależności od szybkości dysku i rozmiaru żądania.
- Plik wejściowy może też znajdować się w zamontowanej lokalizacji sieciowej. W takim przypadku w procesie będzie uczestniczyć również stos sieciowy, co zwiększy złożoność, opóźnienie i liczbę potencjalnych ponownych prób każdej operacji.
- Wreszcie nawet
printfnie gwarantuje wyświetlania informacji w konsoli i może być przekierowywane do pliku lub lokalizacji sieciowej, w którym to przypadku musi przejść te same kroki co powyżej.
Krótko mówiąc, operacje wejścia/wyjścia mogą być powolne i nie można przewidzieć, ile czasu zajmie konkretne wywołanie, na podstawie szybkiego spojrzenia na kod. Podczas wykonywania tej operacji cała aplikacja będzie wyglądać na zamrożoną i nie będzie reagować na działania użytkownika.
Nie dotyczy to tylko języków C i C++. Większość języków systemowych prezentuje wszystkie dane wejścia-wyjścia w formie synchronicznych interfejsów API. Jeśli na przykład przetłumaczysz ten przykład na język Rust, interfejs API może wyglądać prościej, ale obowiązują te same zasady. Wystarczy, że wykonasz połączenie i synchronicznie poczekasz na zwrócenie wyniku, podczas gdy usługa wykona wszystkie kosztowne operacje i ostatecznie zwróci wynik w ramach jednego wywołania:
fn main() {
let s = std::fs::read_to_string("name.txt");
println!("Hello, {}!", s);
}
Ale co się stanie, gdy spróbujesz skompilować którykolwiek z tych przykładów do WebAssembly i przetłumaczyć go na potrzeby internetu? Podaj konkretny przykład: co może oznaczać operacja „odczyt pliku”? Musiałby odczytać dane z pamięci.
Model asynchroniczny internetu
W internecie dostępnych jest wiele różnych opcji przechowywania danych, które można mapować, np. pamięć w pamięci (obiekty JS), localStorage, IndexedDB, pamięć po stronie serwera i nowy interfejs File System Access API.
Tylko 2 z nich – pamięć w pamięci i localStorage – mogą być używane synchronicznie, a oba te interfejsy API mają największe ograniczenia dotyczące tego, co i jak długo można przechowywać. Wszystkie pozostałe opcje udostępniają tylko asynchroniczne interfejsy API.
Jest to jedna z podstawowych cech wykonywania kodu w internecie: każda czasochłonna operacja, w tym każda operacja wejścia-wyjścia, musi być asynchroniczna.
Wynika to z faktu, że internet jest historycznie jednowątkowy, a każdy kod użytkownika, który ma wpływ na interfejs, musi być uruchamiany w tym samym wątku co interfejs. Musi konkurować z innymi ważnymi zadaniami, takimi jak układ, renderowanie i obsługa zdarzeń, o czas procesora. Nie chcesz, aby fragment kodu JavaScript lub WebAssembly mógł rozpocząć operację „odczytu pliku” i blokować wszystko inne – całą kartę lub, w przeszłości, całą przeglądarkę – przez okres od milisekund do kilku sekund, dopóki nie zostanie zakończona.
Zamiast tego kod może tylko zaplanować operację wejścia/wyjścia wraz z wywołaniem zwrotnym, które ma zostać wykonane po zakończeniu operacji. Takie wywołania zwrotne są wykonywane w ramach pętli zdarzeń przeglądarki. Nie będę tu wchodzić w szczegóły, ale jeśli chcesz dowiedzieć się, jak działa pętla zdarzeń, przeczytaj artykuł Tasks, microtasks, queues and schedules, w którym ten temat jest szczegółowo omówiony.
W skrócie: przeglądarka uruchamia wszystkie fragmenty kodu w rodzaju nieskończonej pętli, pobierając je z kolejki jeden po drugim. Gdy zostanie wywołane jakieś zdarzenie, przeglądarka umieszcza odpowiedni moduł obsługi w kolejce, a w następnej iteracji pętli jest on wyjmowany z kolejki i wykonywany. Ten mechanizm umożliwia symulowanie współbieżności i wykonywanie wielu operacji równoległych przy użyciu tylko jednego wątku.
Ważne jest, aby pamiętać, że podczas wykonywania niestandardowego kodu JavaScript (lub WebAssembly) pętla zdarzeń jest blokowana i nie można reagować na żadne zewnętrzne moduły obsługi, zdarzenia, operacje wejścia/wyjścia itp. Jedynym sposobem na uzyskanie wyników operacji wejścia/wyjścia jest zarejestrowanie wywołania zwrotnego, zakończenie wykonywania kodu i przekazanie kontroli z powrotem do przeglądarki, aby mogła ona kontynuować przetwarzanie oczekujących zadań. Po zakończeniu operacji wejścia/wyjścia Twój moduł obsługi stanie się jednym z tych zadań i zostanie wykonany.
Jeśli na przykład chcesz przepisać powyższe przykłady w nowoczesnym JavaScript i odczytać nazwę ze zdalnego adresu URL, użyj interfejsu Fetch API i składni async-await:
async function main() {
let response = await fetch("name.txt");
let name = await response.text();
console.log("Hello, %s!", name);
}
Chociaż wygląda to na synchroniczne, w rzeczywistości każdy element await jest w zasadzie lukrem składniowym dla wywołań zwrotnych:
function main() {
return fetch("name.txt")
.then(response => response.text())
.then(name => console.log("Hello, %s!", name));
}
W tym uproszczonym przykładzie, który jest nieco bardziej przejrzysty, żądanie jest uruchamiane, a odpowiedzi są subskrybowane za pomocą pierwszego wywołania zwrotnego. Gdy przeglądarka otrzyma wstępną odpowiedź – tylko nagłówki HTTP – asynchronicznie wywołuje tę funkcję zwrotną. Wywołanie zwrotne zaczyna odczytywać treść jako tekst za pomocą response.text() i subskrybuje wynik za pomocą innego wywołania zwrotnego. Gdy fetch pobierze wszystkie treści, wywoła ostatnie wywołanie zwrotne, które wyświetli w konsoli tekst „Hello, (username)!”.
Dzięki asynchronicznemu charakterowi tych kroków oryginalna funkcja może zwrócić kontrolę do przeglądarki, gdy tylko zaplanowane zostaną operacje wejścia/wyjścia. Pozostawia to cały interfejs użytkownika responsywny i dostępny dla innych zadań, w tym renderowania, przewijania itp., podczas gdy operacje wejścia/wyjścia są wykonywane w tle.
Ostatni przykład: nawet proste interfejsy API, takie jak „sleep”, które powodują, że aplikacja czeka określoną liczbę sekund, są również formą operacji wejścia/wyjścia:
#include <stdio.h>
#include <unistd.h>
// ...
printf("A\n");
sleep(1);
printf("B\n");
Możesz przetłumaczyć to w bardzo prosty sposób, który zablokuje bieżący wątek do czasu wygaśnięcia:
console.log("A");
for (let start = Date.now(); Date.now() - start < 1000;);
console.log("B");
W rzeczywistości dokładnie to robi Emscripten w domyślnej implementacji funkcji „sleep”, ale jest to bardzo nieefektywne, blokuje cały interfejs i nie pozwala na obsługę innych zdarzeń. Zwykle nie należy tego robić w kodzie produkcyjnym.
Bardziej idiomatyczna wersja funkcji „sleep” w JavaScript polega na wywołaniu funkcji setTimeout() i zasubskrybowaniu jej za pomocą procedury obsługi:
console.log("A");
setTimeout(() => {
console.log("B");
}, 1000);
Co łączy te przykłady i interfejsy API? W każdym przypadku idiomatyczny kod w języku oryginalnego systemu używa blokującego interfejsu API do operacji wejścia/wyjścia, podczas gdy odpowiedni przykład dla internetu używa zamiast tego asynchronicznego interfejsu API. Podczas kompilowania na potrzeby internetu musisz jakoś przekształcić te 2 modele wykonywania, a WebAssembly nie ma jeszcze wbudowanej możliwości, aby to zrobić.
Pokonywanie różnic za pomocą Asyncify
W tym miejscu pojawia się Asyncify. Asyncify to funkcja czasu kompilacji obsługiwana przez Emscripten, która umożliwia wstrzymanie całego programu i asynchroniczne wznowienie go w późniejszym czasie.
Użycie w C / C++ z Emscripten
Jeśli w ostatnim przykładzie chcesz użyć Asyncify do wdrożenia asynchronicznego uśpienia, możesz to zrobić w ten sposób:
#include <stdio.h>
#include <emscripten.h>
EM_JS(void, async_sleep, (int seconds), {
Asyncify.handleSleep(wakeUp => {
setTimeout(wakeUp, seconds * 1000);
});
});
…
puts("A");
async_sleep(1);
puts("B");
EM_JS to makro, które umożliwia definiowanie fragmentów kodu JavaScript tak, jakby były funkcjami C. Wewnątrz użyj funkcji Asyncify.handleSleep(), która nakazuje Emscriptenowi zawieszenie programu i udostępnia moduł obsługi wakeUp(), który powinien zostać wywołany po zakończeniu operacji asynchronicznej. W przykładzie powyżej funkcja obsługi jest przekazywana do setTimeout(), ale można jej używać w dowolnym innym kontekście, który akceptuje wywołania zwrotne. Na koniec możesz wywołać funkcję async_sleep() w dowolnym miejscu, tak jak zwykłą funkcję sleep() lub dowolny inny synchroniczny interfejs API.
Podczas kompilowania takiego kodu musisz poinformować Emscripten o konieczności aktywowania funkcji Asyncify. Aby to zrobić, przekaż -s ASYNCIFY oraz -s ASYNCIFY_IMPORTS=[func1,
func2] z listą funkcji podobną do tablicy, które mogą być asynchroniczne.
emcc -O2 \
-s ASYNCIFY \
-s ASYNCIFY_IMPORTS=[async_sleep] \
...
Dzięki temu Emscripten wie, że wywołania tych funkcji mogą wymagać zapisania i przywrócenia stanu, więc kompilator wstrzyknie kod pomocniczy wokół takich wywołań.
Teraz, gdy wykonasz ten kod w przeglądarce, zobaczysz ciągły dziennik danych wyjściowych, tak jak można się spodziewać, z B pojawiającym się po krótkim opóźnieniu po A.
A
B
Możesz też zwracać wartości z funkcji Asyncify. Musisz zwrócić wynik funkcji handleSleep() i przekazać go do wywołania zwrotnego wakeUp(). Jeśli na przykład zamiast odczytywać dane z pliku chcesz pobrać liczbę ze zdalnego zasobu, możesz użyć fragmentu kodu podobnego do poniższego, aby wysłać żądanie, zawiesić kod C i wznowić jego działanie po pobraniu treści odpowiedzi. Wszystko to odbywa się bezproblemowo, jakby wywołanie było synchroniczne.
EM_JS(int, get_answer, (), {
return Asyncify.handleSleep(wakeUp => {
fetch("answer.txt")
.then(response => response.text())
.then(text => wakeUp(Number(text)));
});
});
puts("Getting answer...");
int answer = get_answer();
printf("Answer is %d\n", answer);
W przypadku interfejsów API opartych na obietnicach, takich jak fetch(), możesz nawet połączyć Asyncify z funkcją async-await JavaScriptu zamiast używać interfejsu API opartego na wywołaniach zwrotnych. W tym celu zamiast Asyncify.handleSleep() zadzwoń pod numer Asyncify.handleAsync(). Zamiast planować wakeUp()wywołanie zwrotneasync, możesz przekazać asyncfunkcję JavaScriptawait i użyć w niej return, dzięki czemu kod będzie wyglądał bardziej naturalnie i synchronicznie, a jednocześnie zachowa wszystkie zalety asynchronicznego wejścia/wyjścia.
EM_JS(int, get_answer, (), {
return Asyncify.handleAsync(async () => {
let response = await fetch("answer.txt");
let text = await response.text();
return Number(text);
});
});
int answer = get_answer();
Oczekuje na wartości złożone
Ten przykład nadal ogranicza Cię tylko do liczb. Co zrobić, jeśli chcesz zaimplementować oryginalny przykład, w którym próbowałem pobrać z pliku imię użytkownika jako ciąg znaków? Możesz to zrobić.
Emscripten udostępnia funkcję o nazwie Embind, która umożliwia obsługę konwersji między wartościami JavaScript i C++. Obsługuje też Asyncify, więc możesz wywoływać await() na zewnętrznych Promise, a będzie to działać tak samo jak await w asynchronicznym kodzie JavaScript z użyciem async-await:
val fetch = val::global("fetch");
val response = fetch(std::string("answer.txt")).await();
val text = response.call<val>("text").await();
auto answer = text.as<std::string>();
Gdy używasz tej metody, nie musisz nawet przekazywać ASYNCIFY_IMPORTS jako flagi kompilacji, ponieważ jest ona domyślnie uwzględniana.
OK, to wszystko świetnie działa w Emscripten. A co z innymi łańcuchami narzędzi i językami?
Użycie w innych językach
Załóżmy, że w kodzie Rust masz podobne wywołanie synchroniczne, które chcesz zamapować na asynchroniczny interfejs API w internecie. Okazuje się, że to też jest możliwe.
Najpierw musisz zdefiniować taką funkcję jako zwykły import za pomocą bloku extern (lub wybranej składni języka dla funkcji zewnętrznych).
extern {
fn get_answer() -> i32;
}
println!("Getting answer...");
let answer = get_answer();
println!("Answer is {}", answer);
i skompiluj kod do WebAssembly:
cargo build --target wasm32-unknown-unknown
Teraz musisz instrumentować plik WebAssembly kodem do zapisywania i przywracania stosu. W przypadku języków C i C++ zrobiłby to za nas Emscripten, ale nie jest on tutaj używany, więc proces jest nieco bardziej manualny.
Na szczęście sama transformacja Asyncify jest całkowicie niezależna od łańcucha narzędzi. Może przekształcać dowolne pliki WebAssembly, niezależnie od tego, który kompilator je wygenerował. Przekształcenie jest dostarczane oddzielnie w ramach optymalizatora wasm-opt z łańcucha narzędzi Binaryen i można je wywołać w ten sposób:
wasm-opt -O2 --asyncify \
--pass-arg=asyncify-imports@env.get_answer \
[...]
Przekaż wartość --asyncify, aby włączyć transformację, a następnie użyj --pass-arg=…, aby podać rozdzieloną przecinkami listę funkcji asynchronicznych, w przypadku których stan programu powinien zostać zawieszony, a następnie wznowiony.
Pozostało tylko dostarczyć kod środowiska wykonawczego, który będzie to robić – wstrzymywać i wznawiać kod WebAssembly. W przypadku C / C++ ten kod byłby dołączany przez Emscripten, ale teraz potrzebujesz niestandardowego kodu JavaScript, który obsługiwałby dowolne pliki WebAssembly. Właśnie w tym celu stworzyliśmy bibliotekę.
Znajdziesz ją na GitHubie pod adresem https://github.com/GoogleChromeLabs/asyncify lub w npm pod nazwą asyncify-wasm.
Symuluje standardowy interfejs API instancji WebAssembly, ale w swojej własnej przestrzeni nazw. Jedyna różnica polega na tym, że w przypadku zwykłego interfejsu WebAssembly API można udostępniać tylko funkcje synchroniczne jako importy, a w przypadku otoki Asyncify można udostępniać również importy asynchroniczne:
const { instance } = await Asyncify.instantiateStreaming(fetch('app.wasm'), {
env: {
async get_answer() {
let response = await fetch("answer.txt");
let text = await response.text();
return Number(text);
}
}
});
…
await instance.exports.main();
Gdy spróbujesz wywołać taką funkcję asynchroniczną – np. get_answer() w powyższym przykładzie – po stronie WebAssembly, biblioteka wykryje zwrócony obiekt Promise, zawiesi i zapisze stan aplikacji WebAssembly, zasubskrybuje zakończenie obietnicy, a później, gdy zostanie ona rozwiązana, bezproblemowo przywróci stos wywołań i stan oraz będzie kontynuować wykonywanie tak, jakby nic się nie stało.
Ponieważ każda funkcja w module może wykonywać wywołanie asynchroniczne, wszystkie eksportowane elementy również mogą być asynchroniczne, więc są opakowywane. W powyższym przykładzie możesz zauważyć, że musisz await wynik instance.exports.main(), aby wiedzieć, kiedy wykonanie naprawdę się zakończy.
Jak to wszystko działa?
Gdy Asyncify wykryje wywołanie jednej z funkcji ASYNCIFY_IMPORTS, rozpoczyna operację asynchroniczną, zapisuje cały stan aplikacji, w tym stos wywołań i wszystkie tymczasowe zmienne lokalne, a później, gdy ta operacja zostanie zakończona, przywraca całą pamięć i stos wywołań oraz wznawia działanie w tym samym miejscu i w tym samym stanie, jakby program nigdy się nie zatrzymał.
Jest to bardzo podobne do funkcji async-await w JavaScript, którą pokazałem wcześniej, ale w przeciwieństwie do niej nie wymaga żadnej specjalnej składni ani obsługi w czasie działania języka. Zamiast tego działa poprzez przekształcanie zwykłych funkcji synchronicznych w czasie kompilacji.
Podczas kompilowania pokazanego wcześniej przykładu asynchronicznego snu:
puts("A");
async_sleep(1);
puts("B");
Asyncify przekształca ten kod w mniej więcej taki kod (pseudokod, rzeczywiste przekształcenie jest bardziej złożone):
if (mode == NORMAL_EXECUTION) {
puts("A");
async_sleep(1);
saveLocals();
mode = UNWINDING;
return;
}
if (mode == REWINDING) {
restoreLocals();
mode = NORMAL_EXECUTION;
}
puts("B");
Początkowo wartość mode jest ustawiona na NORMAL_EXECUTION. W związku z tym, gdy taki przekształcony kod zostanie wykonany po raz pierwszy, oceniona zostanie tylko część prowadząca do async_sleep(). Gdy tylko zaplanowana zostanie operacja asynchroniczna, Asyncify zapisuje wszystkie zmienne lokalne i odwija stos, zwracając wartość z każdej funkcji aż do góry. W ten sposób przywraca kontrolę do pętli zdarzeń przeglądarki.
Gdy async_sleep() zostanie rozwiązany, kod obsługi Asyncify zmieni mode na REWINDING i ponownie wywoła funkcję. Tym razem gałąź „normalnego wykonania” jest pomijana – ponieważ wykonała już zadanie poprzednim razem i nie chcę drukować litery „A” dwa razy – i zamiast tego przechodzi bezpośrednio do gałęzi „przewijania”. Po osiągnięciu tego punktu przywraca wszystkie zapisane zmienne lokalne, zmienia tryb z powrotem na „normalny” i kontynuuje wykonywanie, jakby kod nigdy nie został zatrzymany.
Koszty przekształcenia
Niestety przekształcenie Asyncify nie jest całkowicie bezpłatne, ponieważ musi wstrzyknąć sporo kodu pomocniczego do przechowywania i przywracania wszystkich tych zmiennych lokalnych, poruszania się po stosie wywołań w różnych trybach itp. Próbuje modyfikować tylko funkcje oznaczone jako asynchroniczne w wierszu poleceń, a także ich potencjalnych wywołujących, ale narzut na rozmiar kodu może nadal wynosić około 50% przed kompresją.

Nie jest to idealne rozwiązanie, ale w wielu przypadkach jest akceptowalne, gdy alternatywą jest brak danej funkcji lub konieczność wprowadzenia znaczących zmian w oryginalnym kodzie.
Zawsze włączaj optymalizacje w przypadku ostatecznych wersji, aby uniknąć jeszcze większego wzrostu. Możesz też zaznaczyć opcje optymalizacji specyficzne dla Asyncify, aby zmniejszyć narzut, ograniczając przekształcenia tylko do określonych funkcji lub tylko do bezpośrednich wywołań funkcji. Wpływa to też nieznacznie na wydajność w czasie działania, ale tylko w przypadku wywołań asynchronicznych. W porównaniu z kosztem rzeczywistej pracy jest on jednak zwykle znikomy.
Wersje demonstracyjne w rzeczywistych warunkach
Teraz, gdy znasz już proste przykłady, przejdę do bardziej skomplikowanych scenariuszy.
Jak wspomnieliśmy na początku tego artykułu, jedną z opcji przechowywania danych w internecie jest asynchroniczny interfejs File System Access API. Umożliwia dostęp do systemu plików prawdziwego hosta z aplikacji internetowej.
Z drugiej strony istnieje standard de facto o nazwie WASI dla operacji wejścia/wyjścia WebAssembly w konsoli i po stronie serwera. Został zaprojektowany jako cel kompilacji dla języków systemowych i udostępnia wszelkiego rodzaju operacje na systemie plików i inne operacje w tradycyjnej formie synchronicznej.
A gdyby tak można było je ze sobą powiązać? Dzięki temu możesz skompilować dowolną aplikację w dowolnym języku źródłowym za pomocą dowolnego łańcucha narzędzi obsługującego platformę WASI i uruchomić ją w piaskownicy w internecie, a jednocześnie umożliwić jej działanie na prawdziwych plikach użytkownika. Dzięki Asyncify możesz to zrobić.
W tej wersji demonstracyjnej skompilowałem pakiet Rust coreutils z kilkoma drobnymi poprawkami do WASI, przekazanymi za pomocą transformacji Asyncify, i wdrożyłem asynchroniczne powiązania z WASI do interfejsu File System Access API po stronie JavaScript. Po połączeniu z komponentem terminala Xterm.js zapewnia realistyczną powłokę działającą w karcie przeglądarki i operującą na prawdziwych plikach użytkownika – tak jak prawdziwy terminal.
Możesz ją sprawdzić na stronie https://wasi.rreverser.com/.
Zastosowania Asyncify nie ograniczają się tylko do timerów i systemów plików. Możesz pójść o krok dalej i korzystać w internecie z bardziej niszowych interfejsów API.
Na przykład za pomocą Asyncify można też zmapować libusb – prawdopodobnie najpopularniejszą bibliotekę natywną do pracy z urządzeniami USB – na WebUSB API, co zapewnia asynchroniczny dostęp do takich urządzeń w internecie. Po zmapowaniu i skompilowaniu otrzymałem standardowe testy i przykłady libusb, które mogłem uruchomić na wybranych urządzeniach bezpośrednio w piaskownicy strony internetowej.

To jednak temat na inny post na blogu.
Te przykłady pokazują, jak potężne może być narzędzie Asyncify w przekształcaniu i portowaniu do internetu wszelkiego rodzaju aplikacji, co pozwala uzyskać dostęp na wielu platformach, tryb piaskownicy i lepsze zabezpieczenia bez utraty funkcjonalności.