Portare le applicazioni USB sul Web. Parte 1: libusb

Scopri come il codice che interagisce con i dispositivi esterni può essere trasferito sul web con le API WebAssembly e Fugu.

Ingvar Stepanyan
Ingvar Stepanyan

In un post precedente, ho mostrato come trasferire sul web le app che utilizzano le API del file system con l'API File System Access, WebAssembly e Asyncify. Ora voglio continuare con lo stesso argomento dell'integrazione delle API Fugu con WebAssembly e del porting delle app sul web senza perdere funzionalità importanti.

Mostrerò come le app che comunicano con i dispositivi USB possono essere trasferite sul web eseguendo il porting di libusb, una libreria USB molto utilizzata scritta in C, su WebAssembly (tramite Emscripten), Asyncify e WebUSB.

Per prima cosa: una demo

La cosa più importante da fare quando si esegue il porting di una libreria è scegliere la demo giusta, ovvero qualcosa che mostri le funzionalità della libreria di cui è stato eseguito il porting, che consenta di testarla in vari modi e che sia visivamente accattivante allo stesso tempo.

L'idea che ho scelto è stata il telecomando per fotocamere reflex digitali. In particolare, un progetto open source, gPhoto2, è attivo in questo ambito da abbastanza tempo da aver eseguito il reverse engineering e implementato il supporto per un'ampia gamma di fotocamere digitali. Supporta diversi protocolli, ma quello che mi interessava di più era il supporto USB, che esegue tramite libusb.

Descriverò i passaggi per creare questa demo in due parti. In questo blog post, descriverò come ho eseguito il porting di libusb e quali trucchi potrebbero essere necessari per eseguire il porting di altre librerie popolari alle API Fugu. Nel secondo post, approfondirò i dettagli sul porting e sull'integrazione di gPhoto2.

Alla fine, ho ottenuto un'applicazione web funzionante che mostra l'anteprima del feed in diretta di una reflex digitale e può controllarne le impostazioni tramite USB. Dai un'occhiata alla demo live o preregistrata prima di leggere i dettagli tecnici:

La demo in esecuzione su un laptop collegato a una fotocamera Sony.

Nota sulle peculiarità specifiche della fotocamera

Potresti aver notato che la modifica delle impostazioni richiede un po' di tempo nel video. Come per la maggior parte degli altri problemi che potresti riscontrare, questo non è causato dalle prestazioni di WebAssembly o WebUSB, ma dal modo in cui gPhoto2 interagisce con la fotocamera specifica scelta per la demo.

Sony a6600 non espone un'API per impostare direttamente valori come ISO, apertura o tempo di esposizione, ma fornisce solo comandi per aumentarli o diminuirli del numero di passaggi specificato. A complicare ulteriormente le cose, non restituisce nemmeno un elenco dei valori effettivamente supportati: l'elenco restituito sembra impostato come hardcoded su molti modelli di fotocamere Sony.

Quando imposti uno di questi valori, gPhoto2 non ha altra scelta se non:

  1. Fai un passo (o alcuni) nella direzione del valore scelto.
  2. Attendi un po' che la videocamera aggiorni le impostazioni.
  3. Leggi il valore effettivamente impostato dalla videocamera.
  4. Verifica che l'ultimo passaggio non abbia saltato il valore desiderato o non abbia raggiunto la fine o l'inizio dell'elenco.
  5. Ripeti.

Potrebbe essere necessario un po' di tempo, ma se il valore è effettivamente supportato dalla videocamera, verrà raggiunto, altrimenti si fermerà sul valore supportato più vicino.

Altre fotocamere avranno probabilmente impostazioni, API di base e caratteristiche diverse. Tieni presente che gPhoto2 è un progetto open source e che i test automatici o manuali di tutti i modelli di fotocamere esistenti non sono semplicemente fattibili, pertanto i report dettagliati sui problemi e le PR sono sempre ben accetti (ma assicurati di riprodurre prima i problemi con il client gPhoto2 ufficiale).

Note importanti sulla compatibilità multipiattaforma

Purtroppo, su Windows a tutti i dispositivi "noti", incluse le fotocamere reflex digitali, viene assegnato un driver di sistema, che non è compatibile con WebUSB. Se vuoi provare la demo su Windows, devi utilizzare uno strumento come Zadig per eseguire l'override del driver per la reflex digitale connessa a WinUSB o libusb. Questo approccio funziona bene per me e per molti altri utenti, ma dovresti utilizzarlo a tuo rischio.

