Portage d'applications USB sur le Web Partie 1: libusb

Découvrez comment transférer sur le Web le code qui interagit avec des appareils externes grâce aux API WebAssembly et Fugu.

Dans un article précédent, j'ai expliqué comment transférer sur le Web des applications utilisant les API du système de fichiers avec l'API File System Access, WebAssembly et Asyncify. Je souhaite à présent poursuivre sur le même sujet : intégrer les API Fugu avec WebAssembly et porter les applications sur le Web sans perdre des fonctionnalités importantes.

Je vais vous montrer comment rendre les applications qui communiquent avec des appareils USB sur le Web en transférant libusb, une bibliothèque USB populaire écrite en C, vers WebAssembly (via Emscripten), Asyncify et WebUSB.

Commençons par une démonstration

Lors du transfert d'une bibliothèque, il est essentiel de choisir la version de démonstration appropriée, qui mette en valeur les fonctionnalités de la bibliothèque transférée, ce qui vous permet de la tester de différentes manières tout en étant visuellement convaincante.

J'ai choisi une télécommande pour reflex numérique. C'est notamment le cas du projet Open Source gPhoto2 qui a fait l'objet d'une rétro-ingénierie et rendre possible la compatibilité avec une grande variété d'appareils photo numériques. Il prend en charge plusieurs protocoles, mais celui qui m'intéressait le plus était le support USB, qu'il fonctionne 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 vous expliquer comment j'ai porté libusb lui-même et quelles astuces pourraient être nécessaires pour transférer d'autres bibliothèques populaires vers les API Fugu. Dans le deuxième message, je détaillerai le portage et l'intégration de gPhoto2.

À la fin, j'ai obtenu une application Web opérationnelle qui permet de prévisualiser le flux en direct d'un reflex numérique et de contrôler ses paramètres via USB. N'hésitez pas à consulter la démonstration en direct ou la démo préenregistrée avant de lire les détails techniques:

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph> La démo exécutée sur un ordinateur portable connecté à une caméra Sony.

Remarque sur les particularités propres à l'appareil photo

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, le problème n'est pas dû aux performances de WebAssembly ou WebUSB, mais de la façon dont gPhoto2 interagit avec l'appareil photo choisi pour la démonstration.

Sony a6600 n'expose pas d'API pour définir directement des valeurs telles que la sensibilité ISO, l'ouverture ou la vitesse d'obturation. À la place, elle fournit uniquement des commandes permettant d'augmenter ou de diminuer ces valeurs en fonction du nombre de pas spécifié. Pour compliquer encore les choses, elle ne renvoie pas non plus de liste des valeurs réellement acceptées : la liste renvoyée semble être codée en dur pour 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:

  1. Faites un ou plusieurs pas dans la direction de la valeur choisie.
  2. Attendez que la caméra mette à jour les paramètres.
  3. Relisez la valeur réelle de l'appareil photo.
  4. Vérifiez que la dernière étape n'a pas dépassé la valeur souhaitée et qu'elle n'a pas été placée à la fin ou au début de la liste.
  5. Recommencez.

Cela peut prendre un certain temps, mais si la valeur est réellement acceptée par la caméra, elle s'en chargera et, dans le cas contraire, elle s'arrêtera à la valeur acceptée la plus proche.

D'autres caméras présenteront 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 d'effectuer des tests automatisés ou manuels de tous les modèles d'appareil photo existants. Par conséquent, les rapports détaillés et les demandes d'informations sont toujours les bienvenus (mais assurez-vous de reproduire les problèmes avec le client gPhoto2 officiel).

Remarques importantes concernant la compatibilité multiplate-forme

Malheureusement, sous Windows, toutes les applications « connues » appareils, 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 version de démonstration sous Windows, vous devez utiliser un outil tel que Zadig pour remplacer le pilote du DSLR connecté par WinUSB ou libusb. Cette approche fonctionne bien pour moi comme pour 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 via WebUSB, bien que cela dépende de votre distribution.

Sous macOS et Android, la démonstration devrait fonctionner immédiatement. Si vous testez cette fonctionnalité sur un téléphone Android, assurez-vous de passer en mode Paysage, car je n'ai pas fait beaucoup d'efforts pour le rendre réactif (les conseillers clientèle sont les bienvenus !):

