Portare le applicazioni USB sul Web. Parte 1: libusb

Scopri come il codice che interagisce con 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 le app sul web utilizzando le API di file system con l'API File System Access, WebAssembly e Asyncify. Ora voglio continuare lo stesso argomento relativo all'integrazione delle API Fugu con WebAssembly e alla portabilità delle app sul web senza perdere funzionalità importanti.

Ti mostrerò come le app che comunicano con i dispositivi USB possono essere trasferite sul web trasferendo libusb, una popolare libreria USB scritta in C, a WebAssembly (tramite Emscripten), Asyncify e WebUSB.

Prima cosa: una demo

La cosa più importante da fare al momento di trasferire una libreria è la scelta della demo giusta, qualcosa che mostri le capacità della libreria trasferita, consentendoti di testarla in diversi modi e allo stesso tempo visivamente accattivante.

L'idea che ho scelto è stato un telecomando DSLR. In particolare, il progetto open source gPhoto2 è stato in questo ambito abbastanza a lungo da eseguire il reverse engineering e l'implementazione del 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.

Descrivi la procedura per la creazione di questa demo in due parti. In questo post del blog, descriverò come ho portato libusb stesso e quali suggerimenti potrebbero essere necessari per trasferire altre librerie popolari alle API Fugu. Nel secondo post, vi parlerò in dettaglio della portabilità e dell'integrazione di gPhoto2.

Alla fine, ho un'applicazione web funzionante che mostra l'anteprima del feed pubblicato da una fotocamera DSLR e può controllarne le impostazioni tramite USB. Guarda il video dal vivo o la demo preregistrata prima di rileggere i dettagli tecnici:

La demo eseguita su un laptop connesso a una videocamera Sony.

Nota sulle peculiarità della fotocamera

Potresti aver notato che la modifica delle impostazioni richiede del tempo durante il video. Come con 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 soltanto comandi per aumentarli o diminuirli del numero di passi specificato. Per rendere le cose più complicate, non restituisce neanche un elenco dei valori effettivamente supportati: l'elenco restituito sembra essere hardcoded in molti modelli di fotocamere Sony.

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

  1. Fai uno o più passi nella direzione del valore scelto.
  2. Attendi un po' che la videocamera aggiorni le impostazioni.
  3. Leggi il valore su cui è stata effettivamente atterrata la videocamera.
  4. Verifica che l'ultimo passaggio non abbia superato il valore desiderato e che non sia stato completato alla fine o all'inizio dell'elenco.
  5. Ripeti.

Può richiedere del tempo, ma se il valore è effettivamente supportato dalla fotocamera, sarà disponibile e, in caso contrario, si fermerà al valore supportato più vicino.

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

Note importanti sulla compatibilità multipiattaforma

Sfortunatamente, su Windows qualsiasi "famosa" ai dispositivi, comprese le fotocamere DSLR, viene assegnato un driver di sistema non compatibile con WebUSB. Se vuoi provare la demo su Windows, dovrai utilizzare uno strumento come Zadig per eseguire l'override del driver della DSLR collegata su WinUSB o libusb. Questo approccio funziona bene per me e molti altri utenti, ma dovresti usarlo a tuo rischio e pericolo.

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

Su macOS e Android, la demo dovrebbe funzionare subito. Se stai provando a usare questa modalità su uno smartphone Android, assicurati di passare alla modalità Orizzontale, dato che non ho lavorato molto per renderla reattiva (i PR sono ben accetti):

Smartphone Android collegato a una fotocamera Canon tramite 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 "Considerazioni specifiche per la piattaforma" di "Creazione di un dispositivo per WebUSB".

Aggiunta di un nuovo backend a libusb

Passiamo ora ai dettagli tecnici. Sebbene sia possibile fornire un'API shim simile a libusb (questa operazione è già stata eseguita da altri in precedenza) e collegare altre applicazioni, questo approccio è soggetto a errori e complica ulteriori estensioni o manutenzione. Volevo fare le cose nel modo giusto, in un modo che potenzialmente potessero essere ripagati a monte e fusa in libusb in futuro.

Fortunatamente, il file README di libusb dice:

"libusb è astratto internamente in modo da poter, possibilmente, essere portato su altri sistemi operativi. Per ulteriori informazioni, consulta il file PORTING."

libusb è strutturato 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. È così che 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, questo è l'aspetto del 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),
};

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

I campi dei dati privati sono utili almeno per archiviare gli handle del sistema operativo per tutte queste cose, dato che senza handle non sappiamo a quale elemento si applica una determinata operazione. Nell'implementazione web, gli handle del sistema operativo sono gli oggetti JavaScript WebUSB sottostanti. Il modo naturale per rappresentarli e archiviarli in Emscripten è tramite la classe emscripten::val, che fa parte di Embind (il sistema di associazione di Emscripten).

La maggior parte dei backend nella cartella sono implementati in C++, mentre alcuni sono implementati in C++. Embind funziona solo con C++, quindi ho fatto una scelta personale e ho aggiunto libusb/libusb/os/emscripten_webusb.cpp con la struttura obbligatoria e con sizeof(val) per i campi dei 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),
};

