Korzystanie z asynchronicznych interfejsów API internetowych w WebAssembly

Interfejsy API I/O w internecie są asynchroniczne, ale w większości języków systemowych są synchroniczne. Podczas kompilowania kodu na potrzeby WebAssembly musisz połączyć jeden typ interfejsu API z innym. W tym celu możesz użyć biblioteki Asyncify. Z tego artykułu dowiesz się, kiedy i jak używać Asyncify oraz jak działa to narzędzie.

Wejście-wyjście w językach systemowych

Zacznijmy od prostego przykładu w języku C. Załóżmy, że chcesz odczytać imię i nazwisko użytkownika z pliku i przywitać go wiadomością „Cześć, (nazwa użytkownika)!”:

#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;
}

Przykład nie robi zbyt wiele, ale pokazuje coś, co znajdziesz w aplikacji o dowolnej wielkości: aplikacja odczytuje dane wejściowe ze świata zewnętrznego, przetworzy je wewnętrznie i zapisze dane wyjściowe z powrotem na zewnątrz. Wszystkie takie interakcje ze światem zewnętrznym odbywają się za pomocą kilku funkcji, które nazywamy funkcjami wejścia-wyjścia, a które w skrócie nazywamy I/O.

Aby odczytać nazwę z C, musisz wykonać co najmniej 2 kluczowe wywołania I/O: fopen, aby otworzyć plik, oraz fread, aby odczytać z niego dane. Po pobraniu danych możesz użyć innej funkcji wejścia/wyjścia printf, aby wydrukować wynik na konsoli.

Na pierwszy rzut oka te funkcje wyglądają dość prosto i nie musisz się zastanawiać nad mechanizmami, które są potrzebne do odczytu lub zapisu danych. W zależności od środowiska może się jednak działo sporo:

  • Jeśli plik wejściowy znajduje się na dysku lokalnym, aplikacja musi wykonać serię operacji dostępu do pamięci i dysku, aby zlokalizować plik, sprawdzić uprawnienia, otworzyć go do odczytu, a następnie odczytać blok po bloku, aż do uzyskania żądanej liczby bajtów. Może to być dość powolne, w zależności od szybkości dysku i żądanego rozmiaru.
  • Plik wejściowy może też znajdować się w zamontowanej lokalizacji sieciowej, co oznacza, że w procesie będzie też uczestniczyć infrastruktura sieciowa, co zwiększa złożoność, opóźnienie i liczbę potencjalnych ponownych prób w przypadku każdej operacji.
  • Na koniec warto dodać, że nawet polecenie printf nie zawsze drukuje na konsoli, ponieważ może zostać przekierowane do pliku lub lokalizacji sieciowej. W takim przypadku trzeba wykonać te same czynności.

Krótko mówiąc, operacje wejścia/wyjścia mogą być powolne i nie da się przewidzieć, ile czasu zajmie wykonanie konkretnego wywołania po pobieżnym przejrzeniu kodu. Podczas wykonywania tej operacji cała aplikacja będzie wyglądać na zablokowaną i nie będzie reagować na działania użytkownika.

Nie dotyczy to tylko języków C i C++. Większość systemowych języków programowania udostępnia wszystkie dane wejściowe i wyjściowe w formie synchronicznych interfejsów API. Jeśli na przykład przetłumaczysz przykład na Rust, interfejs API może wyglądać na prostszy, ale obowiązują te same zasady. Wystarczy, że wykonasz wywołanie i sychronicznie poczekasz na wynik, podczas gdy funkcja wykonuje wszystkie kosztowne operacje i ostatecznie zwraca wynik w jednym wywołaniu:

fn main() {
    let s = std::fs::read_to_string("name.txt");
    println!("Hello, {}!", s);
}

Co się jednak stanie, gdy spróbujesz skompilować dowolny z tych przykładów na WebAssembly i przetłumaczyć go na potrzeby internetu? Na przykład, co może oznaczać operacja „odczyt pliku”? Musi odczytać dane z niektórego miejsca do przechowywania.