Su Linux, probabilmente dovrai impostare autorizzazioni personalizzate per consentire l'accesso alla tua reflex digitale tramite WebUSB, anche se questo dipende dalla tua distribuzione.

Su macOS e Android, la demo dovrebbe funzionare immediatamente. Se stai provando su uno smartphone Android, assicurati di passare alla modalità Orizzontale, poiché non ho dedicato molto impegno a renderlo adattabile (le PR sono benvenute):

Smartphone Android collegato a una fotocamera Canon tramite un cavo USB-C.
La stessa demo in esecuzione su uno smartphone Android. Immagine di Surma.

Per una guida più approfondita sull'utilizzo multipiattaforma di WebUSB, consulta la sezione "Considerazioni specifiche per la piattaforma" di "Creazione di un dispositivo per WebUSB".

Aggiunta di un nuovo backend a libusb

Ora passiamo ai dettagli tecnici. Sebbene sia possibile fornire un'API shim simile a libusb (come è già stato fatto in precedenza) e collegare altre applicazioni, questo approccio è soggetto a errori e rende più difficile qualsiasi ulteriore estensione o manutenzione. Volevo fare le cose per bene, in modo che in futuro potessero essere potenzialmente integrate in libusb.

Fortunatamente, il file README di libusb dice:

"libusb è astratto internamente in modo tale che, si spera, possa essere trasferito su altri sistemi operativi. Per saperne di più, consulta il file PORTING."

libusb è strutturata in modo che l'API pubblica sia separata dai "backend". Questi backend sono responsabili dell'elenco, dell'apertura, della chiusura e della comunicazione effettiva con i dispositivi tramite le API di basso livello del sistema operativo. In questo modo, libusb astrae già le differenze tra Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku e Solaris e funziona su tutte queste piattaforme.

Ho dovuto aggiungere un altro backend per il "sistema operativo" Emscripten+WebUSB. Le implementazioni per questi backend si trovano nella cartella 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

Ogni backend include l'intestazione libusbi.h con tipi e helper comuni e deve esporre una variabile usbi_backend di tipo usbi_os_backend. Ad esempio, ecco come appare il backend di 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),
};

Esaminando le proprietà, possiamo vedere che la struttura include il nome del backend, un insieme delle sue funzionalità, i gestori per varie operazioni USB di basso livello sotto forma di puntatori di funzione e, infine, le dimensioni da allocare per l'archiviazione dei dati privati a livello di dispositivo/contesto/trasferimento.

I campi di dati privati sono utili almeno per archiviare gli handle del sistema operativo per tutti questi elementi, poiché senza handle non sappiamo a quale elemento si applica una determinata operazione. Nell'implementazione web, gli handle del sistema operativo sarebbero gli oggetti JavaScript WebUSB sottostanti. Il modo naturale per rappresentarli e archiviarli in Emscripten è tramite la classe emscripten::val, fornita come parte di Embind (il sistema di binding di Emscripten).

La maggior parte dei backend nella cartella è implementata in C, ma alcuni sono implementati in C++. Embind funziona solo con C++, quindi la scelta è stata fatta per me e ho aggiunto libusb/libusb/os/emscripten_webusb.cpp con la struttura richiesta e con sizeof(val) per i campi di dati privati:

#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),
};

Memorizzazione di oggetti WebUSB come handle del dispositivo

libusb fornisce puntatori pronti all'uso all'area allocata per i dati privati. Per lavorare con questi puntatori come istanze val, ho aggiunto piccoli helper che li creano in loco, li recuperano come riferimenti e spostano i valori:

// 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 asincrone in contesti C sincroni

Ora era necessario un modo per gestire le API WebUSB asincrone in cui libusb prevede operazioni sincrone. Per questo, potrei utilizzare Asyncify o, più nello specifico, la sua integrazione Embind tramite val::await().

Volevo anche gestire correttamente gli errori WebUSB e convertirli in codici di errore libusb, ma al momento Embind non offre un modo per gestire le eccezioni JavaScript o i rifiuti di Promise dal lato C++. Questo problema può essere risolto rilevando un rifiuto sul lato JavaScript e convertendo il risultato in un oggetto { error, value } che ora può essere analizzato in sicurezza dal lato C++. Ho utilizzato una combinazione della macro EM_JS e delle 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()};
  }
};

