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

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

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

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

Commençons par le commencement: une démonstration

La chose la plus importante à effectuer lors du portage d'une bibliothèque est de choisir la bonne démo, c'est-à-dire une démo qui mettrait en valeur les fonctionnalités de la bibliothèque transférée. Vous pourrez ainsi la tester de différentes manières, tout en étant visuellement attrayante.

J'ai choisi d'utiliser un reflex numérique. Par exemple, le projet Open Source gPhoto2 est présent dans ce domaine depuis suffisamment longtemps pour procéder à la rétro-ingénierie et à la compatibilité avec une grande variété d'appareils photo numériques. Il prend en charge plusieurs protocoles, mais celui qui m'a le plus intéressé est la prise en charge USB, qui fonctionne grâce à libusb.

Je décrirai en deux étapes les étapes de création de cette démonstration. Dans cet article de blog, je décrirai comment j'ai fait le portage de libusb et quelles astuces pourraient être nécessaires pour transférer d'autres bibliothèques populaires vers les API Fugu. Dans le deuxième message, j'expliquerai en détail le portage et l'intégration de gPhoto2.

Au final, j'ai eu une application Web fonctionnelle qui donne un aperçu du flux en direct d'un reflex numérique et peut contrôler ses paramètres via USB. N'hésitez pas à visionner la démonstration en direct ou préenregistrée avant de vous plonger dans les détails techniques:

Démonstration exécutée sur un ordinateur portable connecté à un appareil photo 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, 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.

Sony a6600 ne propose pas d'API permettant de définir directement des valeurs telles que l'ISO, l'ouverture ou la vitesse d'obturation, mais uniquement des commandes permettant d'augmenter ou de diminuer ces valeurs en fonction du nombre de pas spécifié. Pour compliquer les choses, elle ne renvoie pas non plus de liste des valeurs prises en charge : la liste renvoyée semble 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:

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

Cela peut prendre un certain temps, mais si la valeur est effectivement prise en charge par l'appareil photo, elle sera atteinte et, dans le cas contraire, elle s'arrêtera à la valeur prise en charge la plus proche.

D'autres caméras auront probablement des ensembles de paramètres, des API sous-jacentes et des particularités différentes. N'oubliez pas que gPhoto2 est un projet Open Source et que les tests automatisés ou manuels de tous les modèles d'appareil photo existants ne sont tout simplement pas réalisables. Les rapports d'incidents et les demandes d'extraction détaillés sont donc toujours les bienvenus (mais assurez-vous d'abord de reproduire les problèmes avec le client officiel de gPhoto2).

Remarques importantes concernant 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 voulez essayer la démo sous Windows, vous devez utiliser un outil comme Zadig afin de remplacer le pilote du reflex connecté par WinUSB ou libusb. Cette approche fonctionne bien pour moi et 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, mais cela dépend de votre distribution.

Sous macOS et Android, la version de démonstration devrait fonctionner immédiatement. Si vous essayez la fonctionnalité sur un téléphone Android, veillez à passer en mode Paysage, car je n'ai pas fait beaucoup d'efforts pour la rendre réactive (les PR sont les bienvenues !):

Téléphone Android connecté à un appareil photo Canon via un câble USB-C.
La même démonstration 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 "Remarques propres à la plate-forme" de la page "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 (cela a déjà été fait par d'autres utilisateurs) et d'associer d'autres applications à celle-ci, cette approche est sujette aux erreurs et complique toute extension ou maintenance supplémentaire. Je voulais faire les choses correctement, de manière à pouvoir potentiellement contribuer en amont et être fusionnées avec libusb à l'avenir.

Heureusement, le fichier libusb README indique:

"libusb est extrait en interne de manière à pouvoir être transféré vers d'autres systèmes d'exploitation. Pour en savoir plus, consultez le fichier PORTING."

Dans la structure de libusb, l'API publique est distincte des "backends". Ces backends sont chargés de répertorier, d'ouvrir et de fermer les appareils, et de communiquer avec eux via les API de bas niveau du système d'exploitation. libusb élimine déjà les différences entre Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku et Solaris et fonctionne sur toutes ces plateformes.

J'ai dû ajouter un autre backend pour le "système d'exploitation" Emscripten+WebUSB. Les implémentations pour 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, le backend Windows se présente comme suit:

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 capacités, des gestionnaires de diverses opérations USB de bas niveau sous la forme de pointeurs de fonction, et, enfin, les tailles à allouer pour le stockage des données privées au niveau de l'appareil, du contexte ou du transfert.