<ph type="x-smartling-placeholder">
</ph> Téléphone Android connecté à un appareil photo Canon via un câble USB-C. <ph type="x-smartling-placeholder">
</ph> La même version de démonstration exécutée sur un téléphone Android. Photo de Surma.

Pour un guide plus détaillé sur l'utilisation multiplate-forme de WebUSB, consultez la section Considérations propres aux plates-formes de la section "Créer un périphérique pour WebUSB".

Ajouter un nouveau backend à libusb

Passons maintenant aux détails techniques. Bien qu'il soit possible de fournir une API shim semblable à libusb (ce qui a été fait par d'autres auparavant) et d'y associer d'autres applications, cette approche est sujette aux erreurs et complique toute extension ou maintenance supplémentaire. Je voulais faire les choses correctement, d'une manière qui puisse être potentiellement contribuée en amont et fusionnée dans libusb à l'avenir.

Heureusement, le fichier README libusb indique:

"libusb est extrait en interne de manière à pouvoir, espérons-le, être transféré vers 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 communs, 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 parcourant les propriétés, nous constatons que le struct 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 le stockage de données privées au niveau de l'appareil, du contexte et du transfert.

Les champs de données privées sont utiles au moins pour stocker les identifiants du système d'exploitation pour toutes ces choses, car sans les identifiants, nous ne savons pas à quel élément une opération donnée s'applique. Dans l'implémentation Web, les OS handle sont les objets JavaScript WebUSB sous-jacents. Le moyen naturel de les représenter et de les stocker dans Emscripten consiste à utiliser la classe emscripten::val, qui est fournie dans 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 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 de périphérique

libusb fournit des pointeurs prêts à l'emploi vers la zone allouée pour les 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 un moyen de gérer les API WebUSB asynchrones où libusb attend des opérations synchrones. Pour cela, je peux 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 dispose actuellement d'aucun moyen de gérer les exceptions JavaScript ou les refus Promise du côté C++. Vous pouvez contourner ce problème en interceptant un refus côté JavaScript et en convertissant le résultat en un objet { error, value } qui peut maintenant être analysé en toute sécurité du côté C++. Pour ce faire, j'ai utilisé une combinaison de la macro EM_JS et des 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.

Exemple :valUSBDevicelibusb_device_handleopen()

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 la 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'existe aucun moyen d'énumérer tous les périphériques USB connectés sur le Web pour des raisons de sécurité. Au lieu de cela, le flux est divisé en deux parties. Tout d'abord, l'application Web demande des appareils avec des 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 répertorie 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 demande 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, une promesse refusée est toujours renvoyée. Il arrive souvent que les applications libusb souhaitent répertorier les appareils connectés au démarrage de l'application. L'utilisation de requestDevice() n'était donc pas possible.

À la place, 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 backend utilise val et promise_result de la même manière que ci-dessus. Il existe quelques astuces plus 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. N'hésitez pas à consulter le code et les commentaires sur GitHub si cela vous intéresse.

Transfert de boucles d'événements sur le Web

Un autre élément du port libusb dont je veux parler est la gestion des événements. Comme décrit dans l'article précédent, la plupart des API dans des langages système tels que 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" (essaie de lire des données ou bloque l'exécution jusqu'à ce que certaines données soient disponibles) à partir d'un ensemble de sources d'E/S externes et, lorsqu'au moins l'une de ces sources répond, la transmet en tant qu'événement au gestionnaire correspondant. Une fois que le gestionnaire a terminé, la commande revient dans la boucle et s'interrompt pour un autre sondage.

Cette approche sur le Web pose un certain nombre de problèmes.

