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 sul web le app utilizzando le API di file system con l'API File System Access, WebAssembly e Asyncify. Ora voglio continuare lo stesso argomento sull'integrazione delle API Fuugu con WebAssembly e sulla portabilità delle app sul web senza perdere funzionalità importanti.

Mostrerò come è possibile trasferire sul web le app che comunicano con dispositivi USB portando libusb, una popolare libreria USB scritta in C, in WebAssembly (tramite Emscripten), Asyncify e WebUSB.

Cominciamo dall’inizio: una demo

La cosa più importante da fare durante il trasferimento di una libreria è scegliere la demo giusta, qualcosa che mostri le funzionalità della libreria trasferita, che ti consenta di testarla in diversi modi e di essere visivamente accattivante allo stesso tempo.

L'idea che ho scelto era un telecomando DSLR. In particolare, un 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 viene eseguito tramite libusb.

I passaggi per la creazione di questa demo sono suddivisi in due parti. In questo post del blog, descriverò come ho trasferito libusb stesso e quali trucchi potrebbero essere necessari per trasferire altre librerie popolari sulle API Fugu. Nel secondo post, esamineremo in dettaglio il trasferimento e l'integrazione di gFoto2.

Alla fine, ho un'applicazione web funzionante che mostra in anteprima il feed in diretta di una fotocamera DSLR e può controllarne le impostazioni tramite USB. Ti invitiamo a dare un'occhiata al live streaming o alla demo preregistrata prima di leggere i dettagli tecnici:

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

Nota sui particolari specifici della fotocamera

Forse avrai notato che la modifica delle impostazioni richiede del 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 gFoto2 interagisce con la fotocamera specifica scelta per la demo.

Sony a6600 non consente di impostare direttamente valori quali ISO, apertura o velocità dell'otturatore, ma fornisce soltanto comandi per aumentarli o diminuirli del numero di passaggi specificato. Per complicare le cose, non restituisce nemmeno un elenco dei valori effettivamente supportati: l'elenco restituito sembra essere hardcoded in molti modelli di fotocamere Sony.

Quando imposti uno di questi valori, gFoto2 non ha altra scelta che:

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

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

Altre videocamere probabilmente avranno impostazioni diverse, API sottostanti e caratteristiche particolari. Tieni presente che gFoto2 è un progetto open source e che non è possibile realizzare test automatici o manuali di tutti i modelli di fotocamere, per cui report dettagliati sui problemi e PR sono sempre benvenuti (ma assicurati di riprodurre i problemi prima con il client gFoto2 ufficiale).

Note importanti sulla compatibilità multipiattaforma

Sfortunatamente, su Windows ai dispositivi "rinomati", 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 su 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 fotocamera DSLR tramite WebUSB, anche se questo dipende dalla tua distribuzione.

Su macOS e Android, la demo dovrebbe funzionare immediatamente. Se la stai provando su uno smartphone Android, assicurati di passare alla modalità Orizzontale perché non mi sono impegnata molto a renderla reattiva (gli PR sono benvenuti):

Telefono Android collegato a una fotocamera Canon tramite 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 per la piattaforma" del documento "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 è stata svolta in precedenza da altri utenti) e collegare altre applicazioni alla stessa, questo approccio è soggetto a errori e rende più difficile qualsiasi estensione o manutenzione. Volevo fare le cose nel modo giusto, in un modo che potesse essere potenzialmente utile a monte e unito in futuro a libusb.

Fortunatamente, il README libusb dice:

"libusb è astratta internamente in modo tale da poterla eventualmente portare ad altri sistemi operativi. Vedi il file PORTING per ulteriori informazioni."

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 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 ed è compatibile con tutte queste piattaforme.

Quello che ho dovuto fare è stato aggiungere un altro backend per il "sistema operativo" di 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 questo 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),
};

