Portare le applicazioni USB sul Web. Parte 1: libusb

Scopri come il codice che interagisce con i dispositivi esterni può essere portato 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 eseguendo il porting di libusb, una popolare libreria USB scritta in C, in WebAssembly (tramite Emscripten), Asyncify e WebUSB.

Prima di tutto, una demo

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

L'idea che ho scelto è il telecomando per DSLR. In particolare, un progetto open source gPhoto2 è presente in questo spazio da tempo sufficiente per eseguire il reverse engineering e implementare il supporto per un'ampia gamma di fotocamere digitali. Supporta diversi protocolli, ma quello che mi interessava di più era il supporto USB, che viene eseguito 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, entrerò nei dettagli sul porting e sull'integrazione di gPhoto2 stesso.

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

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

Nota sulle peculiarità specifiche della videocamera

Potresti aver notato che la modifica delle impostazioni richiede un po' di tempo nel 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.

La 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 in base al numero di passaggi specificato. A complicare ulteriormente la questione, non viene restituito nemmeno un elenco dei valori effettivamente supportati: l'elenco restituito sembra essere 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 più) nella direzione del valore scelto.
  2. Attendi un po' affinché la videocamera aggiorni le impostazioni.
  3. Leggi il valore su cui è stata effettivamente atterrata la videocamera.
  4. Verifica che l'ultimo passaggio non abbia saltato il valore desiderato né sia andato oltre la fine o l'inizio dell'elenco.
  5. Ripeti.

L'operazione può richiedere del tempo, ma se il valore è effettivamente supportato dalla videocamera, verrà raggiunto. In caso contrario, si fermerà sul valore supportato più vicino.

È probabile che le altre videocamere abbiano insiemi di impostazioni, API di base e peculiarità diversi. Tieni presente che gPhoto2 è un progetto open source e i test automatici o manuali di tutti i modelli di fotocamere disponibili non sono semplicemente possibili, quindi sono sempre ben accetti report dettagliati dei problemi e PR (ma assicurati prima di riprodurre i problemi con il client gPhoto2 ufficiale).

Note importanti sulla compatibilità multipiattaforma

Purtroppo, su Windows a tutti i dispositivi "noti", incluse 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 sostituire il driver della DSLR collegata con WinUSB o libusb. Questo approccio funziona bene per me e per molti altri utenti, ma devi utilizzarlo a tuo rischio.

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

Su macOS e Android, la demo dovrebbe funzionare immediatamente. 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 un cavo USB-C.
La stessa demo in esecuzione su uno smartphone Android. Foto di Surma.

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

Aggiunta di un nuovo backend a libusb

Passiamo 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 per bene, in modo che in futuro il mio contributo potesse essere restituito all'upstream e unito a libusb.

Fortunatamente, il file README di libusb dice:

"libusb è astratto internamente in modo che possa 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. In questo modo, libusb rimuove 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, il backend Windows ha il seguente aspetto:

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 di dati privati sono utili almeno per memorizzare gli handle del sistema operativo per tutti questi elementi, in quanto 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, fornita nell'ambito 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 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),
};

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 costruiscono in-place, 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 è necessario un modo per gestire le API WebUSB asincrone in cui libusb prevede operazioni sincrone. Per questo, potrei utilizzare Asyncify o, più specificamente, la sua integrazione di Embind tramite val::await().

Volevo anche gestire correttamente gli errori WebUSB e convertirli in codici di errore libusb, ma al momento Embind non ha modo di gestire le eccezioni JavaScript o i rifiuti Promise 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++. Ho utilizzato una combinazione di macro EM_JS e 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 campi error e value separatamente.

Ad esempio, il recupero di un val che rappresenta un USBDevice da libusb_device_handle, la chiamata al relativo metodo open(), l'attesa del risultato e la restituzione di 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

Naturalmente, prima di poter aprire un dispositivo, libusb deve recuperare un elenco di 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 connessi 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 dell'handler 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 potrebbero spesso voler elencare i dispositivi connessi all'avvio dell'applicazione, pertanto 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 già mostrato sopra. Esistono altri hack più interessanti nel codice di gestione del trasferimento dei dati, ma questi dettagli di implementazione sono meno importanti ai fini di questo articolo. Se ti interessa, dai un'occhiata al codice e ai commenti su GitHub.