Archiviazione di oggetti WebUSB come handle del dispositivo

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

// 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 occorreva un modo per gestire le API WebUSB asincrone dove libusb prevede operazioni sincrone. Per farlo, potrei usare 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 ha alcun modo di 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 modo sicuro dal lato C++. Per farlo, 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, recuperare un val che rappresenta un USBDevice da libusb_device_handle, chiamare il relativo metodo open(), in attesa del risultato e restituire un codice di errore come codice di stato libusb è simile al seguente:

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 dispositivo

Ovviamente, 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.

Il problema è che, a differenza di altre piattaforme, non è possibile elencare tutti i dispositivi USB collegati sul web per motivi di sicurezza. Il flusso è invece suddiviso in due parti. Innanzitutto, l'applicazione web richiede i dispositivi con proprietà specifiche tramite navigator.usb.requestDevice() e l'utente sceglie manualmente il dispositivo da esporre o rifiuta la richiesta di autorizzazione. Successivamente, 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, mostrare una richiesta di autorizzazione con un elenco di dispositivi connessi è considerata un'operazione sensibile e deve essere attivata dall'interazione dell'utente (ad esempio il clic su un pulsante su una pagina), altrimenti restituisce sempre una promessa rifiutata. Spesso le applicazioni libusb desideravano elencare i dispositivi connessi all'avvio dell'applicazione, quindi non era possibile utilizzare requestDevice().

Ho dovuto invece lasciare la chiamata di navigator.usb.requestDevice() allo sviluppatore finale e 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 già mostrato sopra. Esistono alcuni altri attacchi interessanti nel codice di gestione del trasferimento di dati, ma i dettagli di implementazione sono meno importanti ai fini di questo articolo. Se ti interessa, assicurati di controllare il codice e i commenti su GitHub.

Trasferire i 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 sono sincrona e la gestione degli eventi non fa eccezione. Di solito viene implementato tramite un loop infinito che "esegue sondaggi" (prova a leggere i dati o blocca l'esecuzione finché non sono disponibili dati) da un insieme di origini I/O esterne e, quando almeno una di queste risponde, lo trasmette come evento al gestore corrispondente. Una volta terminato il gestore, il controllo torna al loop e si ferma per un altro sondaggio.

Esistono un paio di problemi con questo approccio sul web.

Innanzitutto, WebUSB non espone e non può esporre gli handle non elaborati dei dispositivi sottostanti, quindi il polling diretto non è possibile. In secondo luogo, libusb utilizza le API eventfd e pipe per altri eventi e per gestire i trasferimenti sui sistemi operativi senza handle dei dispositivi non elaborati, ma eventfd al momento non è supportato in Emscripten e pipe, sebbene supportato, attualmente non è conforme alle specifiche e non può attendere gli eventi.

Infine, il problema maggiore è che il web ha il suo loop di eventi. Questo loop di eventi globale viene utilizzato per qualsiasi operazione di I/O esterna (inclusi fetch(), timer o, in questo caso, WebUSB) e richiama i gestori di eventi o Promise ogni volta che le operazioni corrispondenti terminano. L'esecuzione di un altro loop di eventi infinito nidificato impedisce l'avanzamento del ciclo di eventi del browser, il che significa che non solo la UI non risponde, ma anche che il codice non riceverà mai notifiche per gli stessi eventi I/O che sta aspettando. Questo di solito si verifica in una situazione di stallo ed è quello che è successo quando ho provato a usare libusb anche in una demo. La pagina si è bloccata.

Come con altri 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 è eseguire il refactoring dell'applicazione per gestire gli eventi I/O in un thread separato e ritrasmettere i risultati al thread principale. L'altro consiste nell'utilizzare Asyncify per mettere in pausa il loop e attendere gli eventi in modo che non impediscano alcun blocco.

Non volevo apportare modifiche significative a libusb o gPhoto2 e ho già utilizzato Asyncify per l'integrazione di Promise, quindi è questo il percorso che ho scelto. 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

Descrizione:

  1. Richiama poll() per controllare se sono già stati segnalati eventi dal backend. Se sono presenti, il giro si interrompe. In caso contrario, l'implementazione di Emscripten di poll() tornerà immediatamente con 0.
  2. Chiama emscripten_sleep(0). Questa funzione utilizza Asyncify e setTimeout() in background e viene qui utilizzata per restituire il controllo al loop di eventi del browser principale. In questo modo il browser può gestire le interazioni degli utenti e gli eventi di I/O, incluso WebUSB.
  3. Controlla se il timeout specificato è ancora scaduto e, in caso contrario, continua il loop.

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

In seguito, ho deciso di migliorarlo sfruttando il sistema di eventi del browser. Esistono diversi modi in cui questa implementazione potrebbe essere migliorata ulteriormente, ma per ora ho scelto di emettere eventi personalizzati direttamente sull'oggetto globale, senza associarli a una particolare struttura di dati libusb. Per farlo, ho utilizzato il seguente meccanismo di attesa e notifica in base alla 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 "svegliarsi" dalla modalità di sospensione asincrona 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;
}

