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 sobre la integración de las APIs de Fugu con WebAssembly y la portabilidad de 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 un control remoto DSLR. En particular, el proyecto de código abierto gPhoto2 ha estado en este espacio el tiempo suficiente como para aplicar ingeniería inversa y aplicar la compatibilidad con una amplia variedad de cámaras digitales. Admite varios protocolos, pero el que más me interesaba es la compatibilidad con USB, que 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é la portabilidad de libusb y los trucos que podrían ser necesarios para la portabilidad de otras bibliotecas populares a las APIs de Fugu. En la segunda publicación, entraré en detalles sobre la portabilidad y la integración de gPhoto2.

Finalmente, obtuve una aplicación web que funciona y que muestra vistas previas del feed en vivo de una cámara réflex digital y puede controlar su configuración mediante USB. No dudes en mirar la demostración en vivo o pregrabada antes de leer detalles técnicos:

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

Nota sobre las peculiaridades específicas de las cámaras

Quizás hayas notado que cambiar la configuración tarda un poco en el video. Al igual que con la mayoría de los problemas que puedes 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 el proceso, tampoco muestra una lista de los valores realmente 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 que realizar las siguientes acciones:

  1. Realiza algunos pasos (o varios) en la dirección del valor elegido.
  2. Espera un poco a que la cámara actualice la configuración.
  3. Lee el valor en el que realmente aterrizó la cámara.
  4. Comprueba que el último paso no saltó sobre el valor deseado ni se ajustó al final o al principio de la lista.
  5. Y todo de nuevo.

Puede tomar un tiempo, pero si el valor es realmente compatible con la cámara, llegará allí; de lo contrario, se detendrá en el valor admitido más cercano.

Es probable que otras cámaras tengan diferentes conjuntos de configuraciones, APIs subyacentes y peculiaridades. Ten en cuenta que gPhoto2 es un proyecto de código abierto. No es posible 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

Por desgracia, en Windows, cualquier instancia A los dispositivos, incluidas las cámaras DSLR, se les asigna un controlador de 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 DSLR conectada a WinUSB o libusb. Este enfoque funciona bien para mí y para muchos otros usuarios, pero deberías usarlo bajo tu propio riesgo.

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 tenga capacidad de respuesta (los RR.PP. son bienvenidos):

Teléfono Android conectado a una cámara Canon a través de un cable USB-C
La misma demostración que se ejecuta en un teléfono Android. Fotografía 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, veamos los detalles técnicos. Si bien es posible proporcionar una API de shim similar a libusb (ya que otras personas ya hicieron esto) y vincular otras aplicaciones con ella, este enfoque es propenso a errores y dificulta la extensión o el mantenimiento adicionales. Quería hacer las cosas bien, de una manera que pudiera volver a subirse y fusionarse con libusb en el futuro.

Por suerte, el archivo libusb README dice lo siguiente:

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

libusb está estructurado de una manera en la 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. 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í es como 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 analizamos las propiedades, podemos ver que la 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 asignar para almacenar datos privados de dispositivo, contexto y transferencia.