Ora posso utilizzare promise_result::await() su qualsiasi Promise restituito dalle operazioni WebUSB e ispezionare i relativi campi error e value separatamente.

Ad esempio, il recupero di un val che rappresenta un USBDevice da libusb_device_handle, la chiamata del relativo metodo open(), l'attesa del risultato e la restituzione di un codice di errore come codice di stato libusb hanno il seguente aspetto:

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

Enumerazione dei dispositivi

Naturalmente, prima di poter aprire un dispositivo, libusb deve recuperare un elenco dei dispositivi disponibili. Il backend deve implementare questa operazione tramite un gestore get_device_list.

La difficoltà è che, a differenza di altre piattaforme, non esiste un modo per enumerare tutti i dispositivi USB connessi sul web per motivi di sicurezza. Il flusso è invece suddiviso in due parti. Innanzitutto, l'applicazione web richiede dispositivi con proprietà specifiche tramite navigator.usb.requestDevice() e l'utente sceglie manualmente il dispositivo che vuole esporre o rifiuta la richiesta di autorizzazione. In seguito, l'applicazione elenca i dispositivi già approvati e connessi tramite navigator.usb.getDevices().

All'inizio ho provato a utilizzare requestDevice() direttamente nell'implementazione del gestore get_device_list. Tuttavia, la visualizzazione di una richiesta di autorizzazione con un elenco di dispositivi connessi è considerata un'operazione sensibile e deve essere attivata dall'interazione dell'utente (ad esempio un clic su un pulsante in una pagina), altrimenti restituisce sempre una promessa rifiutata. Le applicazioni libusb spesso vogliono elencare i dispositivi connessi all'avvio dell'applicazione, quindi l'utilizzo di requestDevice() non era un'opzione.

Invece, ho dovuto lasciare l'invocazione di navigator.usb.requestDevice() allo sviluppatore finale ed esporre solo i dispositivi già approvati da 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 maggior parte del codice di backend utilizza val e promise_result in modo simile a quanto mostrato sopra. Esistono altri hack interessanti nel codice di gestione del trasferimento di dati, ma questi dettagli di implementazione sono meno importanti ai fini di questo articolo. Se ti interessa, assicurati di controllare il codice e i commenti su GitHub.

Porting dei loop di eventi sul web

Un altro aspetto della porta libusb di cui voglio parlare è la gestione degli eventi. Come descritto nell'articolo precedente, la maggior parte delle API nei linguaggi di sistema come C è sincrona e la gestione degli eventi non fa eccezione. In genere viene implementato tramite un loop infinito che "esegue il polling " (tenta di leggere i dati o blocca l'esecuzione finché non sono disponibili alcuni dati) da un insieme di origini di I/O esterne e, quando almeno una di queste risponde, la passa come evento al gestore corrispondente. Una volta terminato il gestore, il controllo torna al loop, che si mette in pausa per un altro polling.

Questo approccio sul web presenta un paio di problemi.

Innanzitutto, WebUSB non espone e non può esporre gli handle non elaborati dei dispositivi sottostanti, quindi il polling diretto non è un'opzione. In secondo luogo, libusb utilizza le API eventfd e pipe per altri eventi, nonché per la gestione dei trasferimenti su sistemi operativi senza handle di dispositivi non elaborati, ma eventfd non è attualmente supportato in Emscripten e pipe, sebbene supportato, al momento non è conforme alla specifica e non può attendere gli eventi.

Infine, il problema principale è che il web ha un proprio loop di eventi. Questo loop di eventi globale viene utilizzato per qualsiasi operazione di I/O esterna (tra cui fetch(), timer o, in questo caso, WebUSB) e richiama i gestori di eventi o Promise ogni volta che le operazioni corrispondenti vengono completate. L'esecuzione di un altro loop di eventi infinito nidificato bloccherà l'avanzamento del loop di eventi del browser, il che significa che non solo l'interfaccia utente non risponderà, ma anche che il codice non riceverà mai notifiche per gli stessi eventi di I/O che sta aspettando. In genere, questo comporta un deadlock, ed è quello che è successo anche quando ho provato a utilizzare libusb in una demo. La pagina si è bloccata.

Come per altre operazioni di I/O di blocco, per trasferire questi loop di eventi sul web, gli sviluppatori devono trovare un modo per eseguirli senza bloccare il thread principale. Un modo è rifattorizzare l'applicazione per gestire gli eventi di I/O in un thread separato e passare i risultati al thread principale. L'altro consiste nell'utilizzare Asyncify per mettere in pausa il loop e attendere gli eventi in modo non bloccante.

