Saiba como o código que interage com dispositivos externos pode ser portado para a Web com as APIs WebAssembly e Fugu.
Em uma postagem anterior, mostrei como portar apps que usam APIs de sistema de arquivos para a Web com a API File System Access, o WebAssembly e o Asyncify. Agora quero continuar no mesmo tópico de integração de APIs Fugu com o WebAssembly e portabilidade de apps para a Web sem perder recursos importantes.
Vou mostrar como os apps que se comunicam com dispositivos USB podem ser portados para a Web. Para isso, vou portar a libusb, uma biblioteca USB conhecida escrita em C, para o WebAssembly (via Emscripten), o Asyncify e a WebUSB.
Antes de mais nada: uma demonstração
A coisa mais importante a fazer 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 que 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 (link em inglês) está nesse espaço há tempo suficiente para fazer engenharia reversa e implementar suporte para uma ampla variedade de câmeras digitais. Ele é compatível com vários protocolos, mas o que mais me interessou foi o suporte a USB, que ele realiza via libusb.
Vou descrever as etapas para criar essa demonstração em duas partes. Nesta postagem do blog, vou descrever como portei a libusb e quais truques podem ser necessários para portar outras bibliotecas conhecidas para as APIs do Fugu. No segundo post, vou entrar em detalhes sobre a portabilidade e a integração do gPhoto2.
No final, consegui um aplicativo da web funcional que mostra o feed ao vivo de uma DSLR e pode controlar as configurações dela por USB. Confira a demonstração ao vivo ou pré-gravada antes de ler os detalhes técnicos:
Observação sobre peculiaridades específicas da câmera
Você deve ter notado que mudar as configurações leva um tempo no vídeo. Como na 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, ela só fornece comandos para aumentar ou diminuir esses valores pelo número especificado de etapas. Para complicar ainda mais, ela também não retorna uma lista dos valores realmente compatíveis. A lista retornada parece ser fixada no código em muitos modelos de câmera da Sony.
Ao definir um desses valores, o gPhoto2 não tem outra escolha a não ser:
- Dê um passo (ou alguns) na direção do valor escolhido.
- Aguarde um pouco para que a câmera atualize as configurações.
- Leia o valor em que a câmera realmente parou.
- Verifique se a última etapa não pulou o valor desejado nem voltou para o fim ou o início da lista.
- Repetir.
Isso pode levar algum tempo, mas, se o valor for compatível com a câmera, ele será alcançado. Caso contrário, o valor compatível mais próximo será usado.
Outras câmeras provavelmente terão conjuntos diferentes de configurações, APIs subjacentes e peculiaridades. O gPhoto2 é um projeto de código aberto, e não é possível testar todos os modelos de câmera de forma automatizada ou manual. Por isso, relatórios detalhados de problemas e solicitações de envio são sempre bem-vindos. No entanto, reproduza os problemas com o cliente oficial do gPhoto2 primeiro.
Observações importantes sobre a compatibilidade multiplataforma
Infelizmente, no Windows, qualquer dispositivo "conhecido", incluindo câmeras DSLR, recebe um driver de sistema que não é compatível com a WebUSB. Se você quiser testar a demonstração no Windows, use uma ferramenta como o Zadig para substituir o driver da DSLR conectada por WinUSB ou libusb. Essa abordagem funciona bem para mim e para muitos outros usuários, mas use 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 deve funcionar sem problemas. Se você estiver testando em um smartphone Android, mude para o modo paisagem, porque não me esforcei muito para torná-lo responsivo (PRs são bem-vindos!):
Para um guia mais detalhado sobre o uso multiplataforma da WebUSB, consulte a seção "Considerações específicas da plataforma" de "Como criar um dispositivo para a WebUSB".
Adicionar um novo back-end à libusb
Agora vamos aos detalhes técnicos. Embora seja possível fornecer uma API shim semelhante à libusb (isso já foi feito por outras pessoas) 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 direito, de uma forma que pudesse ser contribuída de volta para o upstream e mesclada à libusb no futuro.
Por sorte, o README da libusb (em inglês) diz:
"A libusb é abstraída internamente de modo que possa ser portada para outros sistemas operacionais. Consulte o arquivo PORTING para mais informações."
A libusb é estruturada de modo que a API pública seja separada dos "back-ends". Esses back-ends são responsáveis por listar, abrir, fechar e se comunicar com os dispositivos usando as 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 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 é 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, do contexto e da transferência.
Os campos de dados particulares são úteis pelo menos para armazenar identificadores do SO para todos esses itens, já que, sem eles, 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 da WebUSB. A maneira natural de representá-los e armazená-los no Emscripten é usando a classe emscripten::val, que é fornecida como parte do Embind (sistema de vinculação 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),
};
Como armazenar objetos WebUSB como identificadores de dispositivo
A libusb fornece ponteiros prontos para uso para a área alocada para dados particulares. 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 síncronos de C
Agora, era preciso encontrar uma maneira de lidar com APIs WebUSB assíncronas em que a libusb espera operações síncronas. Para isso, eu poderia usar o Asyncify ou, mais especificamente, a integração do Embind com o val::await().
Eu também queria processar corretamente os erros da WebUSB e convertê-los em códigos de erro da libusb, mas o Embind não tem como processar exceções do JavaScript ou rejeições de Promise do lado do C++. Para contornar esse problema, capture uma rejeição no lado do JavaScript e converta o resultado em um objeto { error, value } que possa 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 eu poderia usar promise_result::await() em qualquer Promise retornado de operações WebUSB e inspecionar os campos error e value separadamente.
Por exemplo, para recuperar um val que representa um USBDevice de libusb_device_handle, chamar o método open(), aguardar o resultado e retornar um código do erro como um código de status da libusb, faça o 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 de dispositivos
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 usando navigator.usb.requestDevice(), e o usuário escolhe manualmente qual dispositivo quer expor ou rejeita o pedido de permissão. Depois, o aplicativo lista os dispositivos já aprovados e conectados usando 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 em um botão em uma página). Caso contrário, ela sempre vai retornar uma promessa rejeitada. Os aplicativos libusb geralmente querem listar os dispositivos conectados na inicialização do aplicativo, então 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 à mostrada acima. Há mais alguns hacks 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
Outra parte da porta libusb que quero discutir é o processamento de eventos. Como descrito no artigo anterior, a maioria das APIs em linguagens de sistema como C é síncrona, e o processamento de eventos não é exceção. Normalmente, ele é implementado por um loop infinito que "pesquisa" (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. Depois que o gerenciador é concluído, o controle retorna ao loop e pausa para outra sondagem.
Há alguns problemas com essa abordagem na Web.
Primeiro, a WebUSB não expõe nem pode expor identificadores brutos dos dispositivos subjacentes. Portanto, não é possível fazer a sondagem direta deles. Em segundo lugar, a libusb usa as APIs eventfd e pipe para outros eventos, bem como para processar transferências em sistemas operacionais sem identificadores de dispositivo brutos, mas eventfd não é compatível com o Emscripten no momento, e pipe, embora seja compatível, atualmente não está em conformidade com a especificação e não pode esperar por eventos.
Por fim, o maior problema é que a Web tem o próprio loop de eventos. Esse loop de eventos global é usado para todas as operações de E/S externas (incluindo fetch(), timers ou, neste caso, WebUSB) e invoca manipuladores de eventos ou Promise sempre que as operações correspondentes são concluídas. A execução de outro loop de eventos infinito e aninhado impede que o loop de eventos do navegador avance. Isso significa que não apenas a interface vai parar de responder, mas também que o código nunca vai 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 a libusb em uma demonstração. A página travou.
Assim como em outras E/S de bloqueio, para portar esses loops de eventos 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 principal. A outra é usar o Asyncify para pausar o loop e aguardar eventos de maneira não bloqueante.
Eu não queria fazer mudanças significativas na libusb ou na gPhoto2, e já tinha usado o Asyncify para a integração com Promise, então esse foi o caminho que escolhi. Para simular uma variante de bloqueio de poll(), para a prova de conceito inicial, usei um loop, como 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:
- Chama
poll()para verificar se algum evento já foi informado pelo back-end. Se houver, o loop será interrompido. Caso contrário, a implementação depoll()do Emscripten vai retornar imediatamente com0. - Chama
emscripten_sleep(0). Essa função usa Asyncify esetTimeout()em segundo plano 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 WebUSB. - Verifique se o tempo limite especificado já expirou e, se não, continue o loop.
Como o comentário menciona, essa abordagem não era ideal, porque continuava salvando e restaurando toda a pilha de chamadas com o Asyncify, mesmo quando ainda não havia eventos USB para processar (o que acontece na maior parte do tempo), e porque o próprio setTimeout() tem uma duração mínima de 4 ms em navegadores mais recentes. Ainda assim, funcionou bem o suficiente para produzir uma transmissão ao vivo de 13 a 14 QPS da DSLR na prova de conceito.
Depois, decidi melhorar o código 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 da 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 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 sem interrupções.
Sistema de build e o primeiro teste
Depois que o back-end foi concluído, precisei 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']"
;;
Primeiro, os 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 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 informar essas flags ao vinculador. Portanto, cada aplicativo que usa essa porta libusb também precisa adicionar as mesmas flags do vinculador à configuração do build.
Por fim, como mencionado anteriormente, a WebUSB exige que a enumeração de dispositivos seja feita por uma interação do usuário. Os exemplos e testes da libusb presumem que 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 manual callMain() (-s EXPORTED_RUNTIME_METHODS=...).
Depois de fazer tudo isso, eu poderia disponibilizar os arquivos gerados com um servidor da Web estático, inicializar a 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" veiculada localmente. O console do DevTools está avaliando `navigator.usb.requestDevice({ filters: [] })`, que acionou uma solicitação de permissão e está pedindo ao usuário para escolher um dispositivo USB que deve ser compartilhado com a página. ILCE-6600 (uma câmera Sony) está selecionada no momento.](https://web.developers.google.cn/static/articles/porting-libusb-to-webusb/image/screenshot-showing-chrom-b19a9e4e1b916.png?hl=pt)
![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(['-v'])", 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.](https://web.developers.google.cn/static/articles/porting-libusb-to-webusb/image/screenshot-the-next-step-bd4e4c7f85be7.png?hl=pt)
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 emocionante!
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 aplicativo, faça o seguinte:
- Faça o download da versão mais recente da libusb como um arquivo como parte do build ou adicione-a como um submódulo do Git no projeto.
- Execute
autoreconf -fivna pastalibusb. - Execute
emconfigure ./configure –host=wasm32 –prefix=/some/installation/pathpara inicializar o projeto para compilação cruzada e definir um caminho onde você quer colocar os artefatos criados. - Execute
emmake make install. - Aponte o aplicativo ou a biblioteca de nível superior para pesquisar a libusb no caminho escolhido anteriormente.
- 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 para cancelamento de transferência. Essa é uma limitação da WebUSB que, por sua vez, decorre da falta de cancelamento de transferência multiplataforma na própria libusb.
- Não há suporte para 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 tanto raro e eu não tinha nenhum dispositivo para testá-lo, então, por enquanto, deixei como não compatível. Se você tiver esses dispositivos e quiser contribuir com a biblioteca, as solicitações de envio são bem-vindas.
- As limitações multiplataforma mencionadas anteriormente. Essas limitações são impostas pelos sistemas operacionais, então não há muito o que fazer, exceto pedir que os usuários substituam o driver ou as permissões. No entanto, se você estiver portando dispositivos HID ou seriais, siga o exemplo da libusb e porte outra biblioteca para outra API Fugu. Por exemplo, é possível portar uma biblioteca C hidapi para WebHID e evitar 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 a libusb, podem ser portadas para a Web com alguns truques de integração.
A portabilidade dessas bibliotecas de baixo nível essenciais e amplamente usadas é particularmente gratificante porque, por sua vez, permite trazer bibliotecas de nível mais alto 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, disponibilizando essas experiências com apenas um clique em um link.
No próximo post, vou mostrar as etapas para criar a demonstração do gPhoto2 para Web, que não apenas recupera informações do dispositivo, mas também usa amplamente o recurso de transferência da libusb. Enquanto isso, espero que o exemplo da libusb tenha sido inspirador e que você teste a demonstração, brinque com a biblioteca ou até mesmo porte outra biblioteca amplamente usada para uma das APIs do Fugu.