Portabilidade de aplicativos USB para a Web. Parte 1: libusb

Saiba como o código que interage com dispositivos externos pode ser transferido para a Web com as APIs WebAssembly e Fugu.

Em uma postagem anterior, mostrei como portar apps que usam APIs do sistema de arquivos para a Web com a API File System Access, o WebAssembly e o Asyncify. Agora quero continuar o mesmo tópico de integração das APIs Fugu com o WebAssembly e a transferência de apps para a Web sem perder recursos importantes.

Vou mostrar como os apps que se comunicam com dispositivos USB podem ser transferidos para a Web transferindo libusb, uma biblioteca USB conhecida escrita em C, para WebAssembly (usando Emscripten), Asyncify e WebUSB.

Primeiro, uma demonstração

O mais importante a fazer ao transferir uma biblioteca é escolher a demonstração certa, algo que mostre os recursos da biblioteca transferida, permitindo que você a teste de várias maneiras e seja visualmente atraente ao mesmo tempo.

A ideia que escolhi foi o controle remoto de DSLR. Em particular, um projeto de código aberto gPhoto2 está nesse espaço há tempo suficiente para fazer engenharia reversa e implementar suporte para uma grande variedade de câmeras digitais. Ele oferece suporte a vários protocolos, mas o que mais me interessava era o suporte a USB, que é realizado por libusb.

Vou descrever as etapas para criar essa demonstração em duas partes. Neste post do blog, vou descrever como fiz a portabilidade da libusb e quais truques podem ser necessários para portar outras bibliotecas conhecidas para as APIs do Fugu. Na segunda postagem, vou explicar em detalhes como portar e integrar o gPhoto2.

No final, consegui um aplicativo da Web que mostra uma prévia do feed ao vivo de uma DSLR e pode controlar as configurações por USB. Confira a demonstração ao vivo ou gravada antes de ler os detalhes técnicos:

A demonstração em execução em um laptop conectado a uma câmera Sony.

Observação sobre as peculiaridades da câmera

Você pode ter notado que a mudança de configurações leva um tempo no vídeo. Como na maioria dos outros problemas, isso não é causado pelo desempenho do WebAssembly ou do WebUSB, e sim pela forma como o gPhoto2 interage com a câmera específica escolhida para a demonstração.

A Sony a6600 não expõe uma API para definir valores como ISO, abertura ou velocidade do obturador diretamente. Em vez disso, ela só oferece comandos para aumentar ou diminuir o número especificado de etapas. Para complicar ainda mais, ele não retorna uma lista dos valores realmente compatíveis. A lista retornada parece estar codificada em muitos modelos de câmera da Sony.

Ao definir um desses valores, o gPhoto2 não tem outra escolha a não ser:

  1. Complete uma ou várias etapas na direção do valor escolhido.
  2. Aguarde um pouco até que a câmera atualize as configurações.
  3. Leia o valor em que a câmera pousou.
  4. Verifique se a última etapa não pulou o valor desejado nem se enrolou no final ou no início da lista.
  5. Esse processo precisa ser repetido.

Isso pode levar algum tempo, mas se o valor for realmente aceito pela câmera, ele vai chegar lá e, se não for, vai parar no valor compatível mais próximo.

Outras câmeras provavelmente terão diferentes conjuntos de configurações, APIs e peculiaridades. O gPhoto2 é um projeto de código aberto, e o teste automatizado ou manual de todos os modelos de câmera simplesmente não é viável. Portanto, relatórios de problemas detalhados e PRs são sempre bem-vindos. No entanto, primeiro reproduza os problemas com o cliente oficial do gPhoto2.

Observações importantes sobre compatibilidade multiplataforma

Infelizmente, no Windows, qualquer dispositivo "conhecido", incluindo câmeras DSLR, recebe um driver do sistema, que não é compatível com o WebUSB. Se você quiser testar a demonstração no Windows, use uma ferramenta como o Zadig para substituir o driver da DSLR conectada para WinUSB ou libusb. Essa abordagem funciona bem para mim e muitos outros usuários, mas você deve usá-la por sua própria conta e risco.

