Hier erfahren Sie, wie Code, der mit externen Geräten interagiert, mit WebAssembly und Fugu APIs ins Web übertragen werden kann.
In einem vorherigen Beitrag habe ich gezeigt, wie Apps, die Dateisystem-APIs verwenden, mit der File System Access API, WebAssembly und Asyncify ins Web übertragen werden. Ich möchte jetzt mit demselben Thema fortfahren: der Integration von Fugu APIs in WebAssembly und dem Portieren von Apps ins Web, ohne wichtige Funktionen zu verlieren.
Ich zeige, wie Apps, die mit USB-Geräten kommunizieren, ins Web übertragen werden können, indem libusb – eine beliebte in C geschriebene USB-Bibliothek – in WebAssembly (über Emscripten), Asyncify und WebUSB übertragen wird.
Das Wichtigste zuerst: eine Demo
Das Wichtigste beim Portieren einer Bibliothek ist die Auswahl der richtigen Demo. Diese sollte die Funktionen der portierten Bibliothek demonstrieren, sie auf unterschiedliche Weise testen lassen und gleichzeitig visuell ansprechend sein.
Meine Idee war die Fernbedienung für Spiegelreflexkameras. Insbesondere das Open-Source-Projekt gPhoto2 ist schon lange in diesem Bereich aktiv und hat die Unterstützung für eine Vielzahl von Digitalkameras durch Reverse-Engineering implementiert. Es unterstützt mehrere Protokolle, aber das, das mich am meisten interessierte, war die USB-Unterstützung, die über libusb erfolgt.
Ich werde die Schritte zum Erstellen dieser Demo in zwei Teilen beschreiben. In diesem Blogpost werde ich beschreiben, wie ich libusb selbst portiert habe und welche Tricks erforderlich sind, um andere beliebte Bibliotheken in Fugu-APIs zu portieren. Im zweiten Beitrag gehe ich ausführlich auf die Portierung und Integration von gPhoto2 selbst ein.
Letztendlich habe ich eine funktionierende Webanwendung bekommen, die eine Vorschau des Livefeeds von einer digitalen Spiegelreflexkamera anzeigt und die Einstellungen über USB steuern kann. Sie können sich die Live-Demo oder die aufgezeichnete Demo ansehen, bevor Sie sich mit den technischen Details beschäftigen:
Hinweis zu kameraspezifischen Macken
Sie haben vielleicht bemerkt, dass das Ändern der Einstellungen im Video etwas dauert. Wie bei den meisten anderen Problemen, die Sie möglicherweise sehen, ist dies nicht auf die Leistung von WebAssembly oder WebUSB zurückzuführen, sondern darauf, wie gPhoto2 mit der für die Demo ausgewählten Kamera interagiert.
Sony A6600 verfügt nicht über eine API, um Werte wie ISO, Blende oder Belichtungszeit direkt einzustellen, sondern bietet nur Befehle zum Erhöhen oder Verringern um die angegebene Anzahl von Schritten. Erschwerend kommt hinzu, dass auch keine Liste der tatsächlich unterstützten Werte zurückgegeben wird. Die Liste scheint für viele Sony-Kameramodelle hartcodiert zu sein.
Wenn Sie einen dieser Werte festlegen, hat gPhoto2 keine andere Wahl, als
- Machen Sie einen oder mehrere Schritte in Richtung des ausgewählten Werts.
- Warten Sie, bis die Kamera die Einstellungen aktualisiert hat.
- Lesen Sie den Wert ab, den die Kamera tatsächlich erfasst hat.
- Achten Sie darauf, dass der letzte Schritt nicht über den gewünschten Wert hinausgesprungen und nicht das Ende oder den Anfang der Liste umschlossen hat.
- Wiederholen.
Es kann einige Zeit dauern, aber wenn der Wert tatsächlich von der Kamera unterstützt wird, kommt er dorthin. Andernfalls wird er beim nächsten unterstützten Wert beendet.
Andere Kameras haben wahrscheinlich unterschiedliche Einstellungen, zugrunde liegende APIs und Besonderheiten. Denken Sie daran, dass gPhoto2 ein Open-Source-Projekt ist und dass entweder automatisierte oder manuelle Tests aller da draußen verfügbaren Kameramodelle einfach nicht realisierbar sind. Daher sind detaillierte Problemberichte und PRs immer willkommen (aber stellen Sie sicher, dass Sie die Probleme zuerst mit dem offiziellen gPhoto2-Kunden reproduzieren).
Wichtige Hinweise zur plattformübergreifenden Kompatibilität
Leider wird unter Windows allen „bekannten“ Geräten, einschließlich DSLR-Kameras, ein Systemtreiber zugewiesen, der nicht mit WebUSB kompatibel ist. Wenn Sie die Demo unter Windows ausprobieren möchten, müssen Sie mit einem Tool wie Zadig den Treiber für die angeschlossene DSLR entweder auf WinUSB oder libusb umstellen. Dieser Ansatz funktioniert bei mir und vielen anderen Nutzern gut, aber Sie sollten ihn auf eigenes Risiko verwenden.
Unter Linux müssen Sie wahrscheinlich benutzerdefinierte Berechtigungen festlegen, um den Zugriff auf Ihre DSLR über WebUSB zu ermöglichen. Dies hängt jedoch von Ihrer Distribution ab.
Unter macOS und Android sollte die Demo ohne weitere Maßnahmen funktionieren. Wenn du es auf einem Android-Smartphone ausprobierst, stelle sicher, dass du in das Querformat umschaltest, da ich nicht viel Mühe gegeben habe, es responsiv zu gestalten (PRs sind willkommen!):
Eine ausführlichere Anleitung zur plattformübergreifenden Verwendung von WebUSB finden Sie im Abschnitt Plattformspezifische Überlegungen des Artikels „Gerät für WebUSB erstellen“.
Hinzufügen eines neuen ‑Backends zu libusb
Kommen wir nun zu den technischen Details. Es ist zwar möglich, eine ähnliche Shim-API wie libusb bereitzustellen (was bereits von anderen getan wurde) und andere Anwendungen damit zu verknüpfen, aber dieser Ansatz ist fehleranfällig und erschwert jede weitere Erweiterung oder Wartung. Ich wollte alles richtig machen, so dass es in Zukunft in die libusb einfließen und mit dieser verschmolzen werden kann.
Glücklicherweise steht in der libusb-README-Datei Folgendes:
„libusb ist intern so abstrahiert, dass es hoffentlich auf andere Betriebssysteme portiert werden kann. Weitere Informationen finden Sie in der Datei PORTING.“
libusb ist so strukturiert, dass die öffentliche API von den "Back-Ends" getrennt ist. Diese Backends sind für das Auflisten, Öffnen, Schließen und die eigentliche Kommunikation mit den Geräten über die Low-Level-APIs des Betriebssystems verantwortlich. libusb abstrahiert bereits die Unterschiede zwischen Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku und Solaris und funktioniert auf all diesen Plattformen.
Ich musste ein weiteres Backend für das „Betriebssystem“ Emscripten+WebUSB hinzufügen. Die Implementierungen für diese Backends befinden sich im Ordner 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
Jedes Back-End enthält den Header libusbi.h
mit gängigen Typen und Hilfsprogrammen und muss eine usbi_backend
-Variable vom Typ usbi_os_backend
verfügbar machen. Das Windows-Backend sieht beispielsweise so aus:
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),
};
Wenn wir uns die Eigenschaften ansehen, sehen wir, dass die Struktur den Namen des Backends, eine Reihe seiner Funktionen, Handler für verschiedene Low-Level-USB-Vorgänge in Form von Funktionszeigern und schließlich Größen zum Speichern privater Daten auf Geräte-, Kontext- und Übertragungsebene enthält.
Die privaten Datenfelder sind zumindest zum Speichern von OS-Handles für all diese Dinge nützlich, da wir ohne Handles nicht wissen, auf welches Element sich ein bestimmter Vorgang bezieht. Bei der Webimplementierung sind die Betriebssystem-Handle die zugrunde liegenden WebUSB-JavaScript-Objekte. Die natürliche Darstellung und Speicherung in Emscripten erfolgt über die Klasse emscripten::val
, die im Rahmen von Embind (das Bindungssystem von Emscripten) bereitgestellt wird.
Die meisten Backends im Ordner sind in C implementiert, einige aber in C++. Embind funktioniert nur mit C++, daher war die Entscheidung für mich getroffen. Ich habe libusb/libusb/os/emscripten_webusb.cpp
mit der erforderlichen Struktur und sizeof(val)
für die privaten Datenfelder hinzugefügt:
#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),
};
WebUSB-Objekte als Geräte-Handle speichern
libusb stellt gebrauchsfertige Verweise auf den zugewiesenen Bereich für private Daten bereit. Um mit diesen Pointern als val
-Instanzen zu arbeiten, habe ich kleine Hilfsfunktionen hinzugefügt, die sie vor Ort erstellen, als Referenzen abrufen und Werte herauskopieren:
// 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))) {}
};
Asynchrone Web-APIs in synchronen C-Kontexten
Es wurde nun eine Möglichkeit benötigt, asynchrone WebUSB APIs zu verarbeiten, bei denen libusb synchrone Vorgänge erwartet. Dazu könnte ich Asyncify oder genauer gesagt die Embind-Integration über val::await()
verwenden.
Außerdem wollte ich WebUSB-Fehler richtig behandeln und in libusb-Fehlercodes konvertieren. Leider gibt es in Embind derzeit keine Möglichkeit, JavaScript-Ausnahmen oder Promise
-Ablehnungen von der C++-Seite aus zu verarbeiten. Dieses Problem lässt sich umgehen, indem eine Ablehnung auf der JavaScript-Seite abgefangen und das Ergebnis in ein { error, value }
-Objekt umgewandelt wird, das jetzt sicher von der C++-Seite aus geparst werden kann. Dazu habe ich eine Kombination aus dem EM_JS
-Makro und den Emval.to{Handle, Value}
APIs verwendet:
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()};
}
};
Jetzt kann ich promise_result::await()
auf alle Promise
anwenden, die von WebUSB-Vorgängen zurückgegeben werden, und die Felder error
und value
separat prüfen.
Das Abrufen eines val
, das ein USBDevice
von libusb_device_handle
darstellt, das Aufrufen der Methode open()
, das Warten auf das Ergebnis und das Zurückgeben eines Fehlercodes als libusb-Statuscode sieht beispielsweise so aus:
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;
}
Geräteaufzählung
Bevor ich ein Gerät öffnen kann, muss libusb natürlich eine Liste der verfügbaren Geräte abrufen. Das Back-End muss diesen Vorgang über einen get_device_list
-Handler implementieren.
Das Problem ist, dass es im Gegensatz zu anderen Plattformen aus Sicherheitsgründen keine Möglichkeit gibt, alle verbundenen USB-Geräte im Web aufzulisten. Stattdessen wird der Fluss in zwei Teile aufgeteilt. Zuerst fordert die Webanwendung über navigator.usb.requestDevice()
Geräte mit bestimmten Eigenschaften an. Der Nutzer wählt dann manuell aus, welches Gerät er freigeben möchte, oder lehnt die Berechtigungsanfrage ab. Anschließend werden in der Anwendung die bereits genehmigten und verbundenen Geräte über navigator.usb.getDevices()
aufgelistet.
Zuerst habe ich versucht, requestDevice()
direkt in der Implementierung des get_device_list
-Handlers zu verwenden. Das Einblenden einer Berechtigungsanfrage mit einer Liste der verbundenen Geräte gilt jedoch als sensibler Vorgang und muss durch eine Nutzerinteraktion ausgelöst werden (z. B. durch Klicken auf eine Schaltfläche auf einer Seite). Andernfalls wird immer ein abgelehntes Promise zurückgegeben. Da libusb-Anwendungen die verbundenen Geräte häufig beim Start der Anwendung auflisten möchten, war die Verwendung von requestDevice()
keine Option.
Stattdessen musste ich die Aufrufe von navigator.usb.requestDevice()
dem Endentwickler überlassen und nur die bereits genehmigten Geräte von navigator.usb.getDevices()
freigeben:
// 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;
}
Der größte Teil des Back-End-Codes verwendet val
und promise_result
auf ähnliche Weise, wie oben gezeigt. Es gibt noch einige weitere interessante Hacks im Code zur Datenverarbeitung, aber diese Implementierungsdetails sind für diesen Artikel weniger wichtig. Sehen Sie sich den Code und die Kommentare auf GitHub an, wenn Sie daran interessiert sind.
Portierung von Ereignisschleifen in das Web
Ein weiterer Teil des libusb-Ports, den ich erwähnen möchte, ist die Ereignisbehandlung. Wie im vorherigen Artikel beschrieben, sind die meisten APIs in Systemsprachen wie C synchron. Die Ereignisbehandlung ist keine Ausnahme. Sie wird in der Regel über einen unendlichen Loop implementiert, der von einer Reihe externer E/A-Quellen „pollt“ (d. h. versucht, Daten zu lesen oder die Ausführung blockiert, bis Daten verfügbar sind) und, wenn mindestens eine davon antwortet, diese als Ereignis an den entsprechenden Handler weitergibt. Sobald der Handler fertig ist, kehrt die Steuerung zur Schleife zurück und pausiert für eine weitere Abfrage.
Bei diesem Ansatz gibt es jedoch einige Probleme im Web.
Erstens stellt WebUSB keine Roh-Handle der zugrunde liegenden Geräte bereit und kann dies auch nicht. Daher ist eine direkte Abfrage nicht möglich. Zweitens verwendet libusb die APIs eventfd
und pipe
für andere Ereignisse sowie für Übertragungen auf Betriebssystemen ohne unformatierte Geräte-Aliasse. eventfd
wird derzeit in Emscripten nicht unterstützt. pipe
wird zwar unterstützt, entspricht derzeit jedoch nicht der Spezifikation und kann nicht auf Ereignisse warten.
Das größte Problem ist schließlich, dass das Web eine eigene Ereignisschleife hat. Diese globale Ereignisschleife wird für alle externen E/A-Vorgänge verwendet (einschließlich fetch()
, Timer oder in diesem Fall WebUSB) und ruft Ereignis- oder Promise
-Handler auf, sobald die entsprechenden Vorgänge abgeschlossen sind. Wenn Sie eine weitere verschachtelte, endlose Ereignisschleife ausführen, wird die Ereignisschleife des Browsers blockiert. Das bedeutet, dass nicht nur die Benutzeroberfläche nicht mehr reagiert, sondern auch, dass der Code nie Benachrichtigungen für die I/O-Ereignisse erhält, auf die er wartet. Dies führt in der Regel zu einem Deadlock, und das geschah auch, als ich versucht habe, Libusb in einer Demo zu verwenden. Die Seite ist eingefroren.
Wie bei anderen blockierenden E/A-Vorgängen müssen Entwickler eine Möglichkeit finden, solche Ereignisschleifen auszuführen, ohne den Hauptthread zu blockieren, um sie ins Web zu portieren. Eine Möglichkeit besteht darin, die Anwendung so zu refaktorisieren, dass E/A-Ereignisse in einem separaten Thread verarbeitet und die Ergebnisse an den Hauptthread zurückgegeben werden. Die andere Möglichkeit besteht darin, Asyncify zu verwenden, um die Schleife zu pausieren und nicht blockierend auf Ereignisse zu warten.
Ich wollte weder an libusb noch an gPhoto2 größere Änderungen vornehmen und da ich Asyncify bereits für die Promise
-Integration verwendet habe, habe ich mich für diesen Weg entschieden. Um eine blockierende Variante von poll()
zu simulieren, habe ich für den ersten Proof of Concept eine Schleife wie unten gezeigt verwendet:
#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
Dabei geschieht Folgendes:
- Ruft
poll()
auf, um zu prüfen, ob vom Backend bereits Ereignisse gemeldet wurden. Falls ja, wird die Schleife beendet. Andernfalls gibt die Emscripten-Implementierung vonpoll()
sofort0
zurück. - Ruft
emscripten_sleep(0)
auf. Diese Funktion verwendet im Hintergrund Asyncify undsetTimeout()
und wird hier genutzt, um die Steuerung der Hauptereignisschleife des Browsers zu ermöglichen. So kann der Browser alle Nutzerinteraktionen und I/O-Ereignisse verarbeiten, einschließlich WebUSB. - Prüfen Sie, ob das angegebene Zeitlimit bereits abgelaufen ist, und setzen Sie andernfalls die Schleife fort.
Wie im Kommentar erwähnt, war dieser Ansatz nicht optimal, da der gesamte Aufrufstapel mit Asyncify gespeichert und wiederhergestellt wurde, auch wenn noch keine USB-Ereignisse zu verarbeiten waren (was die meiste Zeit der Fall ist). Außerdem hat setTimeout()
selbst in modernen Browsern eine minimale Dauer von 4 ms. Trotzdem hat es gut funktioniert, um einen Livestream mit 13–14 fps mit digitalen Spiegelreflexkameras als Proof of Concept zu erstellen.
Später beschloss ich, es durch den Einsatz des Browser-Ereignissystems zu verbessern. Es gibt mehrere Möglichkeiten, wie diese Implementierung weiter verbessert werden könnte, aber jetzt habe ich beschlossen, benutzerdefinierte Ereignisse direkt für das globale Objekt auszugeben, ohne sie mit einer bestimmten Libusb-Datenstruktur zu verknüpfen. Dazu habe ich den folgenden Mechanismus zum Warten und Benachrichtigen verwendet, der auf dem EM_ASYNC_JS
-Makro basiert:
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);
}
});
Die Funktion em_libusb_notify()
wird immer dann verwendet, wenn libusb versucht, ein Ereignis wie den Abschluss einer Datenübertragung zu melden:
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
}
Der Teil em_libusb_wait()
wird verwendet, um den Asyncify-Ruhemodus zu beenden, wenn entweder ein em-libusb
-Ereignis empfangen wird oder die Zeitüberschreitung abgelaufen ist:
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;
}
Durch die deutliche Reduzierung von Ruhe- und Aufwachvorgängen wurden die Effizienzprobleme der früheren emscripten_sleep()
-basierten Implementierung behoben und der Durchsatz der DSLR-Demo von 13–14 fps auf über 30 fps erhöht, was für einen flüssigen Livefeed ausreicht.
System erstellen und ersten Test durchführen
Nachdem das Backend fertig war, musste ich es Makefile.am
und configure.ac
hinzufügen. Interessant ist hier nur die Änderung der Emscripten-spezifischen Flags:
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']"
;;
Erstens haben ausführbare Dateien auf Unix-Plattformen normalerweise keine Dateiendungen. Emscripten liefert jedoch je nach angeforderter Erweiterung eine andere Ausgabe. Ich verwende AC_SUBST(EXEEXT, …)
, um die Erweiterung der ausführbaren Datei in .html
zu ändern, damit jede ausführbare Datei in einem Paket – Tests und Beispiele – zu einer HTML-Datei mit der Standard-Shell von Emscripten wird, die das Laden und Instanziieren von JavaScript und WebAssembly übernimmt.
Zweitens: Da ich Embind und Asyncify verwende, muss ich diese Funktionen (--bind -s ASYNCIFY
) aktivieren und über die Linkerparameter ein dynamisches Speicherwachstum (-s ALLOW_MEMORY_GROWTH
) zulassen. Leider gibt es keine Möglichkeit für eine Bibliothek, diese Flags an die Verknüpfung zu melden. Daher muss jede Anwendung, die diesen Libusb-Port verwendet, die gleichen Verknüpfungs-Flags auch ihrer Build-Konfiguration hinzufügen.
Wie bereits erwähnt, muss bei WebUSB die Geräteaufzählung über eine Nutzergeste erfolgen. In den Beispielen und Tests von libusb wird davon ausgegangen, dass Geräte beim Start aufgezählt werden können, und es kommt ohne Änderungen zu einem Fehler. Stattdessen musste ich die automatische Ausführung (-s INVOKE_RUN=0
) deaktivieren und die manuelle callMain()
-Methode (-s EXPORTED_RUNTIME_METHODS=...
) freigeben.
Nachdem das alles erledigt war, konnte ich die generierten Dateien mit einem statischen Webserver bereitstellen, WebUSB initialisieren und die ausführbaren HTML-Dateien mithilfe der Entwicklertools manuell ausführen.
Es sieht nicht nach viel aus, aber bei der Portierung von Bibliotheken auf eine neue Plattform ist es ziemlich spannend, zum ersten Mal eine gültige Ausgabe zu generieren.
Anschluss verwenden
Wie oben erwähnt, hängt der Port von einigen Emscripten-Funktionen ab, die derzeit in der Verknüpfungsphase der Anwendung aktiviert werden müssen. Wenn Sie diesen libusb-Port in Ihrer eigenen Anwendung verwenden möchten, gehen Sie so vor:
- Laden Sie die aktuelle Version von libusb herunter. Sie können sie entweder als Archiv als Teil Ihres Builds herunterladen oder Ihrem Projekt als Git-Submodul hinzufügen.
- Führen Sie
autoreconf -fiv
im Ordnerlibusb
aus. - Führen Sie
emconfigure ./configure –host=wasm32 –prefix=/some/installation/path
aus, um das Projekt für die plattformübergreifende Kompilierung zu initialisieren und einen Pfad festzulegen, unter dem die erstellten Artefakte abgelegt werden sollen. - Führen Sie
emmake make install
aus. - Suchen Sie in der Anwendung oder Bibliothek auf höherer Ebene unter dem zuvor ausgewählten Pfad nach der Libusb-Datei.
- Fügen Sie den Linkargumenten Ihrer Anwendung die folgenden Flags hinzu:
--bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH
.
Die Bibliothek hat derzeit einige Einschränkungen:
- Keine Unterstützung bei Stornierung von Übertragungen. Dies ist eine Einschränkung von WebUSB, die wiederum auf das Fehlen einer plattformübergreifenden Übertragungsstornierung in libusb selbst zurückzuführen ist.
- Keine Unterstützung für isochrone Übertragungen. Es sollte nicht schwierig sein, ihn hinzuzufügen, indem man die Implementierung vorhandener Übertragungsmodi als Beispiel verwendet. Es ist jedoch auch ein etwas seltener Modus und ich hatte keine Geräte, auf denen ich ihn testen konnte. Daher habe ich ihn vorerst nicht unterstützt. Wenn Sie solche Geräte haben und zur Bibliothek beitragen möchten, sind PRs willkommen.
- Die oben genannten plattformübergreifenden Einschränkungen. Diese Einschränkungen werden von den Betriebssystemen auferlegt. Wir können hier also nicht viel tun, außer die Nutzer zu bitten, den Treiber oder die Berechtigungen zu überschreiben. Wenn Sie jedoch HID- oder serielle Geräte portieren, können Sie dem Beispiel für libusb folgen und eine andere Bibliothek auf eine andere Fugu API portieren. Sie können beispielsweise eine C-Bibliothek hidapi auf WebHID portieren und diese Probleme, die mit dem Low-Level-USB-Zugriff verbunden sind, vollständig umgehen.
Fazit
In diesem Beitrag habe ich gezeigt, wie mithilfe der Emscripten-, Asyncify- und Fugu-APIs selbst Low-Level-Bibliotheken wie libusb mit einigen Integrationstricks ins Web übertragen werden können.
Das Portieren solcher wichtigen und weit verbreiteten Low-Level-Bibliotheken ist besonders lohnenswert, da dadurch auch Bibliotheken höherer Ebene oder sogar ganze Anwendungen ins Web gebracht werden können. So können Funktionen, die bisher nur Nutzern einer oder zwei Plattformen zur Verfügung standen, jetzt auf allen Arten von Geräten und Betriebssystemen genutzt werden.
Im nächsten Beitrag erkläre ich, wie Sie die gPhoto2-Webdemo erstellen, die nicht nur Geräteinformationen abrufen, sondern auch die Übertragungsfunktion von libusb intensiv nutzt. In der Zwischenzeit hoffe ich, dass Sie das Libusb-Beispiel inspirieren konnten. Sie werden die Demo ausprobieren, mit der Bibliothek selbst experimentieren oder vielleicht sogar eine andere weit verbreitete Bibliothek in eine der Fugu-APIs übertragen.