Portabilidad de aplicaciones USB a la Web Parte 1: libusb

Descubre cómo el código que interactúa con dispositivos externos se puede transferir a la Web con las APIs de WebAssembly y Fugu.

En una publicación anterior, mostré cómo portar apps con APIs del sistema de archivos a la Web con la API de Acceso al sistema de archivos, WebAssembly y Asyncify. Ahora quiero continuar con el mismo tema sobre la integración de las APIs de Fugu con WebAssembly y la portabilidad de apps a la Web sin perder funciones importantes.

Te mostraré cómo las apps que se comunican con dispositivos USB pueden transferirse a la Web con la portabilidad de libusb, una biblioteca USB popular escrita en C, a WebAssembly (a través de Emscripten), Asyncify y WebUSB.

Lo primero es lo primero: una demostración

Lo más importante que se debe hacer cuando se realiza la portabilidad de una biblioteca es elegir la demostración adecuada, algo que muestre las capacidades de la biblioteca transferida, lo que te permite probarla de varias maneras y, al mismo tiempo, ser visualmente atractiva.

La idea que elegí fue un control remoto DSLR. En particular, un proyecto de código abierto gPhoto2 ha estado en este espacio el tiempo suficiente para aplicar ingeniería inversa e implementar compatibilidad con una amplia variedad de cámaras digitales. Admite varios protocolos, pero el que más me interesaba era la compatibilidad con USB, que se realiza a través de libusb.

Describiré los pasos para crear esta demostración en dos partes. En esta entrada de blog, describiré cómo porté libusb en sí y qué trucos podrían ser necesarios para portar otras bibliotecas populares a las APIs de Fugu. En la segunda publicación, explicaré los detalles sobre la portabilidad y la integración de gPhoto2.

Al final, obtuve una aplicación web en funcionamiento que ofrece una vista previa del feed en vivo desde una cámara réflex digital y puede controlar su configuración mediante USB. No dudes en mirar la demostración en vivo o la pregrabada antes de leer los detalles técnicos:

La demostración que se ejecuta en una laptop conectada a una cámara Sony.

Nota sobre las peculiaridades de la cámara

Tal vez hayas notado que los cambios de configuración tardan un tiempo en completarse. Al igual que con la mayoría de los otros problemas que puedes ver, esto no se debe al rendimiento de WebAssembly o WebUSB, sino por la forma en que gPhoto2 interactúa con la cámara específica elegida para la demostración.

Sony A6600 no expone una API para establecer valores como ISO, apertura o velocidad del obturador de forma directa, sino que solo proporciona comandos para aumentarlos o disminuirlos al número de pasos especificado. Para complicar el asunto, tampoco muestra una lista de los valores admitidos; la lista que se muestra parece codificada en muchos modelos de cámaras Sony.

Cuando estableces uno de esos valores, gPhoto2 no tiene otra opción más que:

  1. Realiza un paso (o varios) en la dirección del valor elegido.
  2. Espera un momento para que la cámara actualice la configuración.
  3. Vuelve a leer el valor al que llegó la cámara.
  4. Comprueba que el último paso no haya saltado el valor deseado ni que esté ajustado al final o al principio de la lista.
  5. Y todo de nuevo.

Esto puede tardar un poco, pero si la cámara admite el valor, llegará allí y, de no ser así, se detendrá en el valor admitido más cercano.

Es probable que otras cámaras tengan diferentes conjuntos de configuración, APIs subyacentes y peculiaridades. Ten en cuenta que gPhoto2 es un proyecto de código abierto y que simplemente no es posible realizar pruebas automáticas o manuales de todos los modelos de cámaras que existen, por lo que siempre se aceptan informes de problemas detallados y PR (pero asegúrate de reproducir primero los problemas con el cliente oficial de gPhoto2).

Notas importantes sobre la compatibilidad multiplataforma

Lamentablemente, en Windows, a todos los dispositivos "conocidos", incluidas las cámaras DSLR, se les asigna un controlador del sistema que no es compatible con WebUSB. Si quieres probar la demostración en Windows, deberás usar una herramienta como Zadig para anular el controlador de la cámara réflex digital conectada a WinUSB o libusb. Este enfoque funciona bien para mí y para muchos otros usuarios, pero debes usarlo bajo tu propia responsabilidad.

En Linux, es probable que debas configurar permisos personalizados para permitir el acceso a la cámara DSLR a través de WebUSB, aunque esto depende de la distribución.

En macOS y Android, la demostración debería funcionar de inmediato. Si la estás probando en un teléfono Android, asegúrate de cambiar al modo horizontal, ya que no me esforcé mucho para que sea responsiva (se aceptan los comunicados de prensa):