No Linux, é provável que você precise definir permissões personalizadas para permitir o acesso à DSLR via WebUSB, embora isso dependa da sua distribuição.

No macOS e no Android, a demonstração deve funcionar imediatamente. Se você estiver testando em um smartphone Android, mude para o modo paisagem, porque não me esforcei muito para deixar o app responsivo (PRs são bem-vindos!):

Smartphone Android conectado a uma câmera Canon por um cabo USB-C.
A mesma demonstração em execução em um smartphone Android. Foto de Surma.

Para um guia mais detalhado sobre o uso do WebUSB em várias plataformas, consulte a seção Considerações específicas da plataforma em "Como criar um dispositivo para o WebUSB".

Como adicionar um novo back-end à libusb

Agora, vamos aos detalhes técnicos. Embora seja possível fornecer uma API de shim semelhante à libusb (isso já foi feito por outras pessoas) e vincular outros aplicativos a ela, essa abordagem é propensa a erros e dificulta qualquer outra extensão ou manutenção. Eu queria fazer as coisas da maneira certa, de uma forma que pudesse ser potencialmente enviada de volta à upstream e mesclada com a libusb no futuro.

Felizmente, o README do libusb (link em inglês) diz:

"A libusb é abstrata internamente de forma que possa ser portada para outros sistemas operacionais. Consulte o arquivo PORTING para mais informações."

A libusb está estruturada de modo que a API pública seja separada dos "backends". Esses back-ends são responsáveis por listar, abrir, fechar e se comunicar com os dispositivos pelas APIs de baixo nível do sistema operacional. É assim que a libusb já abstrai as diferenças entre Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku e Solaris e funciona em todas essas plataformas.

O que eu precisava fazer era adicionar outro back-end para o "sistema operacional" Emscripten+WebUSB. As implementações desses back-ends estão na pasta 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 back-end inclui o cabeçalho libusbi.h com tipos e helpers comuns e precisa expor uma variável usbi_backend do tipo usbi_os_backend. Por exemplo, o back-end do Windows tem esta aparência:

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

Analisando as propriedades, podemos ver que a struct inclui o nome do back-end, um conjunto de recursos, manipuladores para várias operações USB de baixo nível na forma de ponteiros de função e, por fim, tamanhos para alocar para armazenar dados privados de dispositivo/contexto/transferência.

Os campos de dados particulares são úteis pelo menos para armazenar identificadores do SO para todas essas coisas, já que, sem identificadores, não sabemos a qual item uma determinada operação se aplica. Na implementação da Web, os identificadores do SO seriam os objetos JavaScript do WebUSB. A maneira natural de representá-los e armazená-los no Emscripten é a classe emscripten::val, fornecida como parte do Embind (sistema de vinculações do Emscripten).

A maioria dos back-ends na pasta é implementada em C, mas alguns são implementados em C++. O Embind só funciona com C++, então a escolha foi feita para mim e adicionei libusb/libusb/os/emscripten_webusb.cpp com a estrutura necessária e com sizeof(val) para os campos de dados particulares:

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

Armazenar objetos do WebUSB como identificadores de dispositivo

O libusb fornece ponteiros prontos para uso para a área alocada de dados privados. Para trabalhar com esses ponteiros como instâncias val, adicionei pequenos auxiliares que os constroem no local, os recuperam como referências e movem os 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 da Web assíncronas em contextos C síncronos

Agora, era necessária uma maneira de lidar com APIs WebUSB assíncronas em que o libusb espera operações síncronas. Para isso, eu poderia usar o Asyncify ou, mais especificamente, a integração do Embind via val::await().

Também queria processar corretamente os erros do WebUSB e convertê-los em códigos de erro do libusb, mas o Embind atualmente não tem como processar exceções do JavaScript ou rejeições de Promise do lado do C++. Esse problema pode ser resolvido com a captura de uma rejeição no lado do JavaScript e convertendo o resultado em um objeto { error, value } que agora pode ser analisado com segurança no lado do C++. Fiz isso com uma combinação da macro EM_JS e das APIs 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()};
  }
};

Agora posso usar promise_result::await() em qualquer Promise retornado das operações do WebUSB e inspecionar os campos error e value separadamente.