Asynchroniczny model sieci

W internecie jest wiele różnych opcji przechowywania, które możesz zmapować, np. pamięć podręczną (obiekty JS), localStorage, IndexedDB, pamięć po stronie serwera i nowy interfejs File System Access API.

Jednak tylko 2 z tych interfejsów API – pamięć podręczna i localStorage – można używać w sposób synchroniczny. Oba te interfejsy API stawiają największe ograniczenia dotyczące tego, co i jak długo można przechowywać. Wszystkie inne opcje udostępniają tylko asynchroniczne interfejsy API.

Jest to jedna z podstawowych właściwości wykonywania kodu w internecie: każda czasochłonna operacja, która obejmuje operacje wejścia-wyjścia, musi być asynchroniczna.

Dzieje się tak, ponieważ internet jest od zawsze jednowątkowy, a każdy kod użytkownika, który ma wpływ na interfejs użytkownika, musi działać w tym samym wątku co interfejs. Musi ona konkurować z innymi ważnymi zadaniami, takimi jak układanie, renderowanie i obsługa zdarzeń, o czas procesora. Nie chcesz, aby fragment kodu JavaScript lub WebAssembly mógł rozpocząć operację „odczyt pliku” i zablokować wszystko inne – całą kartę lub, w przeszłości, cały przeglądarkę – przez czas od milisekund do kilku sekund, dopóki nie zakończy się operacja.

Zamiast tego kod może tylko zaplanować operację wejścia/wyjścia wraz z wywołaniem zwrotnym, które zostanie wykonane po jej zakończeniu. Takie funkcje zwracane są w ramach pętli zdarzeń przeglądarki. Nie będę tutaj wchodzić w szczegóły, ale jeśli chcesz dowiedzieć się więcej o tym, jak działa pętla zdarzeń, przeczytaj artykuł Task, microtask, queue and schedules, w którym szczegółowo omawiamy ten temat.

W skrócie: przeglądarka uruchamia wszystkie elementy kodu w nieskończonej pętli, pobierając je z kolejki pojedynczo. Gdy zostanie wywołane jakieś zdarzenie, przeglądarka umieszcza odpowiedni moduł obsługi w kole, a w kole następnej iteracji pętli moduł ten jest pobierany z kole 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.

Pamiętaj, że podczas wykonywania niestandardowego kodu JavaScript (lub kodu WebAssembly) pętla zdarzeń jest zablokowana. W tym czasie 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 oddanie kontroli przeglądarce, aby mogła kontynuować przetwarzanie oczekujących zadań. Gdy operacje wejścia/wyjścia zostaną zakończone, twój moduł obsługi stanie się jednym z tych zadań i zostanie wykonany.

Jeśli np. chcesz przepisać powyższe przykłady w nowoczesnej wersji JavaScriptu i zamiast tego odczytać nazwę z zewnętrznego adresu URL, użyjesz 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);
}

Mimo że wygląda to na działanie synchroniczne, pod maską każda funkcja await jest w istocie skrótem dla funkcji zwrotnych:

function main() {
  return fetch("name.txt")
    .then(response => response.text())
    .then(name => console.log("Hello, %s!", name));
}

W tym przykładzie bez cukru, który jest nieco bardziej przejrzysty, żądanie jest uruchamiane, a odpowiedzi są subskrybowane w ramach pierwszego wywołania zwrotnego. Gdy przeglądarka otrzyma początkową odpowiedź (tylko nagłówki HTTP), wywołuje tę funkcję odwoływania asinkronicznie. Wywołanie zwrotne zaczyna odczytywać treść jako tekst za pomocą funkcji response.text() i subskrybuje wynik za pomocą kolejnego wywołania zwrotnego. Gdy fetch pobierze całą zawartość, wywoła ostatnią funkcję z zwróceniami wywołaniami, która wypisuje na konsoli „Cześć, (nazwa użytkownika)”.

