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 transferir apps que usam APIs do sistema de arquivos para a Web com a API File System Access, o WebAssembly e o Asyncify. Agora, vamos continuar com o mesmo tópico de integração das APIs do Fuug com o WebAssembly e da portabilidade de apps para a Web sem perder recursos importantes.

Vou mostrar como apps que se comunicam com dispositivos USB podem ser transferidos para a Web por meio da portabilidade do libusb, uma biblioteca USB conhecida escrita em C, para WebAssembly (via Emscripten), Asyncify e WebUSB.

Antes de mais nada: uma demonstração

O mais importante a se 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 DSLR. Mais especificamente, o projeto de código aberto gPhoto2 já está no 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 foi o suporte a USB, que é executado via libusb.

Vou descrever as etapas para criar essa demonstração em duas partes. Nesta postagem do blog, vou descrever como fiz a portabilidade do libusb e quais truques podem ser necessários para transferir outras bibliotecas populares para as APIs do Fugu. Na segunda postagem, entrei em detalhes sobre a portabilidade e a integração do gPhoto2.

Por fim, criei um aplicativo da web que funciona como uma prévia do feed ao vivo de uma DSLR e consegue controlar as configurações por USB. Sinta-se à vontade para conferir a demonstração ao vivo ou gravada antes de ler os detalhes técnicos:

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

Observação sobre peculiaridades específicas da câmera

Você deve ter notado que mudar as configurações demora um pouco 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 diretamente valores como ISO, abertura ou velocidade do obturador. Em vez disso, ele só fornece comandos para aumentar ou diminuir esses valores de acordo com o número especificado de etapas. Para complicar as coisas, ele também não retorna uma lista dos valores realmente compatíveis. A lista retornada parece fixada no código em muitos modelos de câmeras Sony.

Ao definir um desses valores, o gPhoto2 não terá outra escolha senão:

  1. Siga uma ou várias etapas na direção do valor escolhido.
  2. Aguarde um momento até que a câmera atualize as configurações.
  3. Leia de novo o valor que a câmera conseguiu encontrar.
  4. Verifique se a última etapa não ultrapassou o valor desejado nem se conectou ao fim ou ao 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 subjacentes e peculiaridades. O gPhoto2 é um projeto de código aberto e não é possível realizar testes automatizados ou manuais de todos os modelos de câmera disponíveis. Portanto, relatórios detalhados de problemas e PRs são sempre bem-vindos (mas reproduza os problemas com o cliente oficial do gPhoto2 primeiro).

Observações importantes sobre compatibilidade multiplataforma