Los campos de datos privados son útiles al menos para almacenar los controladores del SO para todos esos elementos, 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 representarlos y almacenarlos en Emscripten es a través de la clase emscripten::val, que se proporciona como parte de Embind (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++, así que la elección fue 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 en el área asignada para datos privados. Para trabajar con esos punteros como instancias de val, agregué pequeños asistentes que los construyen in situ, los recuperan como referencias y mueven 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, puedo usar Asyncify o, más específicamente, su integración de Embind mediante 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 solucionar este problema, 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 en las operaciones de WebUSB y, luego, inspeccionar sus campos error y value por separado.

Por ejemplo, recuperar una 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 código de estado libusb tiene el siguiente aspecto:

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 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 razones 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 una solicitud de permiso con una lista de dispositivos conectados se considera una operación sensible y debe activarse cuando el usuario hace clic en un botón de una página. De lo contrario, siempre se mostrará una promesa rechazada. Es posible que las aplicaciones libusb a menudo quieran enumerar los dispositivos conectados cuando se inician, 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 exponer solo 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 una manera similar a 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 te interesa.

Cómo transferir bucles de eventos a la Web

Una parte más 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 “encuesta” (intenta leer datos o bloquea la ejecución hasta que haya algunos datos 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 eventfd no es compatible actualmente en 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 evento global se usa para cualquier operación de E/S externa (lo que incluye fetch(), cronómetros o, en este caso, WebUSB) y invoca controladores de eventos o Promise cada vez que finalizan las operaciones correspondientes. La ejecución de otro bucle de eventos infinitos, anidados bloqueará el progreso del bucle de eventos del navegador, 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. 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 tildó.

Al igual que con otras E/S de bloqueo, para transferir 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 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 una manera que no provoque bloqueos.

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í. A fin de simular una variante de bloqueo de poll(), para 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 algunas, 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 volver a controlar el bucle de eventos del navegador principal. Esto permite que el navegador controle cualquier interacción del usuario y eventos de E/S, incluido WebUSB.
  3. Verifica si ya venció el tiempo de espera especificado y, de lo contrario, continúa con el bucle.

Como se menciona en el comentario, este enfoque no era óptimo, ya que seguía restableciendo toda la pila de llamadas con Asyncify incluso cuando aún no había eventos USB para controlar (lo que es la mayor parte del tiempo) y porque setTimeout() tiene una duración mínima de 4 ms en los 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í mejorarla aprovechando el sistema de eventos del navegador. Hay varias formas en las que esta implementación podría mejorarse aún más, pero, por ahora, elegí emitir eventos personalizados directamente en el objeto global, sin asociarlos a una estructura de datos libusb particular. Lo hice con el 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" del estado de sueño 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 una reducción significativa en 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 demostración de la cámara DSLR de 13 a 14 FPS a 30 o más FPS constantes, lo que es suficiente para una transmisión en vivo fluida.

El sistema de compilación y la primera prueba

Cuando finalizó el proceso de backend, 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 archivos ejecutables en plataformas Unix normalmente no tienen extensiones de archivo. Sin embargo, Emscripten produce diferentes resultados 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 manera de que una biblioteca informe esas marcas al vinculador, por lo que cada aplicación que use este puerto libusb también deberá agregar los mismos marcadores del 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. En los ejemplos y las pruebas de libusb, se supone que pueden enumerar los dispositivos durante el inicio y fallan con un error sin realizar cambios. En su lugar, 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 Herramientas para desarrolladores abierta en una página &quot;testlibusb&quot; que se entrega localmente. La consola de Herramientas para desarrolladores está evaluando `navigator.usb.requestDevice({ filters: [] })`, que activó un mensaje de permiso y le pide al usuario que elija un dispositivo USB que deba compartirse con la página. 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, la consola evaluó una nueva expresión `Module.callMain([&#39;-v&#39;])`, que ejecutó la app `testlibusb` en modo detallado. En el resultado, se 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 transfieren bibliotecas a una nueva plataforma, es muy emocionante llegar a la etapa en que se produce un resultado válido por primera vez.

Usa 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. Si quieres usar este puerto libusb en tu propia aplicación, deberás hacer lo siguiente:

  1. Descarga el archivo libusb más reciente, ya sea 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 a fin de inicializar el proyecto para la 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 archivo 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 las siguientes 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 hay compatibilidad con transferencias isócronas. No debería ser difícil agregarlo siguiendo la implementación de modos de transferencia existentes como ejemplos, pero también es un modo poco común y no tenía ningún dispositivo en el que probarlo, así que por ahora lo dejé como no compatible. Si tienes uno de esos dispositivos y quieres contribuir a la biblioteca, ¡te damos la bienvenida a las RR.PP.!
  • Las limitaciones anteriores entre plataformas mencionadas anteriormente. Esas limitaciones se imponen a los sistemas operativos, por lo que no podemos hacer mucho en este caso, excepto pedirles a los usuarios que anulen el controlador o los permisos. Sin embargo, si vas a portar dispositivos en serie o HID, puedes seguir el ejemplo de libusb y portar alguna otra biblioteca a otra API de Fugu. Por ejemplo, puedes portar una biblioteca de 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 transferir 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 que se deben seguir para crear la demostración web de gPhoto2, 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 inspirado y que pruebes la demostración, juegues con la propia biblioteca o, quizás, incluso puedas transferir otra biblioteca muy utilizada a una de las APIs de Fugu.