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 ao portar uma biblioteca é escolher a demonstração certa, algo que mostre os recursos da biblioteca portada, 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 pela 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 falar sobre a portabilidade e a integração do 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 peculiaridades específicas da câmera

Você pode ter notado que a mudança das 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, mas 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. Faça uma ou algumas 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 compatível com a câmera, ele será alcançado. Caso contrário, ele 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 a 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, provavelmente será necessário definir permissões personalizadas para permitir o acesso à sua DSLR pelo 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 é pela classe emscripten::val, que é 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 na área alocada para 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 é necessário um método para processar APIs WebUSB assíncronas em que o libusb espera operações síncronas. Para isso, usei o Asyncify ou, mais especificamente, a integração do Embind usando 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 capturando uma rejeição no lado do JavaScript e convertendo o resultado em um objeto { error, value } que pode ser analisado com segurança do 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 de dispositivos

Obviamente, antes de abrir qualquer dispositivo, a 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. Em seguida, o aplicativo lista os dispositivos já aprovados e conectados pelo navigator.usb.getDevices().

No início, tentei usar requestDevice() diretamente na implementação do gerenciador get_device_list. No entanto, mostrar uma solicitação de permissão com uma lista de dispositivos conectados é considerado uma operação sensível e precisa ser acionada pela interação do usuário (como um clique no botão em uma página). Caso contrário, ela sempre retornará uma promessa rejeitada. Os aplicativos libusb geralmente querem listar os dispositivos conectados ao iniciar o 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 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 eventos infinito aninhado bloqueia o progresso do loop de eventos do navegador, o que significa que a interface não responde mais e o código não recebe 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 travou.

Assim como com 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.

Não quero fazer mudanças significativas no libusb ou no gPhoto2, e já usei o Asyncify para integração com Promise, então esse é 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 foi informado pelo back-end. Se houver, o loop será interrompido. Caso contrário, a implementação de poll() do Emscripten 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. Se não, 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. Há várias maneiras de melhorar essa implementação, mas, por enquanto, escolhi emitir eventos personalizados diretamente no objeto global, sem associá-los a uma estrutura de dados específica do libusb. 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 espera 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 uma saída diferente 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 de JavaScript e 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 ao 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, o WebUSB exige que a enumeração de dispositivos seja feita por um gesto do usuário. Os exemplos e testes do libusb presumem que eles podem enumerar dispositivos na inicialização e falhar com um erro sem alterações. 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 portar bibliotecas para uma nova plataforma, chegar ao estágio em que ela produz uma saída válida pela primeira vez é muito empolgante.

Como usar a porta

Como mencionado acima, a porta depende de alguns recursos do Emscripten que precisam ser ativados na fase 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 versão mais recente do libusb como um arquivo como parte do build ou adicione-o 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 a compilação cruzada e definir um caminho para 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 isocrona. 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 contornar 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é mesmo bibliotecas de baixo nível, como libusb, podem ser portadas para a Web com alguns truques de integração.

Portar bibliotecas de baixo nível essenciais e amplamente usadas é particularmente gratificante, porque, por sua vez, permite levar bibliotecas de nível superior 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.

No próximo post, vou mostrar as etapas envolvidas na criação da demonstração da gPhoto2 na Web, que não apenas recupera informações do dispositivo, mas também usa extensivamente o recurso de transferência da 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.