Teléfono Android conectado a una cámara Canon mediante un cable USB-C
La misma demostración que se ejecuta en un teléfono Android. Foto de Surma.

Para obtener una guía más detallada sobre el uso multiplataforma de WebUSB, consulta la sección"Consideraciones específicas de la plataforma" de "Cómo compilar un dispositivo para WebUSB".

Agrega un backend nuevo a libusb

Ahora, pasemos a los detalles técnicos. Si bien es posible proporcionar una API de corrección de compatibilidad similar a libusb (esto ya lo hicieron otras personas) y vincular otras aplicaciones en su contra, este enfoque es propenso a errores y dificulta cualquier extensión o mantenimiento adicional. Quería hacer las cosas bien, de manera que se pudieran contribuir de nuevo en sentido ascendente y combinarse en libusb en el futuro.

Afortunadamente, el archivo libusb README dice lo siguiente:

"libusb se abstrae internamente para que pueda trasladarse a otros sistemas operativos. Consulta el archivo PORTING para obtener más información”.

libusb se estructura de modo que la API pública esté separada de los "backends". Esos backends son responsables de crear listas, abrirse, cerrarse y comunicarse realmente con los dispositivos a través de las APIs de bajo nivel del sistema operativo. Así es como libusb abstrae las diferencias entre Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku y Solaris, y funciona en todas estas plataformas.

Lo que tuve que hacer fue agregar otro backend para el "sistema operativo" Emscripten+WebUSB. Las implementaciones para esos backends se encuentran en la carpeta 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

Cada backend incluye el encabezado libusbi.h con tipos y asistentes comunes, y debe exponer una variable usbi_backend de tipo usbi_os_backend. Por ejemplo, así se ve el backend de 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),
};

Si revisamos las propiedades, podemos ver que el struct incluye el nombre del backend, un conjunto de sus capacidades, controladores para varias operaciones USB de bajo nivel en forma de punteros de función y, por último, tamaños para almacenar datos privados a nivel del dispositivo/contexto/transferencia.

Los campos de datos privados son útiles, al menos, para almacenar controladores de SO para todo eso, ya que sin ellos no sabemos a qué elemento se aplica una operación determinada. En la implementación web, los controladores del SO serían los objetos de JavaScript de WebUSB subyacentes. La forma natural de representarlas y almacenarlas en Emscripten es mediante la clase emscripten::val, que se proporciona como parte de Embind (el sistema de vinculaciones de Emscripten).

La mayoría de los backends de la carpeta se implementan en C, pero algunos se implementan en C++. Embind solo funciona con C++, por lo que la elección es mía y agregué libusb/libusb/os/emscripten_webusb.cpp con la estructura requerida y con sizeof(val) para los campos de datos privados:

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

Almacenar objetos WebUSB como controladores de dispositivos

libusb proporciona punteros listos para usar al área asignada para datos privados. Para trabajar con esos punteros como instancias de val, agregué pequeños asistentes que los crean en el lugar, los recuperan como referencias y mueven los valores hacia afuera:

// 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))) {}
};

APIs web asíncronas en contextos de C síncronos

Ahora necesitaba una forma de controlar las APIs de WebUSB asíncronas en las que libusb espera operaciones síncronas. Para ello, podría usar Asyncify o, más específicamente, su integración de Embind a través de val::await().

También quería manejar correctamente los errores de WebUSB y convertirlos en códigos de error libusb, pero, por el momento, Embind no tiene ninguna manera de controlar las excepciones de JavaScript ni los rechazos de Promise desde el lado de C++. Este problema se puede solucionar si se detecta un rechazo en JavaScript y se convierte el resultado en un objeto { error, value } que ahora se puede analizar de forma segura desde el lado de C++. Lo hice con una combinación de la macro EM_JS y las APIs de 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()};
  }
};

Ahora puedo usar promise_result::await() en cualquier Promise que se muestre mediante operaciones de WebUSB e inspeccionar sus campos error y value por separado.

Por ejemplo, recuperar un val que representa un USBDevice de libusb_device_handle, llamar a su método open(), esperar su resultado y mostrar un código de error como un código de estado libusb se ve de la siguiente manera:

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

Enumeración de dispositivos

Por supuesto, antes de que pueda abrir cualquier dispositivo, libusb necesita recuperar una lista de los dispositivos disponibles. El backend debe implementar esta operación a través de un controlador get_device_list.