A causa della significativa riduzione delle ore di sospensione e riattivazione, questo meccanismo ha risolto i problemi di efficienza della precedente implementazione basata su emscripten_sleep() e ha aumentato la velocità effettiva della demo DSLR da 13-14 FPS a 30+ FPS, un risultato sufficiente per un feed dal vivo regolare.

Il sistema di compilazione e il primo test

Al termine del backend, ho dovuto aggiungerlo a Makefile.am e configure.ac. L'unica cosa interessante qui è 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, gli eseguibili sulle piattaforme Unix normalmente non hanno estensioni di file. Tuttavia, Emscripten produce un output diverso a seconda dell'estensione richiesta. Utilizzo AC_SUBST(EXEEXT, …) per modificare l'estensione eseguibile in .html in modo che qualsiasi eseguibile all'interno di un pacchetto (test ed esempi) diventi un codice HTML con la shell predefinita di Emscripten che si occupa di caricare e creare un'istanza per JavaScript e WebAssembly.

In secondo luogo, poiché utilizzo Embind e Asyncify, devo abilitare queste funzionalità (--bind -s ASYNCIFY) e consentire la crescita della memoria dinamica (-s ALLOW_MEMORY_GROWTH) tramite parametri del linker. Sfortunatamente, una libreria non ha modo di segnalare questi flag al linker, quindi ogni applicazione che utilizza questa porta libusb dovrà aggiungere gli stessi flag del linker anche nella configurazione della build.

Infine, come accennato in precedenza, WebUSB richiede l'enumerazione del dispositivo tramite un gesto dell'utente. Gli esempi e i test libusb presuppongono che possano enumerare i dispositivi all'avvio e non restituiscono un errore senza modifiche. Ho dovuto invece disattivare l'esecuzione automatica (-s INVOKE_RUN=0) ed esporre il metodo callMain() manuale (-s EXPORTED_RUNTIME_METHODS=...).

Fatto 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 in una pagina &quot;testlibusb&quot; pubblicata localmente. La console DevTools sta valutando &quot;navigator.usb.requestDevice({ filtri: [] })&quot;, il che ha attivato una richiesta di autorizzazione e al momento sta chiedendo all&#39;utente di scegliere un dispositivo USB da condividere con la pagina. Al momento è selezionata la ILCE-6600 (una videocamera Sony).

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

Non sembra molto, ma quando trasferisci le librerie su una nuova piattaforma, arrivare al punto in cui viene prodotto per la prima volta un output valido è davvero entusiasmante.

Utilizzo della porta

Come accennato sopra, la porta dipende da alcune funzionalità Emscripten che al momento devono essere abilitate nella fase di collegamento dell'applicazione. Se vuoi utilizzare questa porta libusb nella tua applicazione, ecco cosa dovrai fare:

  1. Scarica la versione più recente di libusb come archivio come parte della build oppure aggiungilo come sottomodulo Git nel tuo 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 per impostare un percorso in cui inserire gli artefatti creati.
  4. Esegui emmake make install.
  5. Punta la tua applicazione o la tua libreria di livello superiore per cercare il libusb nel percorso scelto in precedenza.
  6. Aggiungi i seguenti flag agli argomenti link dell'applicazione: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

Al momento la libreria presenta alcune limitazioni:

  • Non offre assistenza per l'annullamento del trasferimento. Questo è un limite di WebUSB, che, a sua volta, deriva dalla mancanza di cancellazione dei trasferimenti multipiattaforma nel libusb stesso.
  • Nessun supporto per il trasferimento sincronizzato. Non dovrebbe essere difficile aggiungerla seguendo l'implementazione delle modalità di trasferimento esistenti come esempi, ma è anche una modalità un po' rara e non avevo dispositivi su cui provarla, quindi per ora l'ho lasciata non supportata. Se disponi di questi dispositivi e vuoi contribuire alla biblioteca, i PR sono i benvenuti.
  • Il nome precedente ha menzionato le limitazioni multipiattaforma. Queste limitazioni sono imposte dai sistemi operativi, quindi non possiamo fare molto qui, se non chiedere agli utenti di ignorare il driver o le autorizzazioni. Tuttavia, se esegui la portabilità di dispositivi HID o seriali, puoi seguire l'esempio di libusb e trasferire altre librerie a un'altra API Fugu. Ad esempio, potresti trasferire una libreria C hidapi in WebHID ed evitare del tutto questi 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.

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

Nel prossimo post indicherò i passaggi per la creazione della demo di gPhoto2 per il web, che non solo recupera le informazioni del dispositivo, ma utilizza anche ampiamente la funzione di trasferimento di libusb. Nel frattempo, spero che tu abbia trovato l'esempio di libusb di ispirazione e che proverai la demo, ti giocherai con la libreria stessa o forse andrai avanti e porterai anche un'altra libreria molto usata in una delle API Fugu.