Dzięki asynchronicznemu charakterowi tych kroków oryginalna funkcja może zwrócić kontrolę przeglądarce, gdy tylko zaplanowane zostaną operacje wejścia/wyjścia, i pozostawić cały interfejs użytkownika w stanie gotowości do obsługi innych zadań, takich jak renderowanie, przewijanie itp., podczas gdy operacje wejścia/wyjścia będą wykonywane w tle.

Na koniec warto wspomnieć, że nawet proste interfejsy API, takie jak „sleep”, które powodują, że aplikacja czeka przez określony czas w sekundach, są też formą operacji wejścia–wyjścia:

#include <stdio.h>
#include <unistd.h>
// ...
printf("A\n");
sleep(1);
printf("B\n");

Oczywiście. Możesz przetłumaczyć go w prosty sposób, aby zablokować bieżący wątek do czasu wygaśnięcia limitu czasu:

console.log("A");
for (let start = Date.now(); Date.now() - start < 1000;);
console.log("B");

W fakcie właśnie to robi Emscripten w swojej domyślnej implementacji funkcji „sleep”, ale jest to bardzo niewydajne, ponieważ blokuje cały interfejs użytkownika i nie pozwala na przetwarzanie innych zdarzeń. Zazwyczaj nie należy tego robić w kodzie produkcyjnym.

Zamiast tego bardziej idiomatyczne wywołanie „sleep” w JavaScriptu polegałoby na wywołaniu setTimeout() i subskrybowaniu go za pomocą metody obsługi:

console.log("A");
setTimeout(() => {
    console.log("B");
}, 1000);

Co łączy wszystkie te przykłady i interfejsy API? W obu przypadkach kod idiomatyczny w pierwotnym języku systemowym używa interfejsu API blokującego do operacji wejścia/wyjścia, podczas gdy odpowiedni przykład w internecie używa interfejsu API asynchronicznego. Podczas kompilowania na potrzeby internetu musisz jakoś przekształcić te 2 modele wykonywania, a WebAssembly nie ma jeszcze wbudowanej możliwości, która umożliwiłaby Ci to zrobienie.

Rozwiązanie problemu za pomocą Asyncify

Właśnie w tym momencie do akcji wkraczają Asyncify.Asyncify Asyncify to funkcja kompilacji obsługiwana przez Emscripten, która umożliwia wstrzymanie całego programu i jego asynchroniczne wznowienie w późniejszym czasie.

Graf funkcji wywołania opisujący przepływ danych z JavaScriptu > WebAssembly > interfejsu API sieci Web > wywołania zadania asynchronicznego, w którym Asyncify łączy wynik zadania asynchronicznego z WebAssembly

Użycie w języku C / C++ z Emscriptenem

Jeśli w przypadku ostatniego przykładu chcesz użyć Asyncify do zaimplementowania 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 JavaScriptu tak, jakby były funkcjami C. Wewnątrz użyj funkcji Asyncify.handleSleep(), która powoduje zawieszenie programu przez Emscripten i zapewnia moduł obsługi wakeUp(), który powinien zostać wywołany po zakończeniu operacji asynchronicznej. W przykładzie powyżej element obsługi jest przekazywany do funkcji setTimeout(), ale można go użyć w dowolnym innym kontekście, który akceptuje wywołania zwrotne. Na koniec możesz wywołać async_sleep() w dowolnym miejscu, tak jak zwykłe sleep() lub dowolny inny synchroniczny interfejs API.

Podczas kompilowania takiego kodu musisz poprosić Emscripten o włączenie funkcji Asyncify. Aby to zrobić, prześlij parametr -s ASYNCIFY oraz -s ASYNCIFY_IMPORTS=[func1, func2] z listą funkcji, 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ć zapisywania i przywracania stanu, więc kompilator wstrzykuje kod pomocniczy wokół takich wywołań.

