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

Saiba como códigos que interagem com dispositivos externos podem ser transferidos 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, a WebAssembly e o Asyncify. Agora, quero continuar com a integração das APIs do Fugu com o WebAssembly e a portabilidade 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 por meio da portabilidade da 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 fazer ao transferir uma biblioteca é escolher a demonstração correta, algo que mostraria 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. Em particular, um projeto de código aberto gPhoto2 esteve nesse espaço por tempo suficiente para fazer engenharia reversa e implementar o suporte para uma ampla variedade de câmeras digitais. Ele oferece suporte a vários protocolos, mas o que mais me interessa foi o suporte para USB, realizado via libusb.

Vou descrever as etapas para criar esta 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, vou entrar em detalhes sobre a portabilidade e a integração do próprio gPhoto2.

Por fim, criei um aplicativo da web em funcionamento que visualiza o feed ao vivo de uma DSLR e pode controlar suas configurações por USB. Confira a demonstração ao vivo ou pré-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ê deve ter notado que mudar as configurações demora um pouco. Como a maioria dos outros problemas que você pode encontrar, 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, fornece apenas comandos para aumentar ou diminuir o valor conforme o número de passos especificado. Para complicar as coisas, ela também não retorna uma lista dos valores realmente suportados. A lista retornada parece estar fixada no código em muitos modelos de câmera Sony.

Ao definir um desses valores, o gPhoto2 não tem outra opção além de:

  1. Dê um ou mais passos na direção do valor escolhido.
  2. Espere a câmera atualizar as configurações.
  3. Leia de volta o valor em que a câmera realmente pousou.
  4. Verifique se a última etapa não ultrapassou o valor desejado nem não incluiu o fim ou o início da lista.
  5. Esse processo precisa ser repetido.

Pode levar algum tempo, mas se o valor for realmente suportado pela câmera, ele chegará lá; caso contrário, será interrompido no valor suportado mais próximo.

Outras câmeras provavelmente terão conjuntos diferentes de configurações, APIs e peculiaridades. Lembre-se de que o gPhoto2 é um projeto de código aberto e os testes automatizados ou manuais de todos os modelos de câmera disponíveis simplesmente não são viáveis. Portanto, relatórios de problemas e PRs detalhados são sempre bem-vindos (mas reproduza os problemas com o cliente oficial do gPhoto2 primeiro).

Observações importantes sobre compatibilidade entre plataformas

Infelizmente, no Windows, todos os dispositivos conhecidos, incluindo câmeras DSLR, têm um driver de sistema, que não é compatível com WebUSB. Para testar a demonstração no Windows, use uma ferramenta como o Zadig (em inglês) para substituir o driver da DSLR conectada por 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, provavelmente será necessário 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, troque para o modo paisagem, porque não me esforcei muito para torná-lo responsivo (os 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. Imagem por Surma.

Para conferir 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 WebUSB".

Como adicionar um novo back-end ao libusb

Agora, vamos aos detalhes técnicos. Embora seja possível fornecer uma API de paliativo 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 de volta para o sistema e se fundir a um libusb no futuro.

Felizmente, o README do libusb diz:

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

O libusb é estruturado de maneira que a API pública é separada dos "back-ends". Esses back-ends são responsáveis por listar, abrir, fechar e 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.

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

Os campos de dados privados são úteis pelo menos para armazenar identificadores do SO para tudo isso. Sem identificadores, não sabemos a qual item qualquer operação se aplica. Na implementação da Web, os identificadores do SO seriam os objetos JavaScript WebUSB subjacentes. A maneira natural de representá-las e armazená-las no Emscripten é pela classe emscripten::val, que é fornecida como parte do Embind (sistema de vinculações do Emscripten).

A maioria dos back-ends da pasta é implementada em C, mas alguns são implementados em C++. Como o Embind só funciona com C++, optei por isso. 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 WebUSB como identificadores de dispositivos

O libusb fornece ponteiros prontos para uso para a área alocada para dados particulares. Para trabalhar com esses ponteiros como instâncias de val, adicionei pequenos auxiliares que os criam no local, os recuperam como referências e movem 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, posso usar o Asyncify ou, mais especificamente, a integração Embind via val::await().

Também queria lidar corretamente com erros de WebUSB e convertê-los em códigos de erro libusb, mas Embind atualmente não tem como lidar com exceções de 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 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 promise_result::await() em qualquer Promise retornada de operações WebUSB e inspecionar os campos error e value separadamente.

Por exemplo, a recuperação de um val representando 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 libusb é semelhante ao seguinte:

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

Obviamente, antes de 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.

O problema é 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 usando 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 vai listar os dispositivos já aprovados e conectados pelo navigator.usb.getDevices().

Inicialmente, 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 é considerada uma operação confidencial 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 recusada. Os aplicativos libusb geralmente querem 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, conforme mostrado acima. Há mais algumas dicas 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. Verifique o código e os comentários no GitHub, caso tenha interesse.

Portabilidade de loops de eventos para a Web

Outra parte da porta libusb que quero discutir é o gerenciamento de eventos. Como descrito no artigo anterior, a maioria das APIs em linguagens de sistema como a C é síncrona, e o processamento de eventos não é exceção. Geralmente, ele é implementado por meio de um loop infinito que "pesquisa" (tenta ler dados ou bloqueia a execução até que alguns dados estejam disponíveis) a partir de um conjunto de fontes externas de E/S e, quando pelo menos uma delas responde, passa isso como um evento para o manipulador correspondente. Depois que o gerenciador é concluído, o controle retorna ao loop e pausa para outra pesquisa.

Há alguns problemas com essa abordagem na Web.

Primeiro, o WebUSB não expõe identificadores brutos dos dispositivos subjacentes. Portanto, pesquisá-los diretamente não é uma opção. Em segundo lugar, o libusb usa as APIs eventfd e pipe para outros eventos, bem como para lidar com transferências em sistemas operacionais sem identificadores de dispositivos brutos, mas eventfd não tem suporte em Emscripten e pipe, embora seja compatível, atualmente não está em conformidade com as especificações e não pode esperar eventos.

Finalmente, o maior problema é que a web possui seu próprio loop de eventos. Esse loop de eventos global é usado para qualquer operação externa de E/S (incluindo fetch(), timers ou, nesse caso, WebUSB) e invoca manipuladores de eventos ou Promise sempre que as operações correspondentes são concluídas. Executar outro loop de eventos aninhado e infinito impedirá que o loop de eventos do navegador avance. Isso significa que a interface não só não responde, mas também o código nunca recebe notificações para os mesmos eventos de E/S que está aguardando. Isso geralmente resulta em um impasse. E foi isso 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 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 lidar com eventos de E/S em uma linha de execução separada e passar os resultados de volta para a linha principal. A outra é usar a funçã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, então escolhi o caminho. Para simular uma variante de bloqueio de poll(), usei uma repetição 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. Chame poll() para verificar se algum evento já foi informado pelo back-end. Se houver algumas, 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() em segundo plano e é usada aqui para retornar o controle ao loop de eventos principal do navegador. Isso permite que o navegador processe interações do usuário e eventos de E/S, incluindo WebUSB.
  3. Verifique se o tempo limite especificado já expirou e, se não tiver, continue o loop.

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

Mais tarde, decidi aprimorá-lo aproveitando 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 libusb específica. Para isso, usei o mecanismo de espera e notificação baseado na macro EM_ASYNC_JS (em inglês):

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 relatar 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 "ativar" do modo de suspensão assíncrono quando um evento em-libusb é recebido ou o tempo limite expirou:

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 em tempos de suspensão e ativações, esse mecanismo corrigiu os problemas de eficiência da implementação anterior baseada no 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 em tempo real estável.

Sistema de build e o primeiro teste

Após o término do back-end, tive que adicioná-lo a Makefile.am e configure.ac. A única parte interessante aqui é a modificação das 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. Assim, 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 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) por meio de parâmetros do vinculador. Infelizmente, não há como uma biblioteca reportar essas sinalizações ao vinculador, portanto, cada aplicativo que usar essa porta libusb também terá que adicionar as mesmas sinalizações do vinculador à configuração da compilação.