Tout d'abord, WebUSB n'expose pas et ne peut pas exposer les poignées brutes des appareils sous-jacents. Il n'est donc pas possible de les interroger directement. Ensuite, 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 handle d'appareil bruts. Cependant, eventfd n'est actuellement pas compatible avec Emscripten et pipe, bien qu'il soit compatible, n'est actuellement pas conforme aux spécifications 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), et appelle les gestionnaires d'événements ou Promise à la fin des opérations correspondantes. L'exécution d'une autre boucle d'événements infinie et imbriquée empêchera la boucle d'événements du navigateur. Cela signifie que non seulement l'interface utilisateur ne répond plus, mais aussi que le code ne recevra jamais de notifications pour les mêmes événements d'E/S qu'il attend. Cela entraîne généralement un interblocage, et c'est également ce qui s'est produit lorsque j'ai essayé d'utiliser libusb dans une démo. La page s'est figée.

Comme pour d'autres E/S bloquantes, les développeurs doivent trouver un moyen d'exécuter ces boucles sans bloquer le thread principal pour transférer ces boucles d'événements sur le Web. L'une des méthodes consiste à refactoriser l'application pour gérer les événements d'E/S dans un thread distinct et transmettre les résultats au thread principal. La seconde consiste à utiliser Asyncify pour mettre en pause la boucle et attendre les événements de manière non bloquante.

Je ne voulais pas apporter de modifications importantes à libusb ni à gPhoto2, et j'ai déjà utilisé Asyncify pour l'intégration de Promise. C'est donc la méthode que j'ai choisie. Pour simuler une variante bloquante de poll(), j'ai utilisé une boucle comme indiqué ci-dessous pour la démonstration de faisabilité initiale:

#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

De quoi s'agit-il ?

  1. Appel de poll() pour vérifier si des événements ont déjà été signalés par le backend S'il y en a, la boucle s'arrête. Sinon, l'implémentation d'Emscripten de poll() renvoie immédiatement le résultat 0.
  2. Il appelle emscripten_sleep(0). Cette fonction utilise Asyncify et setTimeout() en arrière-plan. Elle permet ici de redonner le contrôle à la boucle d'événements du navigateur principal. Cela permet au navigateur de gérer les interactions utilisateur et les événements d'E/S, y compris WebUSB.
  3. Vérifiez si le délai avant expiration spécifié a déjà expiré. Si ce n'est pas le cas, continuez la boucle.

Comme indiqué dans le commentaire, cette approche n'était pas optimale, car elle continuait à enregistrer la totalité 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 la plupart du temps), et parce que setTimeout() lui-même a une durée minimale de 4 ms dans les navigateurs modernes. Pour la démonstration de faisabilité, elle a toutefois été suffisamment efficace pour produire une diffusion en direct à 13-14 FPS sur un reflex numérique.

Par la suite, j'ai décidé de l'améliorer en tirant parti du 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 de 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, comme la fin du 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
}

Pendant ce temps, la partie em_libusb_wait() sert à "réveiller". de la mise en veille Asyncify à la réception d'un événement em-libusb ou à l'expiration du délai d'inactivité:

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

En raison d'une réduction significative du nombre de veilles et de wakeups, ce mécanisme a résolu les problèmes d'efficacité de l'implémentation précédente basée sur emscripten_sleep() et augmenté le débit de démonstration du reflex numérique de 13 à 14 FPS à plus de 30 FPS, ce qui est suffisant pour un flux en direct fluide.

Créer le système et le premier test

Une fois le backend terminé, j'ai dû l'ajouter à Makefile.am et configure.ac. Le seul élément intéressant ici est la modification des indicateurs 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 exécutables sur les plateformes Unix n'ont normalement pas d'extensions de fichier. Emscripten, cependant, produit un résultat différent selon l'extension demandée. J'utilise AC_SUBST(EXEEXT, …) pour remplacer l'extension exécutable par .html afin que tous les exécutables d'un package (tests et exemples) deviennent un code HTML avec le shell par défaut d'Emscripten qui se charge du chargement et de l'instanciation de 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 Linker. Malheureusement, il n'existe aucun moyen pour une bibliothèque de signaler ces indicateurs à l'éditeur de liens. Par conséquent, chaque application qui utilise ce port libusb devra également ajouter les mêmes indicateurs d'éditeur de liens dans sa configuration de compilation.