Por exemplo, recuperar um val que represente um USBDevice de libusb_device_handle, chamando o método open(), aguardando o resultado e retornando um código de erro como um código de status do libusb é semelhante a este:

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

Enumeração do dispositivo

É claro que, antes que eu possa abrir qualquer dispositivo, o libusb precisa recuperar uma lista de dispositivos disponíveis. O back-end precisa implementar essa operação usando um gerenciador get_device_list.

A dificuldade é que, ao contrário de outras plataformas, não há como enumerar todos os dispositivos USB conectados na Web por motivos de segurança. Em vez disso, o fluxo é dividido em duas partes. Primeiro, o aplicativo da Web solicita dispositivos com propriedades específicas por meio de navigator.usb.requestDevice(), e o usuário escolhe manualmente qual dispositivo quer expor ou rejeita a solicitação de permissão. Depois disso, o aplicativo lista os dispositivos já aprovados e conectados via navigator.usb.getDevices().

No início, tentei usar o requestDevice() diretamente na implementação do gerenciador get_device_list. No entanto, a exibição de uma solicitação de permissão com uma lista de dispositivos conectados é considerada uma operação sensível e precisa ser acionada pela interação do usuário (como um clique de botão em uma página). Caso contrário, ela sempre retorna uma promessa rejeitada. Os aplicativos libusb podem querer listar os dispositivos conectados na inicialização. Portanto, usar requestDevice() não era uma opção.

Em vez disso, tive que deixar a invocação de navigator.usb.requestDevice() para o desenvolvedor final e expor apenas os dispositivos já aprovados 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;
}

A maior parte do código de back-end usa val e promise_result de maneira semelhante, como mostrado acima. Há alguns hacks mais interessantes no código de processamento de transferência de dados, mas esses detalhes de implementação são menos importantes para os fins deste artigo. Confira o código e os comentários no GitHub se tiver interesse.

Como portar loops de eventos para a Web

Outro aspecto do porta libusb que quero discutir é o processamento de eventos. Conforme descrito no artigo anterior, a maioria das APIs em linguagens de sistema, como C, são síncronas, e o processamento de eventos não é uma exceção. Ele geralmente é implementado por um loop infinito que "consulta" (tenta ler dados ou bloqueia a execução até que alguns dados estejam disponíveis) de um conjunto de fontes de E/S externas e, quando pelo menos uma delas responde, transmite isso como um evento para o gerenciador correspondente. Quando o gerenciador é concluído, o controle retorna ao loop e pausa para outra pesquisa.

Essa abordagem tem alguns problemas na Web.

Primeiro, o WebUSB não expõe e não pode expor identificadores brutos dos dispositivos subjacentes. Portanto, a pesquisa direta deles não é uma opção. Em segundo lugar, a libusb usa as APIs eventfd e pipe para outros eventos, além de processar transferências em sistemas operacionais sem identificadores de dispositivo brutos, mas o eventfd não tem suporte no Emscripten e o pipe, embora tenha suporte, não está em conformidade com a especificação e não pode aguardar eventos.

Por fim, o maior problema é que a Web tem seu próprio loop de eventos. Esse loop de eventos global é usado para qualquer operação de E/S externa (incluindo fetch(), timers ou, neste caso, WebUSB) e invoca gerenciadores de eventos ou Promise sempre que as operações correspondentes são concluídas. A execução de outro loop de evento aninhado e infinito impede que o loop de eventos do navegador progredirá, o que significa que não apenas a IU deixará de responder, mas também que o código nunca receberá notificações para os mesmos eventos de E/S que está esperando. Isso geralmente resulta em um deadlock, e foi o que aconteceu quando tentei usar o libusb em uma demonstração. A página congelou.

Assim como em outras E/S de bloqueio, para transferir essas linhas de execução de eventos para a Web, os desenvolvedores precisam encontrar uma maneira de executar essas linhas sem bloquear a linha de execução principal. Uma maneira é refatorar o aplicativo para processar eventos de E/S em uma linha de execução separada e transmitir os resultados de volta para a principal. A outra é usar o Asyncify para pausar o loop e aguardar os eventos sem bloqueio.