Esaminando le proprietà, possiamo vedere che lo struct include il nome del backend, un insieme delle sue funzionalità, gestori di varie operazioni USB di basso livello sotto forma di puntatori a 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 archiviare gli handle del sistema operativo per tutti questi aspetti, poiché senza handle non sappiamo a quale elemento si applica una determinata operazione. Nell'implementazione web, gli handle del sistema operativo corrisponderanno agli oggetti JavaScript WebUSB sottostanti. Il modo naturale per rappresentarli e archiviarli in Emscripten è tramite la classe emscripten::val, che è fornita come parte di Embind (il sistema di associazioni 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),
};

Archiviazione di oggetti WebUSB come handle dei dispositivi

libusb fornisce puntatori pronti all'uso nell'area allocata per i dati privati. Per lavorare con questi puntatori come istanze val, ho aggiunto piccoli helper che li costruiscono sul posto, 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 era necessario un modo per gestire le API WebUSB asincrone dove libusb si aspetta operazioni sincrone. Per farlo, potrei usare Asyncify o, più nello specifico, la sua integrazione con 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 per gestire le eccezioni JavaScript o i rifiuti 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++. L'ho fatto con 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 potrei utilizzare promise_result::await() su qualsiasi Promise restituito dalle operazioni WebUSB e ispezionare separatamente i campi error e value.

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 ha 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

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 c'è modo di enumerare tutti i dispositivi USB connessi sul web per motivi di sicurezza. ma viene suddiviso in due parti. Innanzitutto, l'applicazione web richiede dispositivi con proprietà specifiche tramite navigator.usb.requestDevice() e l'utente sceglie manualmente il dispositivo da esporre oppure rifiuta la richiesta di autorizzazione. In seguito, nell'applicazione saranno elencati 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 in una pagina), altrimenti restituisce sempre una promessa rifiutata. Le applicazioni libusb potrebbero spesso voler elencare i dispositivi connessi all'avvio dell'applicazione, quindi l'utilizzo di requestDevice() non era un'opzione.

Ho dovuto lasciare la chiamata 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 alcuni attacchi più interessanti nel codice di gestione del trasferimento di dati, ma i dettagli di implementazione sono meno importanti ai fini di questo articolo. Assicurati di controllare il codice e i commenti su GitHub, se ti interessa.

Trasferimento dei loop di eventi sul web

Un altro aspetto dello spazio 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. Di solito viene implementato tramite un ciclo infinito che esegue il "sondaggio " (prova a leggere i dati o blocca l'esecuzione fino a quando alcuni dati non sono disponibili) da un insieme di origini I/O esterne e, quando almeno una di queste risponde, lo passa 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 non è possibile eseguirne direttamente il polling. In secondo luogo, libusb utilizza le API eventfd e pipe per altri eventi, oltre che per gestire i trasferimenti sui sistemi operativi senza handle di dispositivi non elaborati, ma al momento eventfd non è supportato in Emscripten e pipe, sebbene sia supportato, al momento non è conforme alla specifica e non può attendere gli eventi.

Infine, il problema maggiore è 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 al termine delle operazioni corrispondenti. L'esecuzione di un altro loop di eventi infinito nidificato impedisce l'avanzamento costante del loop di eventi del browser, il che significa che non solo l'interfaccia utente non risponde, ma anche che il codice non riceverà mai notifiche per gli stessi eventi I/O che è in attesa. Questo di solito comporta un deadlock, ed è quello che è successo quando ho provato a usare libusb anche in una demo. La pagina si è bloccata.

Come per altri blocchi I/O, per portare questi loop di eventi sul web, gli sviluppatori devono trovare un modo per eseguirli senza bloccare il thread principale. Un modo è il refactoring dell'applicazione per gestire gli eventi di I/O in un thread separato e passare i risultati a quello principale. L'altra funzionalità consiste nell'utilizzare Asyncify per mettere in pausa il loop e attendere gli eventi in modo non bloccato.

Non volevo apportare modifiche significative a libusb o gFoto2 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(), ho utilizzato un loop come mostrato di seguito per la proof of concept iniziale:

#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. Richiama poll() per verificare se il backend ha già segnalato eventi. Se ce ne sono alcune, il loop si arresta. In caso contrario, l'implementazione di poll() da parte di Emscripten verrà restituita immediatamente con 0.
  2. Chiama il numero emscripten_sleep(0). Questa funzione utilizza Asyncify e setTimeout() in background ed è utilizzata qui per restituire il controllo al loop di eventi principale del browser. Ciò consente al browser di gestire tutte le interazioni degli utenti e gli eventi I/O, incluso WebUSB.
  3. Controlla se il timeout specificato è già scaduto e, in caso contrario, continua il loop.

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

In seguito, ho deciso di migliorarla sfruttando il sistema di eventi del browser. Esistono diversi modi in cui questa implementazione potrebbe essere ulteriormente migliorata, ma per il momento ho scelto di emettere eventi personalizzati direttamente sull'oggetto globale, senza associarli a una particolare struttura di dati libusb. Ho provveduto a farlo tramite 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, come 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 "risvegliarsi" dalla fase di sonno 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;
}