Enfin, comme indiqué précédemment, WebUSB exige que l'énumération de l'appareil soit effectuée par un geste de l'utilisateur. Les exemples et les tests libusb supposent qu'ils peuvent énumérer les appareils au démarrage et qu'ils échouent avec une erreur sans aucune 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 tout cela fait, 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&#39;écran montrant une fenêtre Chrome avec les outils de développement ouverts sur une page &quot;testlibusb&quot; diffusée localement. La console DevTools évalue &quot;navigator.usb.requestDevice({ filters: [] })&quot;, ce qui a déclenché une invite d&#39;autorisation qui demande actuellement à l&#39;utilisateur de choisir un périphérique USB à partager avec la page. ILCE-6600 (appareil photo Sony) est actuellement sélectionné.

Capture d&#39;écran de l&#39;étape suivante, avec les outils de développement toujours ouverts Une fois l&#39;appareil sélectionné, la console a évalué une nouvelle expression &quot;Module.callMain([&#39;-v&#39;])&quot; qui a exécuté l&#39;application &quot;testlibusb&quot; en mode détaillé. La sortie 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.

Cela ne ressemble pas à grand-chose, mais lorsque vous transférez des bibliothèques vers une nouvelle plate-forme, arriver à l'étape où elle génère une sortie 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'association de l'application. Si vous souhaitez utiliser ce port libusb dans votre propre application, procédez comme suit:

  1. 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.
  2. Exécutez autoreconf -fiv dans le dossier libusb.
  3. Exécutez emconfigure ./configure –host=wasm32 –prefix=/some/installation/path pour initialiser le projet en vue d'une compilation croisée et pour définir le chemin d'accès où vous souhaitez placer les artefacts compilés.
  4. Exécutez emmake make install.
  5. Pointez votre application ou votre bibliothèque de niveau supérieur pour rechercher le libusb sous le chemin choisi précédemment.
  6. Ajoutez les options suivantes aux arguments "link" de votre application: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

La bibliothèque présente actuellement quelques limites:

  • Il n'est pas possible d'annuler un transfert. Il s'agit d'une limitation de WebUSB qui, à son tour, est due à l'absence d'annulation de transfert entre plates-formes dans libusb lui-même.
  • Non compatible avec les transferts isochrones. Il ne devrait pas être difficile de l'ajouter en suivant l'implémentation de modes de transfert existants comme exemples, mais c'est aussi un mode assez rare et je n'avais aucun appareil sur lequel le tester. Pour l'instant, je l'ai laissé comme non compatible. Si vous possédez de tels appareils et que vous souhaitez contribuer à la bibliothèque, les relations publiques sont les bienvenues !
  • Nous avons mentionné précédemment les limites liées au multiplate-forme. Ces limites étant imposées par les systèmes d'exploitation, nous ne pouvons pas faire grand-chose ici, sauf demander aux utilisateurs de remplacer le pilote ou les autorisations. Toutefois, si vous transférez des appareils HID ou série, vous pouvez suivre l'exemple libusb et transférer une autre bibliothèque vers une autre API Fugu. Par exemple, vous pouvez porter une bibliothèque C hidapi sur WebHID et esquiver ces problèmes, associés à l'accès USB de bas niveau, complètement.

Conclusion

Dans ce post, j'ai montré comment, avec l'aide des API Emscripten, Asyncify et Fugu, même des bibliothèques de bas niveau comme libusb peuvent être transposées sur le Web avec quelques astuces d'intégration.

Le portage de bibliothèques de bas niveau essentielles et largement utilisées est particulièrement gratifiant, car cela permet également de mettre des bibliothèques de niveau supérieur, voire des applications entières, sur le Web. Les expériences qui étaient auparavant limitées aux utilisateurs d'une ou deux plates-formes s'ouvrent ainsi sur tous types d'appareils et de systèmes d'exploitation, d'un simple clic.

Dans le post suivant, je vous présenterai les étapes à suivre pour créer la version de démonstration Web de gPhoto2, qui permet non seulement de récupérer les informations de l'appareil, mais qui utilise également la fonctionnalité de transfert de libusb. En attendant, j'espère que vous avez trouvé l'exemple de libusb inspirant et que vous testerez la démo, jouerez avec la bibliothèque elle-même ou pourrez même transférer une autre bibliothèque largement utilisée dans l'une des API Fugu.