Eu não queria fazer mudanças significativas no libusb ou no gPhoto2 e já usei o Asyncify para a integração do Promise. Esse foi o caminho que escolhi. Para simular uma variante de bloqueio de poll(), usei um loop para a prova de conceito inicial, conforme mostrado abaixo:

#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

O que ele faz é:

  1. Chama poll() para verificar se algum evento já foi relatado pelo back-end. Se houver, o loop será interrompido. Caso contrário, a implementação de poll() do Emscripten vai retornar imediatamente com 0.
  2. Chama emscripten_sleep(0). Essa função usa Asyncify e setTimeout() e é usada aqui para devolver o controle ao loop de eventos principal do navegador. Isso permite que o navegador processe todas as interações do usuário e eventos de E/S, incluindo o WebUSB.
  3. Verifique se o tempo limite especificado já expirou e, em caso negativo, continue o loop.

Como mencionado no comentário, essa abordagem não era ideal, porque continuava salvando e restaurando toda a pilha de chamadas com o Asyncify, mesmo quando não havia eventos USB para processar (o que acontece na maioria das vezes), e porque o setTimeout() tem uma duração mínima de 4 ms em navegadores modernos. Ainda assim, funcionou bem o suficiente para produzir uma transmissão ao vivo de 13 a 14 QPS da DSLR na prova de conceito.

Mais tarde, decidi melhorar o app usando o sistema de eventos do navegador. Existem várias maneiras de melhorar ainda mais essa implementação, mas, por enquanto, optei por emitir eventos personalizados diretamente no objeto global, sem associá-los a uma estrutura de dados libusb específica. Fiz isso usando o seguinte mecanismo de espera e notificação com base na 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);
  }
});

A função em_libusb_notify() é usada sempre que a libusb tenta informar um evento, como a conclusão da transferência de dados:

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
}

Enquanto isso, a parte em_libusb_wait() é usada para "acordar" do modo de suspensão do Asyncify quando um evento em-libusb é recebido ou o tempo limite expira:

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

Devido à redução significativa de tempos de inatividade e ativação, esse mecanismo corrigiu os problemas de eficiência da implementação anterior baseada em emscripten_sleep() e aumentou a taxa de transferência de demonstração de DSLR de 13 a 14 QPS para mais de 30 QPS consistentes, o que é suficiente para um feed ao vivo tranquilo.

Criar o sistema e o primeiro teste

Depois que o back-end foi concluído, tive que adicioná-lo a Makefile.am e configure.ac. A única parte interessante aqui é a modificação de flags específicas do 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']"
  ;;

Em primeiro lugar, executáveis em plataformas Unix normalmente não têm extensões de arquivo. No entanto, o Emscripten produz saídas diferentes, dependendo da extensão solicitada. Estou usando AC_SUBST(EXEEXT, …) para mudar a extensão executável para .html, de modo que qualquer executável em um pacote (testes e exemplos) se torne um HTML com o shell padrão do Emscripten, que cuida do carregamento e da instanciação do JavaScript e do WebAssembly.

Em segundo lugar, como estou usando Embind e Asyncify, preciso ativar esses recursos (--bind -s ASYNCIFY) e permitir o crescimento dinâmico da memória (-s ALLOW_MEMORY_GROWTH) usando parâmetros de linker. Infelizmente, não há como uma biblioteca informar essas flags para o vinculador. Portanto, todos os aplicativos que usam essa porta libusb também precisam adicionar as mesmas flags de vinculador à configuração de build.

Por fim, como mencionado anteriormente, a WebUSB exige que a enumeração do dispositivo seja feita por um gesto do usuário. Os exemplos e testes de libusb presumem que eles podem enumerar dispositivos na inicialização e falham com um erro sem mudanças. Em vez disso, tive que desativar a execução automática (-s INVOKE_RUN=0) e expor o método callMain() manual (-s EXPORTED_RUNTIME_METHODS=...).

Depois disso, pude disponibilizar os arquivos gerados com um servidor da Web estático, inicializar o WebUSB e executar esses executáveis HTML manualmente com a ajuda das DevTools.