Non volevo apportare modifiche significative a libusb o gPhoto2 e avevo già utilizzato Asyncify per l'integrazione di Promise, quindi ho scelto questo percorso. Per simulare una variante di blocco di poll(), per la proof of concept iniziale ho utilizzato un loop come mostrato di seguito:

#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

Ecco cosa fa:

  1. Chiama poll() per verificare se il backend ha già segnalato eventi. Se ce ne sono, il loop si interrompe. In caso contrario, l'implementazione di poll() di Emscripten restituirà immediatamente 0.
  2. Chiamate emscripten_sleep(0). Questa funzione utilizza Asyncify e setTimeout() in background e viene utilizzata qui per restituire il controllo al loop di eventi principale del browser. In questo modo, il browser può gestire tutte le interazioni degli utenti e gli eventi di I/O, incluso WebUSB.
  3. Controlla se il timeout specificato è scaduto e, in caso contrario, continua il loop.

Come indicato nel commento, questo approccio non era ottimale, perché continuava a salvare e ripristinare l'intero stack di chiamate con Asyncify anche quando non c'erano ancora eventi USB da gestire (ovvero la maggior parte delle volte) e perché setTimeout() stesso ha una durata minima di 4 ms nei browser moderni. Tuttavia, ha funzionato abbastanza bene da produrre un live streaming da 13-14 FPS da una DSLR nella proof of concept.

In seguito, ho deciso di migliorarlo sfruttando il sistema di eventi del browser. Esistono diversi modi per migliorare ulteriormente questa implementazione, ma per il momento ho scelto di emettere eventi personalizzati direttamente sull'oggetto globale, senza associarli a una particolare struttura di dati libusb. Ho utilizzato il seguente meccanismo di attesa e notifica basato sulla 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 funzione em_libusb_notify() viene utilizzata ogni volta che libusb tenta di segnalare un evento, ad esempio il completamento del trasferimento di dati:

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
}

Nel frattempo, la parte em_libusb_wait() viene utilizzata per "riattivare" dalla sospensione di Asyncify quando viene ricevuto un evento em-libusb o il timeout è scaduto:

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

Grazie alla significativa riduzione delle sospensioni e dei risvegli, questo meccanismo ha risolto i problemi di efficienza della precedente implementazione basata su emscripten_sleep() e ha aumentato il throughput della demo della DSLR da 13-14 FPS a oltre 30 FPS costanti, sufficienti per un feed in diretta fluido.

Sistema di build e primo test

Dopo aver completato il backend, ho dovuto aggiungerlo a Makefile.am e configure.ac. L'unica parte interessante è la modifica dei flag specifici di 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']"
  ;;

Innanzitutto, i file eseguibili sulle piattaforme Unix normalmente non hanno estensioni del file. Emscripten, tuttavia, produce un output diverso a seconda dell'estensione richiesta. Utilizzo AC_SUBST(EXEEXT, …) per modificare l'estensione dell'eseguibile in .html in modo che qualsiasi eseguibile all'interno di un pacchetto (test ed esempi) diventi un HTML con la shell predefinita di Emscripten che si occupa del caricamento e della creazione di istanze di JavaScript e WebAssembly.

In secondo luogo, poiché utilizzo Embind e Asyncify, devo attivare queste funzionalità (--bind -s ASYNCIFY) e consentire la crescita dinamica della memoria (-s ALLOW_MEMORY_GROWTH) tramite i parametri del linker. Purtroppo, non esiste un modo per una libreria di segnalare questi flag al linker, quindi ogni applicazione che utilizza questa porta libusb dovrà aggiungere gli stessi flag del linker anche alla propria configurazione di compilazione.

Infine, come accennato in precedenza, WebUSB richiede che l'enumerazione dei dispositivi venga eseguita tramite un'attivazione dall'utente. Gli esempi e i test di libusb presuppongono che possano enumerare i dispositivi all'avvio e non vanno a buon fine con un errore senza modifiche. Invece, ho dovuto disattivare l'esecuzione automatica (-s INVOKE_RUN=0) ed esporre il metodo manuale callMain() (-s EXPORTED_RUNTIME_METHODS=...).

Una volta completato tutto questo, ho potuto pubblicare i file generati con un server web statico, inizializzare WebUSB ed eseguire manualmente gli eseguibili HTML con l'aiuto di DevTools.