Gdy wykonasz ten kod w przeglądarce, zobaczysz płynny dziennik wyjściowy, w którym instrukcja B będzie się pojawiać po krótkim opóźnieniu po instrukcji 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ć z pliku chcesz pobrać liczbę z zdalnego zasobu, możesz użyć fragmentu kodu podobnego do tego poniżej, aby wysłać żądanie, zawiesić kod C i wznowić jego działanie po pobraniu treści odpowiedzi. Wszystko to odbywa się płynnie, tak 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 obietnicy, takich jak fetch(), możesz nawet połączyć Asyncify z funkcją async-await JavaScriptu zamiast używać interfejsu API opartego na wywołaniu zwrotnym. W tym celu zamiast Asyncify.handleSleep() wybierz Asyncify.handleAsync(). Zamiast planować wywołanie funkcji wakeUp(), możesz przekazać funkcję JavaScript async i użyć w niej funkcji awaitreturn, dzięki czemu kod będzie wyglądał jeszcze bardziej naturalnie i synchronicznie, a Ty nie stracisz żadnych korzyści asynchronicznych operacji 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();

Oczekywanie wartości złożonych

W tym przykładzie nadal ograniczasz się do liczb. Co zrobić, jeśli chcesz zastosować oryginalny przykład, w którym próbuję pobrać nazwę użytkownika z pliku jako ciąg znaków? Możesz to zrobić.

Emscripten udostępnia funkcję Embind, która umożliwia konwersję wartości JavaScript na C++ i odwrotnie. Obsługuje też Asyncify, więc możesz wywoływać await() w zewnętrznych Promise, a będzie to działać tak samo jak await w asynchronicznym kodzie JavaScript:

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>();

Korzystając z tej metody, nie musisz nawet przekazywać parametru ASYNCIFY_IMPORTS jako flagi kompilacji, ponieważ jest on już domyślnie uwzględniony.

Wszystko działa świetnie w Emscripten. A co z innymi zestawami narzędzi i językami?

Użycie w innych językach

Załóżmy, że masz w kodzie Rusta podobne wywołanie synchroniczne, które chcesz zmapować na asynchroniczny interfejs API w internecie. Okazuje się, że Ty też możesz to zrobić.

Najpierw musisz zdefiniować taką funkcję jako zwykły import za pomocą bloku extern (lub składni wybranego języka dla funkcji obcych).

extern {
    fn get_answer() -> i32;
}

println!("Getting answer...");
let answer = get_answer();
println!("Answer is {}", answer);

Skompiluj kod na WebAssembly:

cargo build --target wasm32-unknown-unknown

Teraz musisz zmodyfikować plik WebAssembly, dodając do niego kod do przechowywania i przywracania stosu. W przypadku C/C++ Emscripten wykonałby to za nas, ale nie jest tu używany, więc proces jest nieco bardziej ręczny.

Na szczęście przekształcenie Asyncify jest całkowicie niezależne od narzędzia. Może przekształcać dowolne pliki WebAssembly, niezależnie od tego, który kompilator je wygenerował. Transformacja jest udostępniana osobno w ramach optymalizatora wasm-opt z pakietu narzędzi Binaryen i może być wywoływana w ten sposób:

wasm-opt -O2 --asyncify \
      --pass-arg=asyncify-imports@env.get_answer \
      [...]

Przekaż wartość --asyncify, aby włączyć przekształcenie, a potem użyj parametru --pass-arg=…, aby podać rozdzieloną przecinkami listę funkcji asynchronicznych, w których stan programu powinien zostać zawieszony, a następnie wznowiony.

Pozostaje tylko udostępnić kod środowiska wykonawczego, który będzie wykonywać te zadania, czyli wstrzymywać i wznawiać kod WebAssembly. W przypadku C / C++ jest to uwzględniane przez Emscripten, ale teraz potrzebujesz niestandardowego kodu JavaScript, który będzie obsługiwać dowolne pliki WebAssembly. Stworzyliśmy bibliotekę właśnie w tym celu.