La dificultad es que, a diferencia de otras plataformas, no hay forma de enumerar todos los dispositivos USB conectados en la Web por razones de seguridad. En cambio, el flujo se divide en dos partes. Primero, la aplicación web solicita dispositivos con propiedades específicas a través de navigator.usb.requestDevice() y el usuario elige manualmente qué dispositivo quiere exponer o rechaza la solicitud de permiso. Luego, la aplicación mostrará los dispositivos ya aprobados y conectados a través de navigator.usb.getDevices().

Al principio, intenté usar requestDevice() directamente en la implementación del controlador get_device_list. Sin embargo, mostrar una solicitud de permiso con una lista de dispositivos conectados se considera una operación sensible y debe activarse por la interacción del usuario (como un clic en un botón en una página); de lo contrario, siempre muestra una promesa rechazada. Las aplicaciones libusb a menudo posiblemente quieran enumerar los dispositivos conectados cuando se inicia la aplicación, por lo que usar requestDevice() no era una opción.

En cambio, tuve que dejar la invocación de navigator.usb.requestDevice() al desarrollador final y solo exponer los dispositivos ya aprobados de navigator.usb.getDevices():

// Store the global `navigator.usb` once upon initialisation.
thread_local const val web_usb = val::global("navigator")["usb"];

int em_get_device_list(libusb_context *ctx, discovered_devs **devs) {
  // C++ equivalent of `await navigator.usb.getDevices()`.
  // Note: at this point we must already have some devices exposed -
  // caller must have called `await navigator.usb.requestDevice(...)`
  // in response to user interaction before going to LibUSB.
  // Otherwise this list will be empty.
  auto result = promise_result::await(web_usb.call<val>("getDevices"));
  if (result.error) {
    return result.error;
  }
  auto &web_usb_devices = result.value;
  // Iterate over the exposed devices.
  uint8_t devices_num = web_usb_devices["length"].as<uint8_t>();
  for (uint8_t i = 0; i < devices_num; i++) {
    auto web_usb_device = web_usb_devices[i];
    // …
    *devs = discovered_devs_append(*devs, dev);
  }
  return LIBUSB_SUCCESS;
}

La mayor parte del código de backend usa val y promise_result de manera similar, como se mostró anteriormente. Hay algunos hackeos más interesantes en el código de manejo de transferencias de datos, pero esos detalles de implementación son menos importantes para los fines de este artículo. Asegúrate de revisar el código y los comentarios en GitHub si estás interesado.

Cómo migrar bucles de eventos a la Web

Un aspecto más del puerto libusb que quiero analizar es el manejo de eventos. Como se describe en el artículo anterior, la mayoría de las APIs en lenguajes del sistema como C son síncronas, y la administración de eventos no es una excepción. Por lo general, se implementa a través de un bucle infinito que realiza un “sondeo” (intenta leer datos o bloquea la ejecución hasta que algunos datos estén disponibles) desde un conjunto de fuentes de E/S externas y, cuando al menos una de esas responde, lo pasa como un evento al controlador correspondiente. Una vez que finaliza el controlador, el control vuelve al bucle y se pausa para otro sondeo.

En la Web, existen algunos problemas con este enfoque.

En primer lugar, WebUSB no expone ni puede exponer los controladores sin procesar de los dispositivos subyacentes, por lo que sondearlos directamente no es una opción. En segundo lugar, libusb usa las APIs de eventfd y pipe para otros eventos, así como para controlar las transferencias en sistemas operativos sin controladores de dispositivos sin procesar, pero, por el momento, eventfd no es compatible con Emscripten y, si bien es compatible, por el momento no cumple con las especificaciones y no puede esperar eventos.pipe

Por último, el mayor problema es que la Web tiene su propio bucle de eventos. Este bucle de eventos global se usa para cualquier operación de E/S externa (incluidos fetch(), temporizadores o, en este caso, WebUSB) y invoca el evento o los controladores de Promise cuando finalizan las operaciones correspondientes. Ejecutar otro bucle de eventos infinito anidado y anidado bloqueará el bucle de eventos del navegador para que no progrese, lo que significa que no solo la IU dejará de responder, sino también que el código nunca recibirá notificaciones de los mismos eventos de E/S que está esperando. Esto suele generar un interbloqueo, y eso es lo que sucedió cuando intenté usar libusb en una demostración. La página se tildó.

Al igual que con otras E/S de bloqueo, para portar estos bucles de eventos a la Web, los desarrolladores deben encontrar una manera de ejecutar esos bucles sin bloquear el subproceso principal. Una forma es refactorizar la aplicación para controlar los eventos de E/S en un subproceso separado y pasar los resultados al principal. La otra es usar Asyncify para pausar el bucle y esperar eventos sin bloqueo.