Captura de tela mostrando uma janela do Chrome com o DevTools aberto em uma página &quot;testlibusb&quot; servida localmente. O console do DevTools está avaliando &quot;navigator.usb.requestDevice({ filters: [] })&quot;, que acionou uma solicitação de permissão e está pedindo que o usuário escolha um dispositivo USB que será compartilhado com a página. A ILCE-6600 (uma câmera Sony) está selecionada.

Captura de tela da próxima etapa, com o DevTools ainda aberto. Depois que o dispositivo foi selecionado, o console avaliou uma nova expressão &quot;Module.callMain(&quot;-v&quot;)&quot;, que executou o app &quot;testlibusb&quot; no modo detalhado. A saída mostra várias informações detalhadas sobre a câmera USB conectada anteriormente: fabricante Sony, produto ILCE-6600, número de série, configuração etc.

Não parece muito, mas, ao transferir bibliotecas para uma nova plataforma, chegar ao ponto em que produz uma saída válida pela primeira vez é bem empolgante.

Como usar a porta

Como mencionado acima, a porta depende de alguns recursos do Emscripten que precisam ser ativados no estágio de vinculação do aplicativo. Se você quiser usar essa porta libusb no seu próprio aplicativo, faça o seguinte:

  1. Faça o download da libusb mais recente como um arquivo como parte do build ou adicione-a como um submódulo do Git no projeto.
  2. Execute autoreconf -fiv na pasta libusb.
  3. Execute emconfigure ./configure –host=wasm32 –prefix=/some/installation/path para inicializar o projeto para compilação cruzada e definir um caminho onde você quer colocar os artefatos criados.
  4. Execute emmake make install.
  5. Aponte seu aplicativo ou biblioteca de nível superior para pesquisar o libusb no caminho escolhido anteriormente.
  6. Adicione as flags abaixo aos argumentos de vinculação do app: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

No momento, a biblioteca tem algumas limitações:

  • Não há suporte para cancelamento de transferência. Essa é uma limitação do WebUSB, que, por sua vez, decorre da falta de cancelamento de transferência entre plataformas na própria libusb.
  • Não há suporte para transferência isocrômica. Não deve ser difícil adicionar esse modo seguindo a implementação dos modos de transferência atuais como exemplos, mas ele também é um modo raro e não tinha dispositivos para testar, então, por enquanto, não há suporte para ele. Se você tiver esses dispositivos e quiser contribuir com a biblioteca, as PRs são bem-vindas.
  • As limitações multiplataforma mencionadas anteriormente. Essas limitações são impostas pelos sistemas operacionais. Portanto, não podemos fazer muito, a não ser pedir aos usuários para substituir o driver ou as permissões. No entanto, se você estiver fazendo a portabilidade de dispositivos HID ou seriais, siga o exemplo da libusb e faça a portabilidade de outra biblioteca para outra API Fugu. Por exemplo, é possível portar uma biblioteca C hidapi para WebHID e evitar completamente esses problemas associados ao acesso USB de baixo nível.

Conclusão

Nesta postagem, mostrei como, com a ajuda das APIs Emscripten, Asyncify e Fugu, até bibliotecas de baixo nível, como a libusb, podem ser adaptadas para a Web com alguns truques de integração.

O processo de portar bibliotecas de baixo nível essenciais e amplamente usadas é particularmente gratificante, porque permite levar bibliotecas de nível mais alto ou até mesmo aplicativos inteiros para a Web. Isso abre experiências que antes eram limitadas aos usuários de uma ou duas plataformas para todos os tipos de dispositivos e sistemas operacionais, tornando essas experiências disponíveis com apenas um clique.

Na próxima postagem, vou mostrar as etapas envolvidas na criação da demonstração do gPhoto2 na Web, que não apenas recupera as informações do dispositivo, mas também usa extensivamente o recurso de transferência do libusb. Enquanto isso, espero que você tenha achado o exemplo da libusb inspirador e que teste a demonstração, brinque com a biblioteca ou talvez até mesmo faça a portabilidade de outra biblioteca amplamente usada para uma das APIs do Fugu.