Les champs de données privées sont utiles au moins pour stocker les identifiants du système d'exploitation pour tous ces éléments, car sans les identifiants, nous ne savons pas à quel élément une opération donnée s'applique. Dans l'implémentation Web, le système d'exploitation gère les objets JavaScript WebUSB sous-jacents. La méthode la plus naturelle pour les représenter et 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 le sont en C++. Embind ne fonctionne qu'avec C++, donc le choix a é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 descripteurs d'appareil

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 vers l'extérieur:

// 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 désormais 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 n'a actuellement aucun moyen de gérer les exceptions JavaScript ni les refus Promise côté C++. Pour contourner ce problème, il suffit d'intercepter un refus côté JavaScript et de convertir 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'utilise 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.

Par exemple, la récupération d'un val représentant un USBDevice à partir de libusb_device_handle, l'appel de sa méthode open(), l'attente de son résultat et le renvoi d'un code d'erreur sous forme de code d'état libusb se présentent 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

Avant de pouvoir ouvrir un appareil, libusb doit bien sûr récupérer la liste des appareils disponibles. Le backend doit implémenter cette opération via un gestionnaire get_device_list.

La difficulté réside dans le fait 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 ayant des propriétés spécifiques via navigator.usb.requestDevice(). L'utilisateur choisit manuellement l'appareil qu'il souhaite exposer ou refuse l'invite d'autorisation. L'application liste ensuite 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. Elle doit être déclenchée par une interaction de l'utilisateur (comme un clic sur un bouton sur une page), sinon elle renvoie toujours une promesse refusée. Les applications libusb voulaient souvent lister les appareils connectés au démarrage de l'application. Il n'était donc pas possible d'utiliser requestDevice().

Au lieu de cela, j'ai dû laisser l'appel de navigator.usb.requestDevice() au développeur final et exposer uniquement les appareils déjà approuvés à partir 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 précédemment. Il existe quelques astuces plus intéressantes dans le code de gestion des transferts de données, mais ces détails d'implémentation sont moins importants pour les besoins de cet article. Si cela vous intéresse, vérifiez le code et les commentaires sur GitHub.

Portage des boucles d'événements vers le Web

Une autre partie 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 les langages système tels que C sont synchrones. La gestion des événements ne fait pas exception. Il est généralement implémenté via une boucle infinie qui "interroge" (tenter de lire les 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 retourne dans la boucle et s'interrompt pour une autre interrogation.

Cette approche présente quelques problèmes sur le Web.

Tout d'abord, WebUSB n'expose pas et ne peut pas exposer les identifiants bruts 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 identifiant d'appareil brut. Toutefois, eventfd n'est actuellement pas compatible avec Emscripten, et pipe, bien qu'il soit pris en charge, ne respecte actuellement pas les spécifications et ne peut pas attendre les événements.

Enfin, le plus gros problème est que le Web a 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 le cas présent, WebUSB), et appelle des gestionnaires d'événements ou Promise chaque fois que les opérations correspondantes sont terminées. L'exécution d'une autre boucle d'événements infinie imbriquée empêche la boucle d'événements du navigateur de progresser. Cela signifie que non seulement l'interface utilisateur ne répond plus, mais également que le code ne recevra jamais de notifications pour les événements d'E/S qu'il attend. Cela provoque généralement un interblocage, et c'est ce qui s'est produit lorsque j'ai essayé d'utiliser libusb dans une démo. La page s'est figée.

Comme avec d'autres E/S bloquantes, pour porter ces boucles d'événements sur le Web, les développeurs doivent trouver un moyen d'exécuter ces boucles sans bloquer le thread principal. 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. L'autre méthode consiste à utiliser Asyncify pour suspendre la boucle et attendre des é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 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

Description:

  1. Appelle 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 de poll() par Emscripten sera immédiatement renvoyée avec 0.
  2. Il appelle emscripten_sleep(0). Cette fonction utilise Asyncify et setTimeout() en arrière-plan. Elle permet de restituer le contrôle à la boucle d'événements du navigateur principal. Le navigateur peut ainsi gérer les interactions utilisateur et les événements d'E/S, y compris WebUSB.
  3. Vérifiez si le délai d'expiration spécifié a expiré et, si ce n'est pas le cas, poursuivez la boucle.

Comme indiqué dans 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 la plupart du temps), et parce que setTimeout() lui-même a une durée minimale de 4 ms dans les navigateurs modernes. Malgré tout, cela a suffisamment fonctionné pour produire une diffusion en direct de 13 à 14 FPS à partir d'un reflex numérique dans la démonstration de faisabilité.