Znajdziesz go na GitHubie pod adresem https://github.com/GoogleChromeLabs/asyncify lub w npm pod nazwą asyncify-wasm.

Symuluje on standardowe API instancjowania WebAssembly, ale w ramach własnej przestrzeni nazw. Jedyną różnicą jest to, że w ramach zwykłego interfejsu WebAssembly możesz importować tylko funkcje synchroniczne, a w ramach owijarki Asyncify możesz importować również funkcje 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 przykładzie powyżej) po stronie WebAssembly, biblioteka wykryje zwróconą wartość Promise, zawiesi i zapisze stan aplikacji WebAssembly, zapisze obietnicę do wykonania, a później, gdy zostanie ona rozwiązana, płynnie przywróci stos wywołań i stan oraz będzie kontynuować wykonywanie kodu tak, jakby nic się nie stało.

Ponieważ każda funkcja w module może wywołać wywołanie asynchroniczne, wszystkie eksporty mogą też być asynchroniczne, więc również zostaną owinięte. Jak widać w powyższym przykładzie, musisz await wynik funkcji instance.exports.main(), aby wiedzieć, kiedy wykonanie zostało zakończone.

Jak to wszystko działa?

Gdy Asyncify wykryje wywołanie jednej z funkcji ASYNCIFY_IMPORTS, rozpoczyna asynchroniczną operację, zapisuje cały stan aplikacji, w tym stos wywołań i wszystkie tymczasowe zmienne lokalne, a później, gdy operacja się zakończy, przywraca całą pamięć i stos wywołań oraz wznawia działanie od tego samego miejsca i w tym samym stanie, jakby program nigdy nie został zatrzymany.

Jest to funkcja podobna do async-await w JavaScript, którą pokazałem wcześniej, ale w odróżnieniu od niej nie wymaga żadnej specjalnej składni ani obsługi w czasie wykonywania przez język. Zamiast tego działa poprzez przekształcanie zwykłych synchronicznych funkcji w czasie kompilacji.

Podczas kompilowania wcześniejszego przykładu asynchronicznego uśpienia:

puts("A");
async_sleep(1);
puts("B");

Asyncify przekształca ten kod w postać mniej więcej taką: (pseudokod, rzeczywista transformacja jest bardziej skomplikowana):

if (mode == NORMAL_EXECUTION) {
    puts("A");
    async_sleep(1);
    saveLocals();
    mode = UNWINDING;
    return;
}
if (mode == REWINDING) {
    restoreLocals();
    mode = NORMAL_EXECUTION;
}
puts("B");

Na początku parametr mode ma wartość NORMAL_EXECUTION. W związku z tym przy pierwszym uruchomieniu przekształconego kodu zostanie oceniona tylko część kodu poprzedzająca instrukcję async_sleep(). Gdy tylko zaplanowana zostanie operacja asynchroniczna, Asyncify zapisuje wszystkie zmienne lokalne i odwija stos, wracając z każdej funkcji aż do góry, oddając w ten sposób kontrolę nad pętlą zdarzeń przeglądarki.

Gdy async_sleep() zostanie rozwiązany, kod obsługi Asyncify zmieni wartość mode na REWINDING i ponownie wywoła funkcję. Tym razem gałąź „normalnego wykonania” jest pomijana, ponieważ już wykonała zadanie podczas poprzedniego wywołania, a ja chcę uniknąć dwukrotnego drukowania „A”. Zamiast tego przechodzi od razu do gałęzi „przewijania wstecz”. Po osiągnięciu tego punktu przywraca wszystkie zapisane lokalne, zmienia tryb z powrotem na „normalny” i kontynuuje wykonywanie kodu tak, jakby nigdy nie został zatrzymany.

Koszty transformacji

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 zmodyfikować tylko funkcje oznaczone jako asynchroniczne na wierszu poleceń, a także ich potencjalnych wywołujących, ale obciążenie rozmiarem kodu może nadal wynosić około 50% przed kompresją.

