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 realizar la portabilidad de apps a la Web con APIs del sistema de archivos con la API de File System Access, WebAssembly y Asyncify. Ahora quiero continuar con el mismo tema de integrar las APIs de Fugu con WebAssembly y portar apps a la Web sin perder funciones importantes.

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

Primero lo primero: una demostración

Lo más importante que se debe hacer al portar una biblioteca es elegir la demostración correcta, algo que muestre las capacidades de la biblioteca trasladada, permitirte probarla de varias maneras y ser visualmente convincente al mismo tiempo.

La idea que elegí fue el control remoto de DSLR. En particular, el proyecto de código abierto gPhoto2 ha estado en este espacio el tiempo suficiente como para aplicar ingeniería inversa e implementar la 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 realiza a través de libusb.

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

Al final, obtuve una aplicación web que funciona y que muestra una vista previa del feed en vivo de una DSLR y puede controlar su configuración a través de USB. No dudes en consultar la demostración en vivo o pregrabada antes de leer los detalles técnicos:

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

Nota sobre las peculiaridades específicas de la cámara

Es posible que hayas notado que cambiar la configuración lleva un tiempo en el video. Al igual que con la mayoría de los otros problemas que podrías ver, esto no se debe al rendimiento de WebAssembly o WebUSB, sino a 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, la apertura o la velocidad del obturador directamente; en su lugar, solo proporciona comandos para aumentarlos o disminuirlos en la cantidad de pasos especificada. Para complicar aún más las cosas, tampoco muestra una lista de los valores admitidos en realidad; la lista que se muestra parece estar codificada en muchos modelos de cámaras Sony.

Cuando se establece uno de esos valores, gPhoto2 no tiene otra opción que hacer lo siguiente:

  1. Da un paso (o varios) en la dirección del valor elegido.
  2. Espera a que la cámara actualice la configuración.
  3. Vuelve a leer el valor en el que se detuvo la cámara.
  4. Verifica que el último paso no haya omitido el valor deseado ni se haya unido 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á a él y, si no, se detendrá en el valor compatible más cercano.

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

Notas importantes sobre la compatibilidad multiplataforma

Lamentablemente, en Windows, a cualquier dispositivo “conocido”, incluidas las cámaras réflex digitales, se le 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 DSLR conectada a WinUSB o libusb. Este enfoque funciona bien para mí y muchos otros usuarios, pero debes usarlo bajo tu propia responsabilidad.

En Linux, es probable que debas configurar permisos personalizados para permitir el acceso a tu DSLR a través de WebUSB, aunque esto depende de tu 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 tenga capacidad de respuesta (los RR.PP. son bienvenidos):

Teléfono Android conectado a una cámara Canon mediante un cable USB-C.
La misma demostración 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”.

Cómo agregar un backend nuevo a libusb

Ahora, veamos los detalles técnicos. Si bien es posible proporcionar una API de shim similar a libusb (otros lo hicieron antes) y vincular otras aplicaciones con ella, este enfoque es propenso a errores y dificulta cualquier extensión o mantenimiento adicional. Quería hacer las cosas bien, de una manera que pudiera contribuir en upstream y fusionarse en libusb en el futuro.

Por suerte, el archivo README de libusb dice lo siguiente:

“libusb se abstrae de forma interna de manera tal que, con suerte, se pueda portar a otros sistemas operativos. Consulta el archivo PORTING para obtener más información”.

libusb está estructurado de manera tal que la API pública está separada de los "backends". Esos backends son responsables de enumerar, abrir, cerrar y comunicarse con los dispositivos a través de las APIs de bajo nivel del sistema operativo. De esta manera, libusb ya 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 de 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 ayudantes 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 observamos las propiedades, podemos ver que la estructura 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, los tamaños que se asignarán para almacenar datos privados a nivel del dispositivo, del contexto o de la transferencia.

Los campos de datos privados son útiles, al menos, para almacenar controladores del SO para todos esos elementos, ya que, sin controladores, 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 representarlos y almacenarlos en Emscripten es a través de 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 se hizo por mí 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),
};