A causa della significativa riduzione dei tempi 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 f/s a più di 30 f/s in modo coerente, sufficiente per un feed live fluido.

Crea il sistema 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. Emscripten, tuttavia, produce output diverso a seconda dell'estensione richiesta. Uso 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 di 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 i parametri del linker. Sfortunatamente, una libreria non può segnalare questi flag al linker, quindi ogni applicazione che utilizza questa porta libusb dovrà aggiungere gli stessi flag linker alla configurazione della build.

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

Una volta eseguita questa operazione, 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({cookies: [] })&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 l&#39;immagine ILCE-6600 (una fotocamera Sony).

Screenshot del passaggio successivo, con DevTools ancora aperto. Dopo la selezione del dispositivo, la 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 si porta le librerie su una nuova piattaforma, arrivare a un livello in cui si produce un output valido per la prima volta è davvero entusiasmante.

Utilizzo della porta

Come indicato sopra, la porta dipende da alcune funzionalità di Emscripten che devono essere attualmente abilitate nella fase di collegamento dell'applicazione. Se desideri 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 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. Punta l'applicazione o la libreria di livello superiore per cercare il file libusb nel percorso scelto in precedenza.
  6. Aggiungi i seguenti flag agli argomenti del link dell'applicazione: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

Al momento la raccolta presenta alcune limitazioni:

  • L'annullamento del trasferimento non è supportato. Questo è un limite di WebUSB, che, a sua volta, deriva dalla mancanza di cancellazione del trasferimento multipiattaforma in libusb stesso.
  • Nessun supporto per il trasferimento sincronizzato. 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 non supportata. Se possiedi dispositivi di questo tipo e vuoi contribuire alla raccolta, i PR sono i benvenuti.
  • Le limitazioni multipiattaforma menzionate in precedenza. Queste limitazioni sono imposte dai sistemi operativi, quindi non possiamo fare molto in questo caso, se non chiedere agli utenti di ignorare il conducente o le autorizzazioni. Tuttavia, se esegui il trasferimento di dispositivi HID o seriali, puoi seguire l'esempio libusb e trasferire un'altra libreria a un'altra API Fugu. Ad esempio, puoi trasferire una libreria C hidapi su 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 portate 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 librerie di livello superiore o persino intere applicazioni sul web. In questo modo, le esperienze prima erano limitate agli utenti di una o due piattaforme e a tutti i tipi di dispositivi e sistemi operativi, rendendole disponibili con un semplice clic su un link.

Nel prossimo post analizzerò i passaggi per la creazione della demo di gFoto2 per il web, che non solo recupera le informazioni del dispositivo, ma utilizza in modo esteso anche la funzionalità di trasferimento di libusb. Nel frattempo, spero che tu abbia trovato stimolante l'esempio di libusb e che proverai la demo, giocherai con la libreria stessa o magari anche tu vada avanti e porti anche un'altra libreria ampiamente utilizzata in una delle API Fugu.