No quería realizar cambios significativos en libusb ni gPhoto2, y ya usé Asyncify para la integración de Promise, así que elegí esa es la ruta de acceso que elegí. Con el objetivo de simular una variante de bloqueo de poll(), usé un bucle como se muestra a continuación para la prueba de concepto inicial:

#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

Lo que hace es:

  1. Llama a poll() para verificar si el backend aún informó algún evento. Si hay alguno, el bucle se detiene. De lo contrario, la implementación de poll() de Emscripten se mostrará de inmediato con 0.
  2. Llamadas emscripten_sleep(0). Esta función usa Asyncify y setTimeout() de forma interna, y se usa aquí para regresar el control al bucle de eventos principal del navegador. De esta manera, el navegador puede controlar cualquier interacción del usuario y evento de E/S, incluido WebUSB.
  3. Verifica si ya venció el tiempo de espera especificado y, de no ser así, continúa el bucle.

Como se menciona en el comentario, este enfoque no era óptimo, ya que seguía guardando y restableciendo toda la pila de llamadas con Asyncify incluso cuando aún no había eventos USB para controlar (lo que suele suceder) y porque setTimeout() en sí tiene una duración mínima de 4 ms en los navegadores modernos. Aun así, funcionó lo suficientemente bien como para producir transmisiones en vivo de 13 a 14 FPS desde una cámara réflex digital en la prueba de concepto.

Más tarde, decidí mejorarlo aprovechando el sistema de eventos del navegador. Existen varias maneras de mejorar esta implementación, pero por ahora decidí emitir eventos personalizados directamente en el objeto global, sin asociarlos a una estructura de datos libusb particular. Lo hice a través del siguiente mecanismo de espera y notificación basado en la macro EM_ASYNC_JS:

EM_JS(void, em_libusb_notify, (void), {
  dispatchEvent(new Event("em-libusb"));
});

EM_ASYNC_JS(int, em_libusb_wait, (int timeout), {
  let onEvent, timeoutId;

  try {
    return await new Promise(resolve => {
      onEvent = () => resolve(0);
      addEventListener('em-libusb', onEvent);

      timeoutId = setTimeout(resolve, timeout, -1);
    });
  } finally {
    removeEventListener('em-libusb', onEvent);
    clearTimeout(timeoutId);
  }
});

La función em_libusb_notify() se usa cada vez que libusb intenta informar un evento, como la finalización de la transferencia de datos:

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
}

Mientras tanto, la parte em_libusb_wait() se usa para "activar" desde la suspensión de Asyncify cuando se recibe un evento em-libusb o se agota el tiempo de espera:

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

Debido a la reducción significativa de las suspensiones y las activaciones, este mecanismo solucionó los problemas de eficiencia de la implementación anterior basada en emscripten_sleep() y aumentó la capacidad de procesamiento de la demostración de la cámara réflex digital de 13 a 14 FPS a más de 30 FPS constantes, lo que es suficiente para brindar un feed en vivo fluido.

Sistema de compilación y primera prueba

Una vez que el backend estaba listo, tuve que agregarlo a Makefile.am y configure.ac. Lo único interesante aquí es la modificación de marcas específicas de 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']"
  ;;

Primero, los ejecutables en plataformas Unix normalmente no tienen extensiones de archivo. Sin embargo, Emscripten produce resultados diferentes según la extensión que solicites. Estoy usando AC_SUBST(EXEEXT, …) para cambiar la extensión ejecutable a .html, de modo que cualquier elemento ejecutable dentro de un paquete (pruebas y ejemplos) se convierta en un código HTML con la shell predeterminada de Emscripten que se encarga de cargar y crear instancias de JavaScript y WebAssembly.

En segundo lugar, como estoy usando Embind y Asyncify, necesito habilitar esas funciones (--bind -s ASYNCIFY) y el crecimiento dinámico de la memoria (-s ALLOW_MEMORY_GROWTH) a través de los parámetros del vinculador. Lamentablemente, no hay forma de que una biblioteca informe esos indicadores al vinculador, por lo que cada aplicación que use este puerto libusb deberá agregar los mismos indicadores del vinculador a la configuración de compilación.

Por último, como se mencionó anteriormente, WebUSB requiere que la enumeración de dispositivos se realice con un gesto del usuario. Los ejemplos y las pruebas de libusb suponen que pueden enumerar dispositivos en el inicio y fallar con un error sin cambios. En su lugar, tuve que inhabilitar la ejecución automática (-s INVOKE_RUN=0) y exponer el método manual callMain() (-s EXPORTED_RUNTIME_METHODS=...).