Portare 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 un "polling" (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 passa come evento al gestore corrispondente. Al termine del gestore, il controllo torna al loop e si mette in pausa per un altro sondaggio.

Esistono alcuni problemi con questo approccio sul web.

Innanzitutto, WebUSB non espone e non può esporre handle non elaborati dei dispositivi sottostanti, pertanto non è possibile eseguirne il polling direttamente. In secondo luogo, libusb utilizza le API eventfd e pipe per altri eventi e per gestire i trasferimenti su sistemi operativi senza handle dei 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 più grande è che il web ha il proprio 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 gestori di eventi o Promise ogni volta che le operazioni corrispondenti terminano. L'esecuzione di un altro loop di eventi infinito nidificato impedirà al loop di eventi del browser di progredire, il che significa che non solo l'interfaccia utente non risponderà, ma anche che il codice non riceverà mai notifiche per gli stessi eventi I/O che sta aspettando. Di solito si verifica un deadlock, ed è quello che è successo anche quando ho provato a utilizzare libusb 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'altra è 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 ho già utilizzato Asyncify per l'integrazione di Promise, quindi è la strada che ho scelto. Per simulare una variante bloccante di poll(), per la prova del concetto iniziale ho utilizzato un ciclo 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. Chiama poll() per verificare se sono stati registrati eventi dal backend. Se sono presenti, il giro 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() sotto il cofano e viene utilizzata qui per restituire il controllo al loop di eventi principale del browser. In questo modo, il browser può gestire qualsiasi interazione utente ed evento I/O, incluso WebUSB.
  3. Controlla se il timeout specificato è già scaduto e, in caso contrario, continua il ciclo.

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 (il che accade la maggior parte del tempo) e perché setTimeout() stesso ha una durata minima di 4 ms nei browser moderni. Tuttavia, ha funzionato abbastanza bene da produrre un live streaming a 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 nell'oggetto globale, senza associarli a una determinata 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 "riattivare" il sonno di Asyncify quando viene ricevuto un evento em-libusb o quando 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 di sospensioni e risvegli, questo meccanismo ha risolto i problemi di efficienza dell'implementazione precedente basata su emscripten_sleep() e ha aumentato il throughput della demo DSLR da 13-14 FPS a oltre 30 FPS, che è sufficiente per un feed live fluido.

Sistema di compilazione e primo test

Al termine del backend, ho dovuto aggiungerlo a Makefile.am e configure.ac. L'unico aspetto 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, gli eseguibili sulle piattaforme Unix in genere non hanno estensioni di file. Emscripten, tuttavia, produce output diversi a seconda dell'estensione richiesta. Sto utilizzando AC_SUBST(EXEEXT, …) per modificare l'estensione dell'eseguibile in .html in modo che qualsiasi file eseguibile all'interno di un pacchetto, test ed esempi, diventi un file HTML con la shell predefinita di Emscripten che si occupa del caricamento e dell'inizializzazione di JavaScript e WebAssembly.

In secondo luogo, poiché utilizzo Embind e Asyncify, devo abilitare queste funzionalità (--bind -s ASYNCIFY) e consentire la crescita dinamica della memoria (-s ALLOW_MEMORY_GROWTH) tramite parametri del linker. Purtroppo, non è possibile per una libreria segnalare questi flag al linker, pertanto 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 gesto dell'utente. Gli esempi e i test di libusb presuppongono di poter enumerare i dispositivi all'avvio e non riescono a eseguire l'operazione senza errori. Ho dovuto invece disattivare l'esecuzione automatica (-s INVOKE_RUN=0) ed esporre il metodo callMain() manuale (-s EXPORTED_RUNTIME_METHODS=...).

Al termine di questa procedura, 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 aperta 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 ILCE-6600 (una fotocamera 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 e così via.

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

Utilizzo della porta

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

  1. Scarica la versione più recente di libusb come archivio nell'ambito della compilazione 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 compilazione incrociata e per impostare un percorso in cui inserire gli artefatti compilati.
  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 di collegamento dell'applicazione: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

Al momento la raccolta 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 stessa.
  • Nessun supporto per il trasferimento isocrono. Non dovrebbe essere difficile aggiungerlo 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 il momento l'ho lasciata come non supportata. Se disponi di questi dispositivi e vuoi contribuire alla biblioteca, i PR sono i benvenuti.
  • In precedenza, le limitazioni multipiattaforma sono state menzionate. 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 il porting di dispositivi HID o seriali, puoi seguire l'esempio di libusb e eseguire il porting di un'altra libreria in un'altra API Fugu. Ad esempio, potresti eseguire il porting di una libreria C hidapi in WebHID e aggirare 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.

La portabilità di 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 addirittura intere applicazioni. In questo modo, le esperienze che in precedenza erano limitate agli utenti di una o due piattaforme sono ora disponibili per tutti i tipi di dispositivi e sistemi operativi, a un solo clic di distanza.

Nel prossimo post illustrerò i passaggi per creare la demo web di gPhoto2, che non solo recupera le informazioni del dispositivo, ma utilizza ampiamente anche la funzionalità di trasferimento di libusb. Nel frattempo, spero che l'esempio di libusb ti abbia ispirato e che tu provi la demo, giochi con la libreria stessa o addirittura porti un'altra libreria ampiamente utilizzata a una delle API Fugu.