Dowiedz się, jak kod, który wchodzi w interakcje z urządzeniami zewnętrznymi, można przenieść do internetu za pomocą interfejsów API WebAssembly i Fugu.
W poprzednim poście pokazałem, jak przenosić aplikacje korzystające z interfejsów API systemu plików do internetu za pomocą File System Access API, WebAssembly i Asyncify. Teraz chcę kontynuować ten sam temat integracji interfejsów API Fugu z WebAssembly i przenoszenia aplikacji do internetu bez utraty ważnych funkcji.
Pokażę, jak aplikacje, które komunikują się z urządzeniami USB, można przenieść do internetu, przenosząc libusb – popularną bibliotekę USB napisaną w języku C – do WebAssembly (za pomocą Emscripten), Asyncify i WebUSB.
Zacznijmy od początku: wersja demonstracyjna
Najważniejszą rzeczą podczas przenoszenia biblioteki jest wybór odpowiedniej wersji demonstracyjnej, która zaprezentuje możliwości przeniesionej biblioteki, pozwoli przetestować ją na różne sposoby i będzie atrakcyjna wizualnie.
Wybrałem pomysł na pilota do lustrzanki cyfrowej. W szczególności projekt open source gPhoto2 jest rozwijany od dłuższego czasu, dzięki czemu udało się przeprowadzić analizę wsteczną i zapewnić obsługę wielu różnych aparatów cyfrowych. Obsługuje kilka protokołów, ale najbardziej interesowała mnie obsługa USB, która jest realizowana przez libusb.
Opiszę kroki tworzenia tej wersji demonstracyjnej w 2 częściach. W tym poście na blogu opiszę, jak udało mi się przenieść libusb i jakie sztuczki mogą być konieczne do przeniesienia innych popularnych bibliotek do interfejsów API Fugu. W drugim poście szczegółowo opiszę przenoszenie i integrację gPhoto2.
W rezultacie powstała działająca aplikacja internetowa, która wyświetla podgląd obrazu na żywo z lustrzanki cyfrowej i umożliwia sterowanie jej ustawieniami przez USB. Zanim zapoznasz się ze szczegółami technicznymi, możesz obejrzeć wersję demonstracyjną na żywo lub nagraną wcześniej:
Uwaga dotycząca specyfiki aparatu
Możesz zauważyć, że zmiana ustawień w filmie zajmuje trochę czasu. Podobnie jak w przypadku większości innych problemów, które możesz napotkać, nie jest to spowodowane wydajnością WebAssembly ani WebUSB, ale sposobem, w jaki gPhoto2 współdziała z konkretnym aparatem wybranym do demonstracji.
Sony a6600 nie udostępnia interfejsu API do bezpośredniego ustawiania wartości takich jak ISO, przysłona czy szybkość migawki, a zamiast tego udostępnia tylko polecenia do ich zwiększania lub zmniejszania o określoną liczbę kroków. Co gorsza, nie zwraca też listy faktycznie obsługiwanych wartości – zwrócona lista wydaje się być zakodowana na stałe w wielu modelach aparatów Sony.
Podczas ustawiania jednej z tych wartości gPhoto2 nie ma innego wyboru, jak:
- Zrób krok (lub kilka) w kierunku wybranej wartości.
- Poczekaj chwilę, aż kamera zaktualizuje ustawienia.
- Odczytaj wartość, na której faktycznie zatrzymał się aparat.
- Sprawdź, czy ostatni krok nie spowodował przeskoczenia żądanej wartości ani nie spowodował przejścia na koniec lub początek listy.
- Powtórz.
Może to trochę potrwać, ale jeśli wartość jest rzeczywiście obsługiwana przez aparat, zostanie osiągnięta. Jeśli nie, aparat zatrzyma się na najbliższej obsługiwanej wartości.
Inne kamery mogą mieć inne ustawienia, interfejsy API i funkcje. Pamiętaj, że gPhoto2 to projekt open source, a automatyczne lub ręczne testowanie wszystkich modeli aparatów jest po prostu niemożliwe, dlatego szczegółowe raporty o problemach i PR są zawsze mile widziane (ale najpierw upewnij się, że problemy występują w oficjalnym kliencie gPhoto2).
Ważne uwagi dotyczące zgodności na wielu platformach
Niestety w systemie Windows wszystkim „znanym” urządzeniom, w tym lustrzankom cyfrowym, jest przypisywany sterownik systemowy, który nie jest zgodny z WebUSB. Jeśli chcesz wypróbować wersję demonstracyjną w systemie Windows, musisz użyć narzędzia takiego jak Zadig, aby zastąpić sterownik podłączonej lustrzanki cyfrowej sterownikiem WinUSB lub libusb. To podejście sprawdza się w moim przypadku i w przypadku wielu innych użytkowników, ale używasz go na własne ryzyko.
W systemie Linux prawdopodobnie musisz ustawić uprawnienia niestandardowe, aby zezwolić na dostęp do lustrzanki cyfrowej przez WebUSB, chociaż zależy to od dystrybucji.
W systemach macOS i Android wersja demonstracyjna powinna działać od razu. Jeśli próbujesz to zrobić na telefonie z Androidem, przełącz się na tryb poziomy, ponieważ nie poświęciłem zbyt wiele uwagi na to, aby strona była responsywna (mile widziane są prośby o ściągnięcie):
Więcej informacji o korzystaniu z WebUSB na wielu platformach znajdziesz w sekcji „Platform-specific considerations” (Uwagi dotyczące poszczególnych platform) w artykule „Building a device for WebUSB” (Tworzenie urządzenia na potrzeby WebUSB).
Dodawanie nowego backendu do libusb
Przejdźmy teraz do szczegółów technicznych. Można wprawdzie udostępnić interfejs API shim podobny do libusb (było to już robione) i połączyć z nim inne aplikacje, ale takie podejście jest podatne na błędy i utrudnia dalsze rozszerzanie lub konserwację. Chciałem zrobić to dobrze, tak aby w przyszłości można było przekazać te zmiany do projektu i scalić je z libusb.
Na szczęście w pliku README biblioteki libusb można znaleźć taką informację:
„libusb jest wewnętrznie abstrakcyjna, dzięki czemu można ją przenieść na inne systemy operacyjne. Więcej informacji znajdziesz w pliku PORTING”.
Biblioteka libusb jest skonstruowana w taki sposób, że publiczny interfejs API jest oddzielony od „backendów”. Te backendy odpowiadają za wyświetlanie, otwieranie, zamykanie i komunikację z urządzeniami za pomocą interfejsów API niskiego poziomu systemu operacyjnego. W ten sposób libusb już teraz eliminuje różnice między systemami Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku i Solaris i działa na wszystkich tych platformach.
Musiałem dodać kolejny backend dla „systemu operacyjnego” Emscripten+WebUSB. Implementacje tych backendów znajdują się w folderze libusb/os:
~/w/d/libusb $ ls libusb/os
darwin_usb.c haiku_usb_raw.h threads_posix.lo
darwin_usb.h linux_netlink.c threads_posix.o
events_posix.c linux_udev.c threads_windows.c
events_posix.h linux_usbfs.c threads_windows.h
events_posix.lo linux_usbfs.h windows_common.c
events_posix.o netbsd_usb.c windows_common.h
events_windows.c null_usb.c windows_usbdk.c
events_windows.h openbsd_usb.c windows_usbdk.h
haiku_pollfs.cpp sunos_usb.c windows_winusb.c
haiku_usb_backend.cpp sunos_usb.h windows_winusb.h
haiku_usb.h threads_posix.c
haiku_usb_raw.cpp threads_posix.h
Każdy backend zawiera nagłówek libusbi.h z typowymi typami i funkcjami pomocniczymi oraz musi udostępniać zmienną usbi_backend typu usbi_os_backend. Na przykład tak wygląda backend systemu Windows:
const struct usbi_os_backend usbi_backend = {
"Windows",
USBI_CAP_HAS_HID_ACCESS,
windows_init,
windows_exit,
windows_set_option,
windows_get_device_list,
NULL, /* hotplug_poll */
NULL, /* wrap_sys_device */
windows_open,
windows_close,
windows_get_active_config_descriptor,
windows_get_config_descriptor,
windows_get_config_descriptor_by_value,
windows_get_configuration,
windows_set_configuration,
windows_claim_interface,
windows_release_interface,
windows_set_interface_altsetting,
windows_clear_halt,
windows_reset_device,
NULL, /* alloc_streams */
NULL, /* free_streams */
NULL, /* dev_mem_alloc */
NULL, /* dev_mem_free */
NULL, /* kernel_driver_active */
NULL, /* detach_kernel_driver */
NULL, /* attach_kernel_driver */
windows_destroy_device,
windows_submit_transfer,
windows_cancel_transfer,
NULL, /* clear_transfer_priv */
NULL, /* handle_events */
windows_handle_transfer_completion,
sizeof(struct windows_context_priv),
sizeof(union windows_device_priv),
sizeof(struct windows_device_handle_priv),
sizeof(struct windows_transfer_priv),
};
Przeglądając właściwości, widzimy, że struktura zawiera nazwę backendu, zestaw jego funkcji, moduły obsługi różnych operacji USB niskiego poziomu w postaci wskaźników funkcji i wreszcie rozmiary do przydzielenia na potrzeby przechowywania prywatnych danych na poziomie urządzenia, kontekstu i transferu.
Prywatne pola danych są przydatne przynajmniej do przechowywania uchwytów systemu operacyjnego do wszystkich tych elementów, ponieważ bez uchwytów nie wiemy, do którego elementu odnosi się dana operacja. W implementacji internetowej uchwyty systemu operacyjnego byłyby podstawowymi obiektami w JavaScripcie WebUSB. Naturalnym sposobem ich reprezentowania i przechowywania w Emscripten jest klasa emscripten::val, która jest dostarczana w ramach Embind (systemu powiązań Emscripten).
Większość backendów w folderze jest zaimplementowana w języku C, ale kilka jest zaimplementowanych w C++. Embind działa tylko z C++, więc wybór został dokonany za mnie i dodałem libusb/libusb/os/emscripten_webusb.cpp z wymaganą strukturą i z sizeof(val) dla prywatnych pól danych:
#include <emscripten.h>
#include <emscripten/val.h>
#include "libusbi.h"
using namespace emscripten;
// …function implementations
const usbi_os_backend usbi_backend = {
.name = "Emscripten + WebUSB backend",
.caps = LIBUSB_CAP_HAS_CAPABILITY,
// …handlers—function pointers to implementations above
.device_priv_size = sizeof(val),
.transfer_priv_size = sizeof(val),
};
Przechowywanie obiektów WebUSB jako uchwytów urządzeń
libusb udostępnia gotowe do użycia wskaźniki do przydzielonego obszaru na dane prywatne. Aby pracować z tymi wskaźnikami jako instancjami val, dodałem małe funkcje pomocnicze, które tworzą je na miejscu, pobierają jako odwołania i przenoszą wartości:
// We store an Embind handle to WebUSB USBDevice in "priv" metadata of
// libusb device, this helper returns a pointer to it.
struct ValPtr {
public:
void init_to(val &&value) { new (ptr) val(std::move(value)); }
val &get() { return *ptr; }
val take() { return std::move(get()); }
protected:
ValPtr(val *ptr) : ptr(ptr) {}
private:
val *ptr;
};
struct WebUsbDevicePtr : ValPtr {
public:
WebUsbDevicePtr(libusb_device *dev)
: ValPtr(static_cast<val *>(usbi_get_device_priv(dev))) {}
};
val &get_web_usb_device(libusb_device *dev) {
return WebUsbDevicePtr(dev).get();
}
struct WebUsbTransferPtr : ValPtr {
public:
WebUsbTransferPtr(usbi_transfer *itransfer)
: ValPtr(static_cast<val *>(usbi_get_transfer_priv(itransfer))) {}
};
Asynchroniczne interfejsy API sieci Web w synchronicznych kontekstach C
Teraz potrzebny był sposób obsługi asynchronicznych interfejsów API WebUSB, w których libusb oczekuje operacji synchronicznych. W tym celu mogę użyć Asyncify, a dokładniej integracji Embind za pomocą val::await().
Chciałem też prawidłowo obsługiwać błędy WebUSB i konwertować je na kody błędów libusb, ale Embind nie ma obecnie możliwości obsługi wyjątków JavaScriptu ani odrzuceń Promise po stronie C++. Ten problem można obejść, przechwytując odrzucenie po stronie JavaScriptu i konwertując wynik na obiekt { error, value }, który można teraz bezpiecznie przeanalizować po stronie C++. Udało mi się to zrobić, łącząc makro EM_JS i interfejsy API Emval.to{Handle, Value}:
EM_JS(EM_VAL, em_promise_catch_impl, (EM_VAL handle), {
let promise = Emval.toValue(handle);
promise = promise.then(
value => ({error : 0, value}),
error => {
const ERROR_CODES = {
// LIBUSB_ERROR_IO
NetworkError : -1,
// LIBUSB_ERROR_INVALID_PARAM
DataError : -2,
TypeMismatchError : -2,
IndexSizeError : -2,
// LIBUSB_ERROR_ACCESS
SecurityError : -3,
…
};
console.error(error);
let errorCode = -99; // LIBUSB_ERROR_OTHER
if (error instanceof DOMException)
{
errorCode = ERROR_CODES[error.name] ?? errorCode;
}
else if (error instanceof RangeError || error instanceof TypeError)
{
errorCode = -2; // LIBUSB_ERROR_INVALID_PARAM
}
return {error: errorCode, value: undefined};
}
);
return Emval.toHandle(promise);
});
val em_promise_catch(val &&promise) {
EM_VAL handle = promise.as_handle();
handle = em_promise_catch_impl(handle);
return val::take_ownership(handle);
}
// C++ struct representation for {value, error} object from above
// (performs conversion in the constructor).
struct promise_result {
libusb_error error;
val value;
promise_result(val &&result)
: error(static_cast<libusb_error>(result["error"].as<int>())),
value(result["value"]) {}
// C++ counterpart of the promise helper above that takes a promise, catches
// its error, converts to a libusb status and returns the whole thing as
// `promise_result` struct for easier handling.
static promise_result await(val &&promise) {
promise = em_promise_catch(std::move(promise));
return {promise.await()};
}
};
Teraz mogę użyć promise_result::await() na dowolnym Promise zwróconym z operacji WebUSB i osobno sprawdzić pola error i value.
Na przykład pobranie obiektu val reprezentującego USBDevice z libusb_device_handle, wywołanie jego metody open(), oczekiwanie na wynik i zwrócenie kodu błędu jako kodu stanu libusb wygląda tak:
int em_open(libusb_device_handle *handle) {
auto web_usb_device = get_web_usb_device(handle->dev);
return promise_result::await(web_usb_device.call<val>("open")).error;
}
Wyliczanie urządzeń
Zanim otworzę jakiekolwiek urządzenie, libusb musi pobrać listę dostępnych urządzeń. Backend musi implementować tę operację za pomocą modułu obsługi get_device_list.
Trudność polega na tym, że ze względów bezpieczeństwa nie można wyświetlić listy wszystkich podłączonych urządzeń USB w internecie, tak jak na innych platformach. Zamiast tego proces jest podzielony na 2 części. Najpierw aplikacja internetowa żąda urządzeń o określonych właściwościach za pomocą navigator.usb.requestDevice(), a użytkownik ręcznie wybiera urządzenie, które chce udostępnić, lub odrzuca prośbę o przyznanie uprawnień. Następnie aplikacja wyświetla listę zatwierdzonych i połączonych urządzeń za pomocą navigator.usb.getDevices().
Na początku próbowałem użyć requestDevice() bezpośrednio w implementacji modułu obsługi get_device_list. Jednak wyświetlanie prośby o przyznanie uprawnień z listą połączonych urządzeń jest uważane za operację poufną i musi być wywoływane przez interakcję użytkownika (np. kliknięcie przycisku na stronie). W przeciwnym razie zawsze zwraca odrzuconą obietnicę. Aplikacje libusb często chcą wyświetlać listę połączonych urządzeń po uruchomieniu aplikacji, więc użycie requestDevice() nie było możliwe.
Zamiast tego musiałem pozostawić wywołanie navigator.usb.requestDevice() deweloperowi i udostępnić tylko zatwierdzone urządzenia z navigator.usb.getDevices():
// Store the global `navigator.usb` once upon initialisation.
thread_local const val web_usb = val::global("navigator")["usb"];
int em_get_device_list(libusb_context *ctx, discovered_devs **devs) {
// C++ equivalent of `await navigator.usb.getDevices()`.
// Note: at this point we must already have some devices exposed -
// caller must have called `await navigator.usb.requestDevice(...)`
// in response to user interaction before going to LibUSB.
// Otherwise this list will be empty.
auto result = promise_result::await(web_usb.call<val>("getDevices"));
if (result.error) {
return result.error;
}
auto &web_usb_devices = result.value;
// Iterate over the exposed devices.
uint8_t devices_num = web_usb_devices["length"].as<uint8_t>();
for (uint8_t i = 0; i < devices_num; i++) {
auto web_usb_device = web_usb_devices[i];
// …
*devs = discovered_devs_append(*devs, dev);
}
return LIBUSB_SUCCESS;
}
Większość kodu backendu używa val i promise_result w podobny sposób jak pokazano powyżej. W kodzie obsługi przesyłania danych jest jeszcze kilka ciekawych rozwiązań, ale szczegóły implementacji są mniej ważne w kontekście tego artykułu. Jeśli chcesz, możesz sprawdzić kod i komentarze na GitHubie.
Przenoszenie pętli zdarzeń do internetu
Ostatnim elementem portu libusb, który chcę omówić, jest obsługa zdarzeń. Jak opisano w poprzednim artykule, większość interfejsów API w językach systemowych, takich jak C, jest synchroniczna, a obsługa zdarzeń nie jest wyjątkiem. Zwykle jest ona implementowana za pomocą nieskończonej pętli, która „odpytuje” (próbuje odczytać dane lub blokuje wykonywanie, dopóki nie będą dostępne jakieś dane) zestaw zewnętrznych źródeł wejścia/wyjścia, a gdy co najmniej jedno z nich odpowie, przekazuje to jako zdarzenie do odpowiedniego modułu obsługi. Po zakończeniu działania modułu obsługi sterowanie wraca do pętli, która zostaje wstrzymana do następnego odpytywania.
W przypadku internetu takie podejście wiąże się z kilkoma problemami.
Po pierwsze, WebUSB nie udostępnia i nie może udostępniać nieprzetworzonych uchwytów urządzeń bazowych, więc bezpośrednie ich odpytywanie nie jest możliwe. Po drugie, libusb używa interfejsów API eventfd i pipe do obsługi innych zdarzeń oraz transferów w systemach operacyjnych bez nieprzetworzonych uchwytów urządzeń, ale eventfd nie jest obecnie obsługiwany w Emscripten, a pipe, mimo że jest obsługiwany, obecnie nie jest zgodny ze specyfikacją i nie może czekać na zdarzenia.
Największym problemem jest jednak to, że sieć ma własną pętlę zdarzeń. Ta globalna pętla zdarzeń jest używana we wszystkich zewnętrznych operacjach wejścia/wyjścia (w tym fetch(), licznikach czasu lub, w tym przypadku, WebUSB) i wywołuje moduły obsługi zdarzeń lub Promise po zakończeniu odpowiednich operacji. Wykonywanie kolejnej, zagnieżdżonej, nieskończonej pętli zdarzeń uniemożliwi postęp pętli zdarzeń przeglądarki, co oznacza, że nie tylko interfejs przestanie reagować, ale też kod nigdy nie otrzyma powiadomień o tych samych zdarzeniach wejścia/wyjścia, na które czeka. Zwykle powoduje to zakleszczenie i tak też się stało, gdy próbowałem użyć libusb w wersji demonstracyjnej. Strona zawiesiła się.
Podobnie jak w przypadku innych blokujących operacji wejścia-wyjścia, aby przenieść takie pętle zdarzeń do internetu, deweloperzy muszą znaleźć sposób na ich uruchamianie bez blokowania głównego wątku. Jednym ze sposobów jest refaktoryzacja aplikacji, aby obsługiwała zdarzenia wejścia/wyjścia w osobnym wątku i przekazywała wyniki z powrotem do wątku głównego. Innym sposobem jest użycie Asyncify do wstrzymania pętli i oczekiwania na zdarzenia w sposób nieblokujący.
Nie chciałem wprowadzać znaczących zmian w libusb ani gPhoto2, a do integracji z Promise użyłem już Asyncify, więc wybrałem tę ścieżkę. Aby zasymulować blokujący wariant poll(), w początkowej wersji modelu koncepcyjnego użyłem pętli, jak pokazano poniżej:
#ifdef __EMSCRIPTEN__
// TODO: optimize this. Right now it will keep unwinding-rewinding the stack
// on each short sleep until an event comes or the timeout expires.
// We should probably create an actual separate thread that does signaling
// or come up with a custom event mechanism to report events from
// `usbi_signal_event` and process them here.
double until_time = emscripten_get_now() + timeout_ms;
do {
// Emscripten `poll` ignores timeout param, but pass 0 explicitly just
// in case.
num_ready = poll(fds, nfds, 0);
if (num_ready != 0) break;
// Yield to the browser event loop to handle events.
emscripten_sleep(0);
} while (emscripten_get_now() < until_time);
#else
num_ready = poll(fds, nfds, timeout_ms);
#endif
Działanie:
- Wywołuje
poll(), aby sprawdzić, czy backend zgłosił już jakieś zdarzenia. Jeśli są, pętla się zatrzymuje. W przeciwnym razie implementacjapoll()w Emscripten natychmiast zwróci0. - Połączenia telefoniczne:
emscripten_sleep(0). Ta funkcja używa Asyncify isetTimeout(), aby przekazać kontrolę z powrotem do głównej pętli zdarzeń przeglądarki. Dzięki temu przeglądarka może obsługiwać wszelkie interakcje użytkownika i zdarzenia wejścia/wyjścia, w tym WebUSB. - Sprawdź, czy upłynął określony czas oczekiwania. Jeśli nie, kontynuuj pętlę.
Jak wspomniano w komentarzu, to podejście nie było optymalne, ponieważ powodowało ciągłe zapisywanie i przywracanie całego stosu wywołań za pomocą Asyncify, nawet gdy nie było jeszcze żadnych zdarzeń USB do obsłużenia (co zdarza się przez większość czasu), oraz ponieważ funkcja setTimeout() ma w nowoczesnych przeglądarkach minimalny czas trwania wynoszący 4 ms. Mimo to działało to na tyle dobrze, że w ramach modelu koncepcyjnego udało się uzyskać transmisję na żywo z lustrzanki cyfrowej z szybkością 13–14 klatek na sekundę.
Później postanowiłem ulepszyć to rozwiązanie, wykorzystując system zdarzeń przeglądarki. Istnieje kilka sposobów na dalsze ulepszenie tej implementacji, ale na razie zdecydowałem się emitować zdarzenia niestandardowe bezpośrednio w obiekcie globalnym, bez powiązania ich z konkretną strukturą danych libusb. Udało mi się to zrobić za pomocą mechanizmu oczekiwania i powiadamiania opartego na makrze EM_ASYNC_JS:
EM_JS(void, em_libusb_notify, (void), {
dispatchEvent(new Event("em-libusb"));
});
EM_ASYNC_JS(int, em_libusb_wait, (int timeout), {
let onEvent, timeoutId;
try {
return await new Promise(resolve => {
onEvent = () => resolve(0);
addEventListener('em-libusb', onEvent);
timeoutId = setTimeout(resolve, timeout, -1);
});
} finally {
removeEventListener('em-libusb', onEvent);
clearTimeout(timeoutId);
}
});
Funkcja em_libusb_notify() jest używana, gdy libusb próbuje zgłosić zdarzenie, np. zakończenie przesyłania danych:
void usbi_signal_event(usbi_event_t *event)
{
uint64_t dummy = 1;
ssize_t r;
r = write(EVENT_WRITE_FD(event), &dummy, sizeof(dummy));
if (r != sizeof(dummy))
usbi_warn(NULL, "event write failed");
#ifdef __EMSCRIPTEN__
em_libusb_notify();
#endif
}
Z kolei część em_libusb_wait() służy do „wybudzania” ze stanu uśpienia Asyncify, gdy zostanie odebrane zdarzenie em-libusb lub upłynie limit czasu:
double until_time = emscripten_get_now() + timeout_ms;
for (;;) {
// Emscripten `poll` ignores timeout param, but pass 0 explicitly just
// in case.
num_ready = poll(fds, nfds, 0);
if (num_ready != 0) break;
int timeout = until_time - emscripten_get_now();
if (timeout <= 0) break;
int result = em_libusb_wait(timeout);
if (result != 0) break;
}
Dzięki znacznemu ograniczeniu liczby uśpień i wybudzeń ten mechanizm rozwiązał problemy z wydajnością wcześniejszej implementacji opartej na emscripten_sleep() i zwiększył przepustowość wersji demonstracyjnej DSLR z 13–14 klatek na sekundę do stałych ponad 30 klatek na sekundę, co wystarcza do płynnego przesyłania obrazu na żywo.
System kompilacji i pierwszy test
Po zakończeniu prac nad backendem musiałem dodać go do Makefile.am i configure.ac. Jedyną interesującą częścią jest modyfikacja flag Emscripten:
emscripten)
AC_SUBST(EXEEXT, [.html])
# Note: LT_LDFLAGS is not enough here because we need link flags for executable.
AM_LDFLAGS="${AM_LDFLAGS} --bind -s ASYNCIFY -s ASSERTIONS -s ALLOW_MEMORY_GROWTH -s INVOKE_RUN=0 -s EXPORTED_RUNTIME_METHODS=['callMain']"
;;
Po pierwsze, pliki wykonywalne na platformach Unix zwykle nie mają rozszerzeń plików. Emscripten generuje jednak różne dane wyjściowe w zależności od tego, o jakie rozszerzenie poprosisz. Używam AC_SUBST(EXEEXT, …), aby zmienić rozszerzenie pliku wykonywalnego na .html, dzięki czemu każdy plik wykonywalny w pakiecie – testy i przykłady – staje się plikiem HTML z domyślną powłoką Emscripten, która dba o wczytywanie i tworzenie instancji JavaScriptu i WebAssembly.
Po drugie, ponieważ używam Embind i Asyncify, muszę włączyć te funkcje (--bind -s ASYNCIFY) oraz zezwolić na dynamiczny wzrost pamięci (-s ALLOW_MEMORY_GROWTH) za pomocą parametrów łącznika. Niestety biblioteka nie może zgłosić tych flag do linkera, więc każda aplikacja, która używa tego portu libusb, musi dodać te same flagi linkera do swojej konfiguracji kompilacji.
Na koniec, jak wspomniano wcześniej, WebUSB wymaga wyliczenia urządzeń za pomocą gestu użytkownika. Przykłady i testy libusb zakładają, że mogą wyliczać urządzenia podczas uruchamiania, i kończą się błędem, jeśli nie zostaną wprowadzone zmiany. Zamiast tego musiałem wyłączyć automatyczne wykonywanie (-s INVOKE_RUN=0) i udostępnić metodę ręczną callMain() (-s EXPORTED_RUNTIME_METHODS=...).
Po wykonaniu tych czynności mogłem udostępniać wygenerowane pliki za pomocą statycznego serwera WWW, inicjować WebUSB i ręcznie uruchamiać te pliki wykonywalne HTML za pomocą Narzędzi deweloperskich.
![Zrzut ekranu przedstawiający okno Chrome z otwartymi Narzędziami deweloperskimi na stronie „testlibusb” hostowanej lokalnie. Konsola Narzędzi deweloperskich ocenia `navigator.usb.requestDevice({ filters: [] })`, co spowodowało wyświetlenie prośby o przyznanie uprawnień i obecnie prosi użytkownika o wybranie urządzenia USB, które ma zostać udostępnione stronie. Obecnie wybrany jest ILCE-6600 (aparat Sony).](https://web.developers.google.cn/static/articles/porting-libusb-to-webusb/image/screenshot-showing-chrom-b19a9e4e1b916.png?hl=pl)
![Zrzut ekranu przedstawiający następny krok z otwartymi Narzędziami deweloperskimi. Po wybraniu urządzenia konsola oceniła nowe wyrażenie `Module.callMain(['-v'])`, które uruchomiło aplikację `testlibusb` w trybie szczegółowym. Dane wyjściowe zawierają różne szczegółowe informacje o wcześniej podłączonej kamerze USB: producent Sony, produkt ILCE-6600, numer seryjny, konfiguracja itp.](https://web.developers.google.cn/static/articles/porting-libusb-to-webusb/image/screenshot-the-next-step-bd4e4c7f85be7.png?hl=pl)
Może nie wygląda to imponująco, ale gdy przenosi się biblioteki na nową platformę, osiągnięcie etapu, na którym po raz pierwszy generują prawidłowe dane wyjściowe, jest bardzo ekscytujące.
Korzystanie z portu
Jak wspomniano powyżej, port zależy od kilku funkcji Emscripten, które obecnie muszą być włączone na etapie łączenia aplikacji. Jeśli chcesz używać tego portu libusb we własnej aplikacji, wykonaj te czynności:
- Pobierz najnowszą wersję libusb jako archiwum w ramach kompilacji lub dodaj ją jako moduł podrzędny git w projekcie.
- Uruchom
autoreconf -fivw folderzelibusb. - Uruchom
emconfigure ./configure –host=wasm32 –prefix=/some/installation/path, aby zainicjować projekt na potrzeby kompilacji skrośnej i ustawić ścieżkę, w której chcesz umieścić utworzone artefakty. - Uruchom
emmake make install. - Skonfiguruj aplikację lub bibliotekę wyższego poziomu tak, aby wyszukiwała libusb w wybranej wcześniej ścieżce.
- Dodaj do argumentów linku aplikacji te flagi:
--bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.
Biblioteka ma obecnie kilka ograniczeń:
- Brak obsługi anulowania przeniesienia. Jest to ograniczenie WebUSB, które z kolei wynika z braku możliwości anulowania transferu między platformami w samej bibliotece libusb.
- Brak obsługi transferu izochronicznego. Dodanie go nie powinno być trudne, jeśli jako przykładu użyjesz implementacji istniejących trybów przesyłania, ale jest to rzadki tryb i nie miałem żadnych urządzeń, na których mógłbym go przetestować, więc na razie pozostawiłem go jako nieobsługiwany. Jeśli masz takie urządzenia i chcesz dodać je do biblioteki, prześlij żądanie pull.
- Wspomniane wcześniej ograniczenia dotyczące wielu platform. Te ograniczenia są nakładane przez systemy operacyjne, więc nie możemy nic zrobić, poza poproszeniem użytkowników o zastąpienie sterownika lub uprawnień. Jeśli jednak przenosisz urządzenia HID lub szeregowe, możesz postępować zgodnie z przykładem libusb i przenieść inną bibliotekę do innego interfejsu API Fugu. Możesz na przykład przenieść bibliotekę C hidapi do WebHID i całkowicie uniknąć problemów związanych z niskopoziomowym dostępem do USB.
Podsumowanie
W tym poście pokazaliśmy, jak za pomocą interfejsów API Emscripten, Asyncify i Fugu można przenieść do internetu nawet biblioteki niskiego poziomu, takie jak libusb, za pomocą kilku sztuczek integracyjnych.
Przenoszenie tak ważnych i powszechnie używanych bibliotek niskiego poziomu jest szczególnie korzystne, ponieważ umożliwia przenoszenie do internetu bibliotek wyższego poziomu, a nawet całych aplikacji. Dzięki temu funkcje, które wcześniej były ograniczone do użytkowników 1 lub 2 platform, są dostępne na wszystkich rodzajach urządzeń i systemów operacyjnych, a użytkownicy mogą z nich korzystać po kliknięciu linku.
W następnym poście opiszę kroki związane z tworzeniem wersji demonstracyjnej gPhoto2 w internecie, która nie tylko pobiera informacje o urządzeniu, ale też intensywnie korzysta z funkcji przesyłania libusb. Mam nadzieję, że przykład libusb był dla Ciebie inspirujący i wypróbujesz wersję demonstracyjną, pobawisz się samą biblioteką, a może nawet przeniesiesz inną powszechnie używaną bibliotekę do jednego z interfejsów API Fugu.