Una vez que todo esto se hizo, pude entregar los archivos generados con un servidor web estático, inicializar WebUSB y ejecutar esos ejecutables HTML de forma manual con la ayuda de Herramientas para desarrolladores.

Captura de pantalla que muestra una ventana de Chrome con Herramientas para desarrolladores abiertas en una página `testlibusb` proporcionada de forma local. La consola de Herramientas para desarrolladores está evaluando &quot;navigator.usb.requestDevice({ filtros: [] })&quot;, que activó un mensaje de permiso y le pide al usuario que elija un dispositivo USB que debería compartirse con la página. Actualmente, está seleccionado ILCE-6600 (una cámara Sony).

Captura de pantalla del paso siguiente con Herramientas para desarrolladores aún abiertas. Después de seleccionar el dispositivo, la consola evaluó la nueva expresión `Module.callMain([&#39;-v&#39;])`, que ejecutó la app `testlibusb` en modo detallado. El resultado muestra varias información detallada sobre la cámara USB conectada anteriormente: fabricante Sony, producto ILCE-6600, número de serie, configuración, etc.

No parece gran cosa, pero, cuando se portan bibliotecas a una plataforma nueva, llegar a la etapa en que produce un resultado válido por primera vez es bastante emocionante.

Cómo usar el puerto

Como se mencionó anteriormente, el puerto depende de algunas funciones de Emscripten que actualmente deben habilitarse en la etapa de vinculación de la aplicación. Deberás hacer lo siguiente si quieres usar este puerto libusb en tu propia aplicación:

  1. Descarga el libusb más reciente como un archivo como parte de tu compilación o agrégalo como un submódulo de Git en tu proyecto.
  2. Ejecuta autoreconf -fiv en la carpeta libusb.
  3. Ejecuta emconfigure ./configure –host=wasm32 –prefix=/some/installation/path para inicializar el proyecto con compilación cruzada y establecer una ruta de acceso en la que desees colocar los artefactos compilados.
  4. Ejecuta emmake make install.
  5. Apunta tu aplicación o biblioteca de nivel superior para buscar el libusb en la ruta elegida anteriormente.
  6. Agrega las siguientes marcas a los argumentos de vínculo de tu aplicación: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

Actualmente, la biblioteca tiene algunas limitaciones:

  • No se admiten transferencias para la cancelación de transferencias. Esta es una limitación de WebUSB que, a su vez, surge de la falta de cancelación de transferencias multiplataforma en libusb.
  • No hay compatibilidad con transferencias isocrónicas. No debería ser difícil agregarlo siguiendo la implementación de modos de transferencia existentes como ejemplos, pero también es un modo un tanto raro y no tenía ningún dispositivo para probarlo, así que por ahora lo dejé como no compatible. Si ya tienes esos dispositivos y quieres contribuir a la biblioteca, ¡las RR. PP. son bienvenidos!
  • Las limitaciones multiplataforma mencionadas anteriormente. Esas limitaciones son impuestas por los sistemas operativos, por lo que no podemos hacer mucho en este caso, excepto pedir a los usuarios que anulen al conductor o a los permisos. Sin embargo, si realizas la portabilidad de dispositivos HID o en serie, puedes seguir el ejemplo de libusb y portar alguna otra biblioteca a otra API de Fugu. Por ejemplo, podrías transferir hidapi de una biblioteca C a WebHID y omitir esos problemas, asociados con el acceso USB de bajo nivel, por completo.

Conclusión

En esta publicación, mostré cómo, con la ayuda de las APIs de Emscripten, Asyncify y Fugu, incluso las bibliotecas de bajo nivel como libusb se pueden transferir a la Web con algunos trucos de integración.

La portabilidad de bibliotecas de bajo nivel tan esenciales y tan usadas es particularmente gratificante porque, a su vez, permite traer bibliotecas de nivel superior o, incluso, aplicaciones completas a la Web. Esto abre experiencias que antes se limitaban a usuarios de una o dos plataformas, a todo tipo de dispositivos y sistemas operativos, lo que permite que esas experiencias estén disponibles con un solo clic de distancia.

En la siguiente publicación, explicaré los pasos para crear la demostración de gPhoto2 web que no solo recupera información del dispositivo, sino que también utiliza ampliamente la función de transferencia de libusb. Mientras tanto, espero que el ejemplo de libusb te haya resultado inspirador. Prueba la demostración, juega con la biblioteca en sí o, incluso, transfiere otra biblioteca muy usada a una de las APIs de Fugu.