Por fim, como mencionado anteriormente, o WebUSB exige que a enumeração de dispositivos seja feita por um gesto do usuário. Exemplos e testes de libusb pressupõem 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 de fazer tudo isso, eu poderia fornecer 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 `testlibusb` exibida localmente. O console do DevTools está avaliando o `Navigator.usb.requestDevice({ filters: [] })`, que acionou uma solicitação de permissão. No momento, o usuário pede para o usuário escolher um dispositivo USB que será compartilhado com a página. A ILCE-6600 (uma câmera Sony) está selecionada 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 `Module.callMain([&#39;-v&#39;])`, 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 estágio em que ela produz um resultado válido 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 etapa de vinculação do aplicativo. Para usar essa porta libusb no seu aplicativo, faça o seguinte:

  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 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 em que você quer colocar os artefatos criados.
  4. Execute emmake make install.
  5. Aponte seu aplicativo ou biblioteca de nível superior para procurar o libusb no caminho escolhido anteriormente.
  6. Adicione as seguintes flags aos argumentos de link do aplicativo: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

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

  • Não há suporte ao cancelamento da transferência. Essa é uma limitação do WebUSB, que, por sua vez, decorrente da falta de cancelamento de transferência entre plataformas no libusb.
  • Sem suporte à 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, então, por enquanto, deixei como sem suporte. Se você tiver esses dispositivos e quiser contribuir com a biblioteca, os RPs são bem-vindos.
  • As limitações entre plataformas mencionadas anteriormente. Essas limitações são impostas pelos sistemas operacionais, então não podemos fazer muito aqui, exceto pedir aos usuários que 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 Fugu. Por exemplo, é possível portar uma biblioteca C hidapi para WebHID e ignorar todos os 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 a libusb, podem ser transferidas para a Web com alguns truques de integração.

A portabilidade dessas bibliotecas essenciais e amplamente utilizadas de baixo nível é particularmente gratificante porque, por sua vez, permite também levar bibliotecas de nível superior ou até mesmo aplicativos inteiros para a Web. Isso abre experiências que antes estavam limitadas a usuários de uma ou duas plataformas, a todos os tipos de dispositivos e sistemas operacionais, disponibilizando essas experiências com apenas um clique no link.

Na próxima postagem, veremos as etapas para criar a demonstração do gPhoto2 na Web, que não apenas recupera informações do dispositivo, mas também usa extensivamente o recurso de transferência do Libusb. Enquanto isso, espero que você tenha gostado do exemplo do libusb e teste a demonstração, brincando com a biblioteca ou talvez vá em frente e transfira outra biblioteca amplamente usada para uma das APIs do Fugu também.