Screenshot che mostra una finestra di Chrome con DevTools aperto su una pagina &quot;testlibusb&quot; pubblicata localmente. La console DevTools sta valutando &quot;navigator.usb.requestDevice({ filters: [] })&quot;, che ha attivato una richiesta di autorizzazione e attualmente chiede all'utente di scegliere un dispositivo USB da condividere con la pagina. Al momento è selezionata ILCE-6600 (una fotocamera Sony).

Screenshot del passaggio successivo, con DevTools ancora aperto. Dopo aver selezionato il dispositivo, Console ha valutato una nuova espressione &quot;Module.callMain(['-v'])&quot;, che ha eseguito l'app &quot;testlibusb&quot; in modalità dettagliata. L'output mostra varie informazioni dettagliate sulla videocamera USB collegata in precedenza: produttore Sony, prodotto ILCE-6600, numero di serie, configurazione e così via.

Non sembra molto, ma quando si esegue il porting delle librerie su una nuova piattaforma, raggiungere la fase in cui viene prodotto un output valido per la prima volta è piuttosto entusiasmante.

Utilizzo della porta

Come accennato sopra, il port dipende da alcune funzionalità di Emscripten che attualmente devono essere abilitate nella fase di collegamento dell'applicazione. Se vuoi utilizzare questa porta libusb nella tua applicazione, devi:

  1. Scarica l'ultima versione di libusb come archivio nell'ambito della build o aggiungila come sottomodulo git nel progetto.
  2. Esegui autoreconf -fiv nella cartella libusb.
  3. Esegui emconfigure ./configure –host=wasm32 –prefix=/some/installation/path per inizializzare il progetto per la cross-compilazione e impostare un percorso in cui inserire gli artefatti creati.
  4. Esegui emmake make install.
  5. Indica alla tua applicazione o alla libreria di livello superiore di cercare libusb nel percorso scelto in precedenza.
  6. Aggiungi i seguenti flag agli argomenti del link della tua applicazione: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

Al momento la libreria presenta alcune limitazioni:

  • Nessun supporto per l'annullamento del trasferimento. Si tratta di una limitazione di WebUSB che, a sua volta, deriva dalla mancanza di annullamento del trasferimento multipiattaforma in libusb stesso.
  • Nessun supporto per il trasferimento isocrono. Non dovrebbe essere difficile aggiungerla seguendo l'implementazione delle modalità di trasferimento esistenti come esempi, ma è anche una modalità piuttosto rara e non avevo dispositivi su cui testarla, quindi per ora l'ho lasciata come non supportata. Se hai questi dispositivi e vuoi contribuire alla libreria, le PR sono benvenute.
  • Le limitazioni multipiattaforma menzionate in precedenza. Queste limitazioni sono imposte dai sistemi operativi, quindi non possiamo fare molto, se non chiedere agli utenti di eseguire l'override del driver o delle autorizzazioni. Tuttavia, se stai eseguendo il porting di dispositivi HID o seriali, puoi seguire l'esempio di libusb ed eseguire il porting di un'altra libreria in un'altra API Fugu. Ad esempio, puoi trasferire una libreria C hidapi in WebHID ed evitare del tutto i problemi associati all'accesso USB di basso livello.

Conclusione

In questo post ho mostrato come, con l'aiuto delle API Emscripten, Asyncify e Fugu, anche le librerie di basso livello come libusb possono essere trasferite sul web con alcuni trucchi di integrazione.

Il porting di queste librerie di basso livello essenziali e ampiamente utilizzate è particolarmente gratificante perché, a sua volta, consente di portare sul web anche librerie di livello superiore o persino intere applicazioni. In questo modo, le esperienze che in precedenza erano limitate agli utenti di una o due piattaforme vengono aperte a tutti i tipi di dispositivi e sistemi operativi, rendendole disponibili con un semplice clic su un link.

Nel prossimo post illustrerò i passaggi necessari per creare la demo web gPhoto2, che non solo recupera le informazioni sul dispositivo, ma utilizza anche ampiamente la funzionalità di trasferimento di libusb. Nel frattempo, spero che l'esempio di libusb ti sia stato d'ispirazione e che proverai la demo, giocherai con la libreria stessa o forse farai un passo avanti e trasferirai un'altra libreria ampiamente utilizzata anche in una delle API Fugu.