Découvrez comment le code qui interagit avec des appareils externes peut être porté sur le Web avec les API WebAssembly et Fugu.
Dans un article précédent, j'ai expliqué comment porter des applications utilisant des API de système de fichiers sur le Web avec l'API File System Access, WebAssembly et Asyncify. Je souhaite maintenant poursuivre sur le même sujet, à savoir l'intégration des API Fugu à WebAssembly et le portage d'applications sur le Web sans perdre de fonctionnalités importantes.
Je vais vous montrer comment les applications qui communiquent avec des appareils USB peuvent être portées sur le Web en portant libusb (une bibliothèque USB populaire écrite en C) vers WebAssembly (via Emscripten), Asyncify et WebUSB.
Avant toute chose : une démonstration
La chose la plus importante à faire lors du portage d'une bibliothèque est de choisir la bonne démo, c'est-à-dire une démo qui met en valeur les capacités de la bibliothèque portée, qui vous permet de la tester de différentes manières et qui est visuellement attrayante.
L'idée que j'ai choisie était la télécommande pour reflex numérique. En particulier, le projet Open Source gPhoto2 existe depuis assez longtemps pour rétroconcevoir et implémenter la compatibilité avec une grande variété d'appareils photo numériques. Il est compatible avec plusieurs protocoles, mais celui qui m'intéressait le plus était la compatibilité USB, qu'il assure via libusb.
Je vais décrire les étapes de création de cette démonstration en deux parties. Dans cet article de blog, je vais décrire comment j'ai porté libusb et quelles astuces peuvent être nécessaires pour porter d'autres bibliothèques populaires vers les API Fugu. Dans le deuxième article, je détaillerai le portage et l'intégration de gPhoto2.
Au final, j'ai obtenu une application Web fonctionnelle qui prévisualise le flux en direct d'un reflex numérique et peut contrôler ses paramètres via USB. N'hésitez pas à consulter la démonstration en direct ou préenregistrée avant de lire les détails techniques :
Remarque sur les particularités de certaines caméras
Vous avez peut-être remarqué que la modification des paramètres prend un certain temps dans la vidéo. Comme pour la plupart des autres problèmes que vous pourriez rencontrer, cela n'est pas dû aux performances de WebAssembly ou de WebUSB, mais à la façon dont gPhoto2 interagit avec l'appareil photo spécifique choisi pour la démonstration.
Le Sony a6600 n'expose pas d'API permettant de définir directement des valeurs telles que la sensibilité ISO, l'ouverture ou la vitesse d'obturation. Il ne fournit que des commandes permettant de les augmenter ou de les diminuer du nombre d'étapes spécifié. Pour compliquer encore les choses, elle ne renvoie pas non plus la liste des valeurs acceptées. La liste renvoyée semble être codée en dur sur de nombreux modèles d'appareils photo Sony.
Lorsque vous définissez l'une de ces valeurs, gPhoto2 n'a pas d'autre choix que de :
- Faites un pas (ou quelques-uns) dans la direction de la valeur choisie.
- Patientez un peu, le temps que la caméra mette à jour les paramètres.
- Lisez la valeur sur laquelle la caméra s'est réellement arrêtée.
- Vérifiez que la dernière étape n'a pas sauté la valeur souhaitée ni bouclé à la fin ou au début de la liste.
- Recommencez.
Cela peut prendre un certain temps, mais si la valeur est effectivement acceptée par la caméra, elle sera atteinte. Sinon, elle s'arrêtera sur la valeur acceptée la plus proche.
D'autres caméras auront probablement des ensembles de paramètres, des API sous-jacentes et des particularités différents. N'oubliez pas que gPhoto2 est un projet Open Source et qu'il n'est tout simplement pas possible de tester automatiquement ou manuellement tous les modèles d'appareils photo existants. Les rapports de problèmes détaillés et les demandes d'extraction sont donc toujours les bienvenus (mais veillez d'abord à reproduire les problèmes avec le client gPhoto2 officiel).
Remarques importantes sur la compatibilité multiplate-forme
Malheureusement, sous Windows, tous les appareils "connus", y compris les appareils photo reflex numériques, se voient attribuer un pilote système qui n'est pas compatible avec WebUSB. Si vous souhaitez essayer la démo sur Windows, vous devrez utiliser un outil tel que Zadig pour remplacer le pilote du reflex numérique connecté par WinUSB ou libusb. Cette approche fonctionne bien pour moi et de nombreux autres utilisateurs, mais vous devez l'utiliser à vos propres risques.
Sous Linux, vous devrez probablement définir des autorisations personnalisées pour autoriser l'accès à votre reflex numérique via WebUSB, bien que cela dépende de votre distribution.
Sous macOS et Android, la démo devrait fonctionner immédiatement. Si vous l'essayez sur un téléphone Android, veillez à passer en mode Paysage, car je n'ai pas fait beaucoup d'efforts pour le rendre responsive (les PR sont les bienvenues !) :
Pour en savoir plus sur l'utilisation multiplate-forme de WebUSB, consultez la section "Considérations spécifiques à la plate-forme" de l'article "Créer un appareil pour WebUSB".
Ajouter un backend à libusb
Passons maintenant aux détails techniques. Bien qu'il soit possible de fournir une API shim semblable à libusb (ce qui a déjà été fait par d'autres) et d'y associer d'autres applications, cette approche est sujette aux erreurs et complique l'extension ou la maintenance. Je voulais faire les choses correctement, de manière à pouvoir éventuellement contribuer en amont et fusionner le code dans libusb à l'avenir.
Heureusement, le fichier README de libusb indique :
"libusb est extrait en interne de manière à pouvoir être porté sur d'autres systèmes d'exploitation. Pour en savoir plus, consultez le fichier PORTING."
libusb est structuré de manière à ce que l'API publique soit distincte des "backends". Ces backends sont chargés de lister, d'ouvrir, de fermer et de communiquer avec les appareils via les API de bas niveau du système d'exploitation. C'est ainsi que libusb élimine déjà les différences entre Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku et Solaris, et fonctionne sur toutes ces plates-formes.
J'ai dû ajouter un autre backend pour le "système d'exploitation" Emscripten+WebUSB. Les implémentations de ces backends se trouvent dans le dossier 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
Chaque backend inclut l'en-tête libusbi.h avec des types et des assistants courants, et doit exposer une variable usbi_backend de type usbi_os_backend. Par exemple, voici à quoi ressemble le backend 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),
};
En examinant les propriétés, nous pouvons voir que la structure inclut le nom du backend, un ensemble de ses fonctionnalités, des gestionnaires pour diverses opérations USB de bas niveau sous la forme de pointeurs de fonction et, enfin, des tailles à allouer pour stocker les données privées au niveau de l'appareil, du contexte ou du transfert.
Les champs de données privés sont utiles au moins pour stocker les handles d'OS pour tous ces éléments, car sans handles, nous ne savons pas à quel élément s'applique une opération donnée. Dans l'implémentation Web, les handles de l'OS sont les objets JavaScript WebUSB sous-jacents. La méthode naturelle pour les représenter et les stocker dans Emscripten consiste à utiliser la classe emscripten::val, qui est fournie dans le cadre d'Embind (le système de liaisons d'Emscripten).
La plupart des backends du dossier sont implémentés en C, mais quelques-uns sont implémentés en C++. Embind ne fonctionne qu'avec C++. Le choix a donc été fait pour moi et j'ai ajouté libusb/libusb/os/emscripten_webusb.cpp avec la structure requise et avec sizeof(val) pour les champs de données privés :
#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),
};
Stocker des objets WebUSB en tant que poignées d'appareil
libusb fournit des pointeurs prêts à l'emploi vers la zone allouée aux données privées. Pour utiliser ces pointeurs en tant qu'instances val, j'ai ajouté de petits assistants qui les construisent sur place, les récupèrent en tant que références et déplacent les valeurs :
// 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))) {}
};
API Web asynchrones dans des contextes C synchrones
Il fallait maintenant trouver un moyen de gérer les API WebUSB asynchrones là où libusb attend des opérations synchrones. Pour ce faire, je pourrais utiliser Asyncify, ou plus précisément son intégration Embind via val::await().
Je voulais également gérer correctement les erreurs WebUSB et les convertir en codes d'erreur libusb, mais Embind ne permet actuellement pas de gérer les exceptions JavaScript ou les rejets Promise du côté C++. Pour contourner ce problème, vous pouvez intercepter un rejet côté JavaScript et convertir le résultat en un objet { error, value } qui peut désormais être analysé de manière sécurisée côté C++. Pour ce faire, j'ai combiné la macro EM_JS et les 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()};
}
};
Je peux maintenant utiliser promise_result::await() sur n'importe quel Promise renvoyé par les opérations WebUSB et inspecter ses champs error et value séparément.
Par exemple, pour récupérer un val représentant un USBDevice à partir de libusb_device_handle, appeler sa méthode open(), attendre son résultat et renvoyer un code d'erreur en tant que code d'état libusb, procédez comme suit :
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;
}
Énumération des appareils
Bien sûr, avant de pouvoir ouvrir un appareil, libusb doit récupérer une liste des appareils disponibles. Le backend doit implémenter cette opération via un gestionnaire get_device_list.
La difficulté est que, contrairement à d'autres plates-formes, il n'est pas possible d'énumérer tous les appareils USB connectés sur le Web pour des raisons de sécurité. Le flux est divisé en deux parties. Tout d'abord, l'application Web demande des appareils dotés de propriétés spécifiques via navigator.usb.requestDevice(), et l'utilisateur choisit manuellement l'appareil qu'il souhaite exposer ou refuse l'invite d'autorisation. Ensuite, l'application liste les appareils déjà approuvés et connectés via navigator.usb.getDevices().
Au début, j'ai essayé d'utiliser requestDevice() directement dans l'implémentation du gestionnaire get_device_list. Toutefois, l'affichage d'une invite d'autorisation avec une liste d'appareils connectés est considéré comme une opération sensible et doit être déclenché par une interaction de l'utilisateur (comme un clic sur un bouton sur une page). Sinon, il renvoie toujours une promesse refusée. Les applications libusb peuvent souvent vouloir lister les appareils connectés au démarrage de l'application. L'utilisation de requestDevice() n'était donc pas une option.
Au lieu de cela, j'ai dû laisser l'appel de navigator.usb.requestDevice() au développeur final et n'exposer que les appareils déjà approuvés de 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;
}
La majeure partie du code du backend utilise val et promise_result de la même manière que celle décrite ci-dessus. Il existe d'autres astuces intéressantes dans le code de gestion du transfert de données, mais ces détails d'implémentation sont moins importants pour les besoins de cet article. Si vous le souhaitez, n'hésitez pas à consulter le code et les commentaires sur GitHub.
Porter des boucles d'événements sur le Web
Je souhaite aborder un autre aspect du port libusb : la gestion des événements. Comme décrit dans l'article précédent, la plupart des API des langages système comme le C sont synchrones, et la gestion des événements ne fait pas exception. Elle est généralement implémentée via une boucle infinie qui "interroge" (tente de lire des données ou bloque l'exécution jusqu'à ce que des données soient disponibles) un ensemble de sources d'E/S externes et, lorsqu'au moins l'une d'entre elles répond, transmet cette réponse en tant qu'événement au gestionnaire correspondant. Une fois le gestionnaire terminé, le contrôle revient à la boucle, qui s'interrompt pour une autre interrogation.
Cette approche pose un certain nombre de problèmes sur le Web.
Tout d'abord, WebUSB n'expose pas et ne peut pas exposer les handles bruts des appareils sous-jacents. Il n'est donc pas possible d'interroger directement ces appareils. Deuxièmement, libusb utilise les API eventfd et pipe pour d'autres événements, ainsi que pour gérer les transferts sur les systèmes d'exploitation sans gestionnaires d'appareils bruts. Cependant, eventfd n'est actuellement pas compatible avec Emscripten, et pipe, bien que compatible, n'est actuellement pas conforme à la spécification et ne peut pas attendre les événements.
Enfin, le plus gros problème est que le Web possède sa propre boucle d'événements. Cette boucle d'événements globale est utilisée pour toutes les opérations d'E/S externes (y compris fetch(), les minuteurs ou, dans ce cas, WebUSB). Elle appelle des gestionnaires d'événements ou de Promise chaque fois que les opérations correspondantes se terminent. L'exécution d'une autre boucle d'événements infinie imbriquée empêchera la boucle d'événements du navigateur de progresser. Cela signifie que non seulement l'UI ne répondra plus, mais aussi que le code ne recevra jamais de notifications pour les événements d'E/S qu'il attend. Cela entraîne généralement un blocage, et c'est ce qui s'est produit lorsque j'ai essayé d'utiliser libusb dans une démonstration. La page s'est figée.
Comme pour les autres E/S bloquantes, pour porter ces boucles d'événements sur le Web, les développeurs doivent trouver un moyen de les exécuter sans bloquer le thread principal. Une solution consiste à refactoriser l'application pour gérer les événements d'E/S dans un thread distinct et à renvoyer les résultats au thread principal. L'autre consiste à utiliser Asyncify pour suspendre la boucle et attendre les événements de manière non bloquante.
Je ne voulais pas apporter de modifications importantes à libusb ou gPhoto2, et j'avais déjà utilisé Asyncify pour l'intégration de Promise. C'est donc la voie que j'ai choisie. Pour simuler une variante bloquante de poll(), j'ai utilisé une boucle pour la démonstration de faisabilité initiale, comme indiqué ci-dessous :
#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
Voici ce qu'il fait :
- Appelle
poll()pour vérifier si des événements ont déjà été signalés par le backend. Si c'est le cas, la boucle s'arrête. Sinon, l'implémentation d'Emscripten depoll()renverra immédiatement0. - Il appelle
emscripten_sleep(0). Cette fonction utilise Asyncify etsetTimeout()en arrière-plan. Elle est utilisée ici pour redonner le contrôle à la boucle d'événements principale du navigateur. Cela permet au navigateur de gérer les interactions des utilisateurs et les événements d'E/S, y compris WebUSB. - Vérifiez si le délai avant expiration spécifié est déjà écoulé. Si ce n'est pas le cas, poursuivez la boucle.
Comme l'indique le commentaire, cette approche n'était pas optimale, car elle continuait à enregistrer et à restaurer l'intégralité de la pile d'appel avec Asyncify, même lorsqu'il n'y avait pas encore d'événements USB à gérer (ce qui est le cas la plupart du temps), et parce que setTimeout() lui-même a une durée minimale de 4 ms dans les navigateurs récents. Toutefois, cela a fonctionné suffisamment bien pour produire un flux en direct de 13 à 14 FPS à partir d'un reflex numérique dans la démonstration de faisabilité.
Plus tard, j'ai décidé de l'améliorer en exploitant le système d'événements du navigateur. Il existe plusieurs façons d'améliorer cette implémentation, mais pour l'instant, j'ai choisi d'émettre des événements personnalisés directement sur l'objet global, sans les associer à une structure de données libusb particulière. Pour ce faire, j'ai utilisé le mécanisme d'attente et de notification suivant, basé sur la macro 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);
}
});
La fonction em_libusb_notify() est utilisée chaque fois que libusb tente de signaler un événement, tel que la fin d'un transfert de données :
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
}
La partie em_libusb_wait() est quant à elle utilisée pour "réveiller" Asyncify de son état de veille lorsqu'un événement em-libusb est reçu ou que le délai avant expiration est écoulé :
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;
}
Grâce à une réduction significative des mises en veille et des réveils, ce mécanisme a résolu les problèmes d'efficacité de l'ancienne implémentation basée sur emscripten_sleep() et a augmenté le débit de la démo DSLR de 13-14 FPS à plus de 30 FPS, ce qui est suffisant pour un flux en direct fluide.
Système de compilation et premier test
Une fois le backend terminé, j'ai dû l'ajouter à Makefile.am et configure.ac. Le seul point intéressant ici est la modification des options spécifiques à 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']"
;;
Tout d'abord, les fichiers exécutables sur les plates-formes Unix n'ont généralement pas d'extension de fichier. Cependant, Emscripten produit un résultat différent selon l'extension que vous demandez. J'utilise AC_SUBST(EXEEXT, …) pour remplacer l'extension exécutable par .html afin que tout exécutable d'un package (tests et exemples) devienne un fichier HTML avec l'interface système par défaut d'Emscripten, qui se charge de charger et d'instancier JavaScript et WebAssembly.
Deuxièmement, comme j'utilise Embind et Asyncify, je dois activer ces fonctionnalités (--bind -s ASYNCIFY) et autoriser la croissance dynamique de la mémoire (-s ALLOW_MEMORY_GROWTH) via les paramètres de l'éditeur de liens. Malheureusement, une bibliothèque ne peut pas signaler ces options à l'éditeur de liens. Par conséquent, chaque application qui utilise ce port libusb devra également ajouter les mêmes options d'éditeur de liens à sa configuration de compilation.
Enfin, comme mentionné précédemment, WebUSB nécessite que l'énumération des appareils soit effectuée via un geste de l'utilisateur. Les exemples et les tests libusb supposent qu'ils peuvent énumérer les appareils au démarrage et échouent avec une erreur sans modification. À la place, j'ai dû désactiver l'exécution automatique (-s INVOKE_RUN=0) et exposer la méthode manuelle callMain() (-s EXPORTED_RUNTIME_METHODS=...).
Une fois toutes ces étapes terminées, j'ai pu diffuser les fichiers générés avec un serveur Web statique, initialiser WebUSB et exécuter manuellement ces exécutables HTML à l'aide des outils de développement.
![Capture d'écran montrant une fenêtre Chrome avec les outils de développement ouverts sur une page "testlibusb" diffusée localement. La console DevTools évalue "navigator.usb.requestDevice({ filters: [] })", ce qui a déclenché une demande d'autorisation. Elle demande actuellement à l'utilisateur de choisir un appareil USB à partager avec la page. ILCE-6600 (un appareil photo Sony) est actuellement sélectionné.](https://web.developers.google.cn/static/articles/porting-libusb-to-webusb/image/screenshot-showing-chrom-b19a9e4e1b916.png?hl=fr)
![Capture d'écran de l'étape suivante, avec les outils de développement toujours ouverts. Une fois l'appareil sélectionné, la console a évalué une nouvelle expression "Module.callMain(['-v'])", qui a exécuté l'application "testlibusb" en mode détaillé. Le résultat affiche diverses informations détaillées sur la caméra USB précédemment connectée : fabricant Sony, produit ILCE-6600, numéro de série, configuration, etc.](https://web.developers.google.cn/static/articles/porting-libusb-to-webusb/image/screenshot-the-next-step-bd4e4c7f85be7.png?hl=fr)
Cela n'a l'air de rien, mais lorsque vous portez des bibliothèques sur une nouvelle plate-forme, atteindre l'étape où elle produit un résultat valide pour la première fois est assez excitant !
Utiliser le port
Comme indiqué ci-dessus, le port dépend de quelques fonctionnalités Emscripten qui doivent actuellement être activées lors de l'étape d'association de l'application. Si vous souhaitez utiliser ce port libusb dans votre propre application, voici ce que vous devez faire :
- Téléchargez la dernière version de libusb sous forme d'archive dans votre build ou ajoutez-la en tant que sous-module Git dans votre projet.
- Exécutez
autoreconf -fivdans le dossierlibusb. - Exécutez
emconfigure ./configure –host=wasm32 –prefix=/some/installation/pathpour initialiser le projet pour la compilation croisée et définir le chemin d'accès où vous souhaitez placer les artefacts compilés. - Exécutez
emmake make install. - Faites pointer votre application ou votre bibliothèque de niveau supérieur pour rechercher libusb sous le chemin d'accès choisi précédemment.
- Ajoutez les options suivantes aux arguments de lien de votre application :
--bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.
La bibliothèque présente actuellement quelques limites :
- L'annulation du transfert n'est pas possible. Il s'agit d'une limitation de WebUSB, qui découle elle-même de l'absence d'annulation de transfert multiplate-forme dans libusb.
- Pas de compatibilité avec le transfert isochrone. Il ne devrait pas être difficile de l'ajouter en suivant l'implémentation des modes de transfert existants comme exemples, mais il s'agit également d'un mode quelque peu rare et je n'avais aucun appareil pour le tester. Je l'ai donc laissé comme non compatible pour le moment. Si vous possédez de tels appareils et que vous souhaitez contribuer à la bibliothèque, les demandes d'extraction sont les bienvenues.
- Les limites multiplates-formes mentionnées précédemment. Ces limites sont imposées par les systèmes d'exploitation. Nous ne pouvons donc pas faire grand-chose, si ce n'est demander aux utilisateurs de remplacer le pilote ou les autorisations. Toutefois, si vous portez des appareils HID ou série, vous pouvez suivre l'exemple libusb et porter une autre bibliothèque vers une autre API Fugu. Par exemple, vous pouvez porter une bibliothèque C hidapi vers WebHID et contourner complètement les problèmes liés à l'accès USB de bas niveau.
Conclusion
Dans cet article, j'ai montré comment, à l'aide des API Emscripten, Asyncify et Fugu, même les bibliothèques de bas niveau comme libusb peuvent être portées sur le Web grâce à quelques astuces d'intégration.
Le portage de ces bibliothèques de bas niveau essentielles et largement utilisées est particulièrement gratifiant, car il permet également d'intégrer des bibliothèques de niveau supérieur, voire des applications entières, sur le Web. Cela permet d'ouvrir des expériences qui étaient auparavant limitées aux utilisateurs d'une ou deux plates-formes, à tous les types d'appareils et de systèmes d'exploitation, et de les rendre accessibles d'un simple clic sur un lien.
Dans le prochain article, je vous expliquerai comment créer la démo Web gPhoto2, qui récupère non seulement les informations sur l'appareil, mais utilise également de manière intensive la fonctionnalité de transfert de libusb. En attendant, j'espère que l'exemple libusb vous a inspiré et que vous allez essayer la démo, jouer avec la bibliothèque elle-même, ou peut-être même porter une autre bibliothèque largement utilisée vers l'une des API Fugu.