Almacenamiento de 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 ayudantes que los construyen en su lugar, los recuperan como referencias y mueven los valores:

// 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 se necesita 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 forma de controlar las excepciones de JavaScript ni los rechazos de Promise desde C++. Para resolver este problema, captura un rechazo en el lado de JavaScript y 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 devuelva de las operaciones de WebUSB y, además, 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 de 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 poder abrir cualquier dispositivo, libusb debe 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 motivos de seguridad. sino que 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 enumera 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 un mensaje de permiso con una lista de dispositivos conectados se considera una operación sensible y debe activarse mediante la interacción del usuario (como hacer clic en un botón de una página). De lo contrario, siempre se muestra una promesa rechazada. A menudo, las aplicaciones de libusb pueden querer mostrar una lista de los dispositivos conectados cuando se inicia la aplicación, por lo que usar requestDevice() no era una opción.

En su lugar, tuve que dejar la invocación de navigator.usb.requestDevice() al desarrollador final y solo exponer los dispositivos ya aprobados desde 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 una manera similar a la que ya 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. Si te interesa, asegúrate de revisar el código y los comentarios en GitHub.

Cómo portar bucles de eventos a la Web

Otro elemento del puerto libusb que quiero analizar es el manejo de eventos. Como se describió en el artículo anterior, la mayoría de las APIs en lenguajes de sistema como C son síncronos, y el control de eventos no es una excepción. Por lo general, se implementa a través de un bucle infinito que "sondea" (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 ellas responde, lo pasa como un evento al controlador correspondiente. Una vez que finaliza el controlador, el control regresa al bucle y se detiene para realizar otro sondeo.

Este enfoque tiene algunos problemas en la Web.

Primero, 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 transferencias en sistemas operativos sin controladores de dispositivos sin procesar, pero, por el momento, eventfd no es compatible con Emscripten, y pipe, si bien es compatible, por el momento no cumple con las especificaciones y no puede esperar a los eventos.

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, además, invoca controladores de eventos o Promise cada vez que terminan las operaciones correspondientes. Ejecutar otro bucle de eventos infinito anidado impedirá que el bucle de eventos del navegador progrese, lo que significa que no solo la IU no responderá, sino que el código nunca recibirá notificaciones de los mismos eventos de E/S que está esperando. Por lo general, esto genera un interbloqueo, y eso es lo que sucedió cuando intenté usar libusb en una demostración. La página se bloqueó.

Al igual que con otras operaciones de E/S de bloqueo, para portar esos 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 independiente y pasar los resultados al principal. La otra es usar Asyncify para pausar el bucle y esperar eventos de forma no bloqueante.

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

#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 ya 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() en segundo plano y se usa aquí para devolver el control al bucle de eventos principal del navegador. Esto permite que el navegador controle cualquier interacción del usuario y eventos de E/S, incluido WebUSB.
  3. Verifica si el tiempo de espera especificado ya venció y, de no ser así, continúa con 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 (que es la mayoría de las veces) y porque setTimeout() tiene una duración mínima de 4 ms en navegadores modernos. Aun así, funcionó lo suficientemente bien como para producir una transmisión 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. Hay varias formas en las que se podría mejorar esta implementación, pero por ahora decidí emitir eventos personalizados directamente en el objeto global, sin asociarlos con una estructura de datos de libusb en 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 "despertar" del estado de suspensión de Asyncify cuando se recibe un evento em-libusb o cuando vence 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 una reducción significativa en las activaciones y desactivaciones, este mecanismo corrigió los problemas de eficiencia de la implementación anterior basada en emscripten_sleep() y aumentó la capacidad de procesamiento de la demo de la DSLR de 13 a 14 FPS a más de 30 FPS coherentes, lo que es suficiente para un feed en vivo fluido.

Sistema de compilación y primera prueba

Después de terminar el backend, tuve que agregarlo a Makefile.am y configure.ac. La única parte interesante aquí es la modificación de las 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']"
  ;;

En primer lugar, los ejecutables en plataformas Unix no suelen tener extensiones de archivo. Sin embargo, Emscripten produce resultados diferentes según la extensión que solicites. Uso AC_SUBST(EXEEXT, …) para cambiar la extensión ejecutable a .html, de modo que cualquier ejecutable dentro de un paquete (pruebas y ejemplos) se convierta en un 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 permitir el crecimiento de memoria dinámica (-s ALLOW_MEMORY_GROWTH) mediante parámetros del vinculador. Lamentablemente, no hay forma de que una biblioteca informe esas marcas al vinculador, por lo que cada aplicación que use este puerto libusb también deberá agregar las mismas marcas de vinculador a su configuración de compilación.

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

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

Captura de pantalla que muestra una ventana de Chrome con DevTools abierta en una página &quot;testlibusb&quot; que se entrega de forma local. La consola de DevTools evalúa &quot;navigator.usb.requestDevice({ filters: [] })&quot;, que activó un mensaje de permiso y, actualmente, le solicita al usuario que elija un dispositivo USB que se compartirá con la página. Actualmente, se seleccionó ILCE-6600 (una cámara Sony).

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

No parece mucho, pero, cuando se portan bibliotecas a una plataforma nueva, llegar a la etapa en la que se 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 estar habilitadas en la etapa de vinculación de la aplicación. Si quieres usar este puerto libusb en tu propia aplicación, esto es lo que debes hacer:

  1. Descarga la versión más reciente de libusb como un archivo como parte de tu compilación o agrégala 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 para la compilación cruzada y establecer una ruta de acceso en la que deseas colocar los artefactos compilados.
  4. Ejecuta emmake make install.
  5. Dirige tu aplicación o biblioteca de nivel superior para que busque libusb en la ruta de acceso elegida anteriormente.
  6. Agrega las siguientes marcas a los argumentos de vinculación de tu aplicación: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

Actualmente, la biblioteca tiene algunas limitaciones:

  • No se admite la cancelación de transferencias. Esta es una limitación de WebUSB, que, a su vez, se debe a la falta de cancelación de transferencia multiplataforma en libusb.
  • No se admite la transferencia isócrona. No debería ser difícil agregarlo siguiendo la implementación de los modos de transferencia existentes como ejemplos, pero también es un modo algo raro y no tenía ningún dispositivo para probarlo, así que, por ahora, lo dejé como no compatible. Si tienes esos dispositivos y quieres contribuir a la biblioteca, aceptamos las PR.
  • Las limitaciones multiplataforma mencionadas anteriormente Los sistemas operativos imponen esas limitaciones, por lo que no podemos hacer mucho, excepto pedirles a los usuarios que anulen el controlador o los permisos. Sin embargo, si portas dispositivos HID o en serie, puedes seguir el ejemplo de libusb y portar alguna otra biblioteca a otra API de Fugu. Por ejemplo, puedes portar una biblioteca C hidapi a WebHID y evitar por completo esos problemas asociados con el acceso USB de bajo nivel.

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 portar a la Web con algunos trucos de integración.

Transferir estas bibliotecas de bajo nivel esenciales y ampliamente usadas es particularmente gratificante porque, a su vez, permite llevar bibliotecas de nivel superior o, incluso, aplicaciones completas a la Web. Esto abre experiencias que antes estaban limitadas a los usuarios de una o dos plataformas, a todo tipo de dispositivos y sistemas operativos, lo que permite que esas experiencias estén disponibles con solo un clic en el vínculo.

En la próxima publicación, explicaré los pasos necesarios para compilar la demostración web de gPhoto2, que no solo recupera información del dispositivo, sino que también usa ampliamente la función de transferencia de libusb. Mientras tanto, espero que el ejemplo de libusb te haya inspirado y que pruebes la demostración, juegues con la biblioteca o, incluso, que portes otra biblioteca muy utilizada a una de las APIs de Fugu.