Infelizmente, no Windows, um "bem conhecido" Os dispositivos, incluindo câmeras DSLR, recebem um driver de sistema, que não é compatível com WebUSB. Se 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 para muitos outros usuários, mas você deve usá-la por sua 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 vai funcionar imediatamente. Se você estiver testando em um smartphone Android, mude para o modo paisagem. Não me esforcei muito para que ele seja responsivo (RPs 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. Imagem de Surma.

Para um guia mais detalhado sobre o uso multiplataforma de WebUSB, consulte as "Considerações específicas de plataforma" de "Como criar um dispositivo para WebUSB".

Como adicionar um novo back-end ao libusb

Agora, vamos aos detalhes técnicos. Embora seja possível fornecer uma API shim semelhante à libusb (isso já foi feito por outras pessoas antes) e vincular outros aplicativos a ela, essa abordagem é propensa a erros e dificulta qualquer extensão ou manutenção adicional. Eu queria fazer as coisas corretamente, de uma forma que pudesse contribuir para o upstream e incorporar no libusb no futuro.

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

“O libusb é abstraído internamente de modo que possa ser transferido para outros sistemas operacionais. Consulte o arquivo PORTING para mais informações."

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

Precisei adicionar outro back-end para o "sistema operacional" Emscripten+WebUSB. As implementações desses back-ends ficam 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 auxiliares comuns e precisa expor uma variável usbi_backend do tipo usbi_os_backend. Por exemplo, o back-end do Windows é assim:

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 estrutura inclui o nome do back-end, um conjunto de recursos, gerenciadores para várias operações USB de baixo nível na forma de ponteiros de função e, por fim, tamanhos a serem alocados para armazenar dados privados no nível do dispositivo/contexto/transferência.

Os campos de dados particulares são úteis pelo menos para armazenar identificadores de SO para tudo isso, já que, sem os identificadores, não sabemos a qual item uma operação se aplica. Na implementação da Web, os identificadores do SO seriam os objetos JavaScript WebUSB subjacentes. 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 da pasta são implementados em C, mas alguns são implementados em C++. O Embind só funciona com C++. Minha escolha foi feita e adicionei libusb/libusb/os/emscripten_webusb.cpp com a estrutura necessária e 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 WebUSB como alças do 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 para fora:

// 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().

Eu também queria processar corretamente os erros WebUSB e convertê-los em códigos de erro libusb, mas o Embind ainda não tem uma maneira de lidar com exceções de JavaScript ou rejeições Promise 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, é possível usar o promise_result::await() em qualquer Promise retornado de operações WebUSB e inspecionar os campos error e value separadamente.

Por exemplo, a recuperação de um val que representa um USBDevice de libusb_device_handle, chama o método open(), aguarda o resultado e retorna um código de erro como um código de status libusb da seguinte forma:

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 pelo 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 o clique de um 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 do aplicativo. 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 do 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, conforme mostrado acima. Existem algumas dicas mais interessantes no código de tratamento da transferência de dados, mas esses detalhes de implementação são menos importantes para os propósitos deste artigo. Se tiver interesse, verifique o código e os comentários no GitHub.

Como transferir repetições de eventos para a Web

Outra parte da porta libusb que quero discutir é o tratamento de eventos. Conforme descrito no artigo anterior, a maioria das APIs em linguagens do sistema como C é síncrona, e o tratamento de eventos não é exceção. Em geral, ela é implementada por um loop infinito que "polls" (tenta ler dados ou bloquear a execução até que alguns dados estejam disponíveis) de um conjunto de origens de E/S externas e, quando pelo menos uma delas responde, passa isso como um evento para o gerenciador correspondente. Quando o manipulador é concluído, o controle retorna ao loop e é pausado para outra enquete.

Há alguns problemas com essa abordagem na Web.

Primeiro, a WebUSB não expõe e não expõe identificadores brutos dos dispositivos. Portanto, pesquisar diretamente esses dispositivos não é uma opção. Em segundo lugar, o libusb usa as APIs eventfd e pipe para outros eventos, bem como para processar transferências em sistemas operacionais sem identificadores de dispositivos brutos, mas eventfd não é compatível com o Emscripten e pipe. Embora seja compatível, não está em conformidade com as especificações e não pode aguardar eventos.

Finalmente, o maior problema é que a web tem seu próprio loop de eventos. Esse loop de evento global é usado para qualquer operação de E/S externa (incluindo fetch(), timers ou, neste caso, WebUSB) e invoca manipuladores de evento 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á aguardando. Isso geralmente resulta em um impasse, e foi o que aconteceu quando tentei usar o libusb em uma demonstração também. A página congelou.

Como acontece com outras E/S de bloqueio, para transferir esses loops de evento para a Web, os desenvolvedores precisam encontrar uma maneira de executá-los 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 linha de execução principal. A outra é usar o Asyncify para pausar o loop e aguardar 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 uma repetição na 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 ela faz é:

  1. Chama poll() para verificar se algum evento já foi relatado pelo back-end. Se houver alguma, a repetição será interrompida. Caso contrário, a implementação de poll() da Emscripten retornará imediatamente com 0.
  2. Chama emscripten_sleep(0). Essa função usa Async e setTimeout() em segundo plano para retornar o controle ao loop de eventos do navegador principal. Isso permite que o navegador processe qualquer interação do usuário e eventos de E/S, incluindo 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 a ideal, porque ela continuava salvando toda a pilha de chamadas com o Asyncify, mesmo quando ainda não havia eventos USB para processar (o que na maior parte do tempo), e porque 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 com DSLR na prova de conceito.

Mais tarde, decidi aprimorá-lo, aproveitando o sistema de eventos do navegador. Há 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 pelo 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 o 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". da suspensão assíncrona quando um evento em-libusb for recebido ou o tempo limite expirar:

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 nas suspensões e ativações, esse mecanismo corrigiu os problemas de eficiência da implementação anterior baseada em emscripten_sleep() e aumentou a capacidade de processamento da demonstração da DSLR de 13 a 14 QPS para mais de 30 QPS, o que é suficiente para um feed ao vivo tranquilo.

Sistema de build e o primeiro teste

Depois que o back-end foi concluído, tive que adicioná-lo a Makefile.am e configure.ac. A única coisa 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']"
  ;;

Primeiro, os 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 dentro de 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.

Segundo, como estou usando o Embind e o Asyncify, preciso ativar esses recursos (--bind -s ASYNCIFY) e permitir o crescimento dinâmico da memória (-s ALLOW_MEMORY_GROWTH) usando parâmetros do vinculador. Infelizmente, não há como uma biblioteca relatar essas sinalizações ao vinculador, portanto, todos os aplicativos que usam essa porta libusb também terão que adicionar as mesmas sinalizações do vinculador à configuração da compilação.

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 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 que tudo isso foi feito, eu poderia disponibilizar os arquivos gerados com um servidor da Web estático, inicializar o WebUSB e executar esses executáveis HTML manualmente com a ajuda do DevTools.

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

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([&#39;-v&#39;])&quot;, que executou o app `testlibusb` 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, veja o que precisa fazer:

  1. Faça o download do libusb mais recente como um arquivo como parte do seu build ou adicione-o como um submódulo git no seu 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. Direcione o aplicativo ou uma biblioteca de nível superior para procurar o libusb abaixo do caminho escolhido anteriormente.
  6. Adicione as seguintes flags aos argumentos de link do seu aplicativo: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

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

  • Sem suporte para cancelamento de transferências. Essa é uma limitação do WebUSB, que, por sua vez, resulta da falta de cancelamento de transferências entre plataformas no próprio libusb.
  • Sem suporte de transferência isócrona. Não deve ser difícil adicioná-lo seguindo a implementação dos modos de transferência existentes como exemplos, mas também é um modo um pouco raro e eu não tinha nenhum dispositivo para testá-lo, por isso, por enquanto, deixei como incompatível. Se você tiver um desses dispositivos e quiser contribuir com a biblioteca, os PRs serão bem-vindos.
  • A mensagem mencionada anteriormente nas limitações multiplataforma. Essas limitações são impostas pelos sistemas operacionais e, por isso, não podemos fazer muito aqui, exceto pedir que os usuários modifiquem o driver ou as permissões. No entanto, se você estiver transferindo dispositivos HID ou seriais, siga o exemplo do libusb e faça a portabilidade de alguma outra biblioteca para outra API do 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.

A portabilidade de bibliotecas de baixo nível essenciais e amplamente utilizadas é particularmente gratificante, porque, por sua vez, permite trazer bibliotecas de nível superior ou até mesmo aplicativos inteiros para a Web. Isso abre experiências que antes eram limitadas a 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 de distância.

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 do libusb inspirador e que experimente a demonstração, brinque com a própria biblioteca ou talvez até vá em frente e faça a portabilidade de outra biblioteca amplamente utilizada para uma das APIs do Fugu também.