Par la suite, 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'utilise le mécanisme d'attente et de notification suivant, qui s'appuie 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 l'achèvement 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() est utilisée pour "rallumer" le mode veille asynchrone lorsqu'un événement em-libusb est reçu ou que le délai a expiré:

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 mises en veille et de réveils, ce mécanisme a résolu les problèmes d'efficacité de l'implémentation antérieure basée sur emscripten_sleep() et permis de faire passer 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.

Système de compilation et premier test

Une fois le backend terminé, j'ai dû l'ajouter à Makefile.am et configure.ac. Le seul aspect intéressant 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’extension de fichier. Emscripten, en revanche, produit des résultats différents en fonction de l'extension demandée. 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 code HTML avec le shell par défaut d'Emmscripten, qui se charge de charger et d'instancier JavaScript et WebAssembly.

Ensuite, comme j'utilise Embind et Asyncify, je dois activer ces fonctionnalités (--bind -s ASYNCIFY) et autoriser l'augmentation dynamique de la mémoire (-s ALLOW_MEMORY_GROWTH) via les paramètres de l'éditeur de liens. 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 à sa configuration de compilation.

Enfin, comme indiqué précédemment, WebUSB exige que l'énumération des appareils 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 é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 callMain() manuelle (-s EXPORTED_RUNTIME_METHODS=...).

Une fois l'opération terminée, 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 dans laquelle les outils de développement sont ouverts sur une page &quot;testlibusb&quot; diffusée en local. La console des outils de développement évalue &quot;navigator.usb.requestDevice({filters: [] })&quot;, ce qui a déclenché une invite d&#39;autorisation et demande actuellement à l&#39;utilisateur de choisir un appareil USB à partager avec la page. L&#39;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 exécute l&#39;application testlibusb en mode détaillé. La sortie affiche diverses informations détaillées sur la caméra USB connectée précédemment: fabricant Sony, produit ILCE-6600, numéro de série, configuration, etc.

Cela ne ressemble pas à grand-chose, mais, lors du portage de bibliothèques vers une nouvelle plate-forme, arriver à l'étape où elle produit 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 en tant qu'archive dans le cadre de votre compilation 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 définir un 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 la libusb sous le chemin d'accès choisi précédemment.
  6. Ajoutez les indicateurs suivants aux arguments de lien de votre application: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

La bibliothèque présente actuellement quelques limites:

  • Impossible d'annuler un transfert. Il s'agit d'une limitation de WebUSB qui, à son tour, est due à l'absence d'annulation de transfert entre les plates-formes dans libusb.
  • Pas de prise en charge des transferts isochrones. Il ne devrait pas être difficile de l'ajouter en suivant l'implémentation des modes de transfert existants comme exemples, mais c'est aussi un mode assez rare et je n'avais aucun appareil sur lequel le tester. Je l'ai donc laissé comme non compatible pour l'instant. Si vous possédez ce type d'appareils et que vous souhaitez apporter votre contribution à la bibliothèque, les RP sont les bienvenues !
  • Les limites d'utilisation multi-plateformes mentionnées précédemment. Ces limites étant imposées par les systèmes d'exploitation, nous ne pouvons donc pas faire grand-chose ici, si ce n'est 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 transférer un hidapi de la bibliothèque C vers WebHID et contourner les problèmes liés à l'accès USB de bas niveau.

Conclusion

Dans cet article, j'ai vu comment, avec l'aide des API Emscripten, Asyncify et Fugu, même des bibliothèques de bas niveau telles que libusb peuvent être transférées sur le Web en quelques astuces d'intégration.

Le portage de bibliothèques de bas niveau essentielles et couramment utilisées est particulièrement gratifiant, car cela permet de transférer des bibliothèques de niveau supérieur ou même des applications entières sur le Web. Cela permet d'offrir des expériences qui étaient auparavant limitées aux utilisateurs d'une ou deux plates-formes, sur tous les types d'appareils et de systèmes d'exploitation, et d'y accéder d'un simple clic.

Dans le prochain post, je détaillerai les étapes de création de la démo Web gPhoto2, qui permet non seulement de récupérer des informations sur un appareil, mais aussi d'utiliser largement la fonctionnalité de transfert de libusb. En attendant, j'espère que vous avez trouvé l'exemple libusb inspirant. Vous allez essayer la démo, jouer avec la bibliothèque elle-même ou peut-être même transférer une autre bibliothèque couramment utilisée vers l'une des API Fugu.