Wykres pokazujący obciążenie rozmiarem kodu w przypadku różnych punktów odniesienia, od prawie 0% w optymalnych warunkach do ponad 100% w najgorszych przypadkach

Nie jest to idealne rozwiązanie, ale w wielu przypadkach jest akceptowalne, gdy alternatywą jest brak funkcji lub konieczność znacznego przeredagowania oryginalnego kodu.

Pamiętaj, aby zawsze włączać optymalizacje w przypadku wersji końcowych, aby nie zwiększać jeszcze bardziej rozmiaru pliku. Możesz też zaznaczyć opcje optymalizacji dotyczące Asyncify, aby zmniejszyć obciążenie, ograniczając przekształcenia tylko do określonych funkcji lub tylko do bezpośrednich wywołań funkcji. Wywołania asynchroniczne mają też niewielki wpływ na wydajność w czasie wykonywania, ale dotyczy to tylko samych wywołań. Jednak w porównaniu z kosztem faktycznej pracy jest to zwykle nieznaczna kwota.

Demonstracje w rzeczywistych warunkach

Po obejrzeniu prostych przykładów przejdę do bardziej skomplikowanych scenariuszy.

Jak wspomniano na początku tego artykułu, jedną z opcji przechowywania w internecie jest asynchroniczny interfejs File System Access API. Umożliwia ona dostęp do rzeczywistego systemu plików hosta z poziomu aplikacji internetowej.

Z drugiej strony istnieje standard de facto o nazwie WASI, który służy do obsługi wejść/wyjść WebAssembly w konsoli i na serwerze. Został zaprojektowany jako docel kompilacji dla języków systemowych i wyświetla wszystkie rodzaje systemów plików i innych operacji w tradycyjnej formie asynchronicznej.

Co, jeśli można je ze sobą powiązać? Następnie możesz skompilować dowolną aplikację w dowolnym języku źródłowym za pomocą dowolnego zestawu narzędzi obsługującego cel WASI i uruchomić ją w piaskownicy w internecie, zachowując przy tym możliwość jej działania na prawdziwych plikach użytkownika. Dzięki Asyncify możesz to zrobić.

W tym pokazie skompilowałem bibliotekę coreutils w Rust z kilkoma drobnymi poprawkami do WASI, przekazanymi za pomocą transformacji Asyncify, oraz zaimplementowałem asynchroniczne wiązania z WASI do interfejsu File System Access API po stronie JavaScript. Po połączeniu z elementem terminala Xterm.js zapewnia realistyczne środowisko, które działa w karcie przeglądarki i obsługuje rzeczywiste pliki użytkownika – tak jak prawdziwy terminal.

Możesz go sprawdzić na żywo na stronie https://wasi.rreverser.com/.

Zastosowania asynchronizacji nie ograniczają się tylko do zegarów i systemów plików. Możesz też użyć bardziej wyspecjalizowanych interfejsów API w internecie.

Na przykład za pomocą Asyncify można mapować libusb – prawdopodobnie najpopularniejszą natywną bibliotekę do pracy z urządzeniami USB – na WebUSB API, które zapewnia asynchroniczny dostęp do takich urządzeń w internecie. Po zmapowaniu i skompilowaniu mam standardowe testy i przykłady libusb do uruchomienia na wybranych urządzeniach bezpośrednio w piaskownicy strony internetowej.

Zrzut ekranu z wyjściami polecenia debugowania libusb na stronie internetowej, zawierający informacje o podłączonym aparacie Canon

Prawdopodobnie jest to temat na inny post na blogu.

Te przykłady pokazują, jak potężne może być narzędzie Asyncify w zaspokajaniu potrzeb użytkowników i przenoszeniu różnych aplikacji do sieci. Pozwala ono uzyskać dostęp do platformy, korzystać z sandboxa i zapewnia większą ochronę bez utraty funkcjonalności.