Портирование USB-приложений в Интернет. Часть 1: libusb

Узнайте, как код, взаимодействующий с внешними устройствами, можно перенести в Интернет с помощью API WebAssembly и Fugu.

В предыдущем посте я показал, как переносить приложения, использующие API файловой системы, в Интернет с помощью API доступа к файловой системе , WebAssembly и Asyncify . Теперь я хочу продолжить ту же тему интеграции API Fugu с WebAssembly и портирования приложений в Интернет без потери важных функций.

Я покажу, как приложения, взаимодействующие с USB-устройствами, можно портировать в Интернет, портируя libusb — популярную USB-библиотеку, написанную на C — в WebAssembly (через Emscripten ), Asyncify и WebUSB .

Прежде всего: демо

Самое важное, что нужно сделать при портировании библиотеки, — это выбрать правильную демо-версию — что-то, что продемонстрирует возможности портированной библиотеки, позволит вам протестировать ее различными способами и в то же время будет визуально привлекательным.

Идеей, которую я выбрал, было дистанционное управление зеркальной камерой. В частности, проект с открытым исходным кодом gPhoto2 существует достаточно долго, чтобы провести реверс-инжиниринг и реализовать поддержку широкого спектра цифровых камер. Он поддерживает несколько протоколов, но больше всего меня интересовала поддержка USB, которую он осуществляет через libusb.

Я опишу шаги по созданию этой демонстрации в двух частях. В этом сообщении блога я опишу, как я портировал саму libusb и какие хитрости могут потребоваться для переноса других популярных библиотек на API Fugu. Во втором посте я подробно расскажу о портировании и интеграции самого gPhoto2.

В конце концов я получил работающее веб-приложение, которое просматривает прямую трансляцию с зеркальной камеры и может управлять ее настройками через USB. Не стесняйтесь посмотреть живое или предварительно записанное демо, прежде чем читать технические подробности:

Демо-версия работает на ноутбуке, подключенном к камере Sony.

Примечание по особенностям камеры

Возможно, вы заметили, что изменение настроек в видео занимает некоторое время. Как и большинство других проблем, с которыми вы можете столкнуться, это вызвано не производительностью WebAssembly или WebUSB, а тем, как gPhoto2 взаимодействует с конкретной камерой, выбранной для демонстрации.

Sony a6600 не предоставляет API для прямой установки таких значений, как ISO, диафрагма или выдержка, а вместо этого предоставляет только команды для их увеличения или уменьшения на указанное количество шагов. Что еще более усложняет ситуацию, он также не возвращает список фактически поддерживаемых значений — возвращаемый список, похоже, жестко запрограммирован во многих моделях камер Sony.

При установке одного из этих значений у gPhoto2 нет другого выбора, кроме как:

  1. Сделайте шаг (или несколько) в направлении выбранного значения.
  2. Подождите немного, пока камера обновит настройки.
  3. Прочтите значение, на котором фактически остановилась камера.
  4. Убедитесь, что последний шаг не перешел через желаемое значение и не обошел конец или начало списка.
  5. Повторить.

Это может занять некоторое время, но если значение действительно поддерживается камерой, она доберется до него, а если нет, то остановится на ближайшем поддерживаемом значении.

Другие камеры, скорее всего, будут иметь другие наборы настроек, базовые API и особенности. Имейте в виду, что gPhoto2 — это проект с открытым исходным кодом, и автоматическое или ручное тестирование всех существующих моделей камер просто невозможно, поэтому подробные отчеты о проблемах и PR всегда приветствуются (но обязательно воспроизведите проблемы с официальным клиент gPhoto2 в первую очередь).

Важные примечания по кроссплатформенной совместимости

К сожалению, в Windows любым «известным» устройствам, включая зеркальные камеры, назначается системный драйвер, несовместимый с WebUSB. Если вы хотите попробовать демо-версию в Windows, вам придется использовать такой инструмент, как Zadig, чтобы переопределить драйвер подключенной зеркальной камеры на WinUSB или libusb. Этот подход отлично работает для меня и многих других пользователей, но вы должны использовать его на свой страх и риск.

В Linux вам, вероятно, потребуется установить специальные разрешения , чтобы разрешить доступ к вашей зеркальной камере через WebUSB, хотя это зависит от вашего дистрибутива.

На macOS и Android демо-версия должна работать «из коробки». Если вы пробуете его на телефоне Android, обязательно переключитесь в ландшафтный режим, поскольку я не приложил особых усилий, чтобы сделать его отзывчивым (PR приветствуются!):

Телефон Android, подключенный к камере Canon с помощью кабеля USB-C.
Та же демо-версия, работающая на телефоне Android. Изображение Сурма .

Более подробное руководство по кросс-платформенному использованию WebUSB см. в разделе «Соображения, связанные с конкретной платформой» статьи «Создание устройства для WebUSB» .

Добавление нового бэкэнда в libusb

Теперь о технических деталях. Хотя можно предоставить промежуточный API, аналогичный libusb (это уже делалось другими ранее), и связать с ним другие приложения, этот подход подвержен ошибкам и затрудняет дальнейшее расширение или обслуживание. Я хотел сделать все правильно, чтобы в будущем можно было бы снова внести свой вклад в исходный код и объединить его с libusb.

К счастью, в README libusb написано:

«Libusb внутренне абстрагирован таким образом, что его можно будет портировать на другие операционные системы. Дополнительную информацию см. в файле PORTING ».

libusb структурирован таким образом, что общедоступный API отделен от «бэкэндов». Эти серверные части отвечают за листинг, открытие, закрытие и фактическую связь с устройствами через низкоуровневые API-интерфейсы операционной системы. Именно так libusb уже абстрагирует различия между Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku и Solaris и работает на всех этих платформах.

Мне нужно было добавить еще один бэкэнд для «операционной системы» Emscripten+WebUSB. Реализации этих бэкэндов находятся в папке 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

Каждый бэкэнд включает заголовок libusbi.h с общими типами и помощниками и должен предоставить переменную usbi_backend типа usbi_os_backend . Например, вот как выглядит серверная часть Windows :

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

Просматривая свойства, мы видим, что структура включает в себя имя бэкэнда, набор его возможностей, обработчики различных низкоуровневых операций USB в виде указателей на функции и, наконец, размеры, которые необходимо выделить для хранения частного устройства/контекста. -/данные уровня передачи.

Поля частных данных полезны, по крайней мере, для хранения дескрипторов ОС для всех этих вещей, поскольку без дескрипторов мы не знаем, к какому элементу применяется та или иная операция. В веб-реализации дескрипторы ОС будут базовыми объектами JavaScript WebUSB. Естественный способ представления и хранения их в Emscripten — через класс emscripten::val , который предоставляется как часть Embind (системы привязок Emscripten).

Большинство серверных частей в папке реализованы на C, но некоторые реализованы на C++. Embind работает только с C++, поэтому выбор был сделан за меня, и я добавил libusb/libusb/os/emscripten_webusb.cpp с необходимой структурой и с sizeof(val) для полей личных данных:

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

Хранение объектов WebUSB в качестве дескрипторов устройств

libusb предоставляет готовые к использованию указатели на выделенную область для личных данных. Чтобы работать с этими указателями как с экземплярами val , я добавил небольшие помощники, которые конструируют их на месте, извлекают их как ссылки и перемещают значения:

// 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))) {}
};

Асинхронные веб-API в синхронных контекстах C

Теперь нужен способ обработки асинхронных API WebUSB, где libusb ожидает синхронных операций. Для этого я мог бы использовать Asyncify или, точнее, его интеграцию с Embind через val::await() .

Я также хотел правильно обрабатывать ошибки WebUSB и преобразовывать их в коды ошибок libusb, но в настоящее время у Embind нет способа обрабатывать исключения JavaScript или отклонения Promise со стороны C++. Эту проблему можно обойти, перехватив отказ на стороне JavaScript и преобразовав результат в объект { error, value } который теперь можно безопасно проанализировать со стороны C++. Я сделал это с помощью комбинации макроса EM_JS и API 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()};
 
}
};

Теперь я мог использовать promise_result::await() для любого Promise возвращаемого из операций WebUSB, и проверять его поля error и value отдельно.

Например, получение значения val представляющего USBDevice из libusb_device_handle , вызов его метода open() , ожидание его результата и возврат кода ошибки в виде кода состояния libusb выглядит следующим образом:

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

Перечисление устройств

Конечно, прежде чем я смогу открыть какое-либо устройство, libusb необходимо получить список доступных устройств. Бэкэнд должен реализовать эту операцию через обработчик get_device_list .

Сложность в том, что, в отличие от других платформ, в целях безопасности нет возможности перечислить все подключенные USB-устройства в сети. Вместо этого поток разделяется на две части. Сначала веб-приложение запрашивает устройства с определенными свойствами через navigator.usb.requestDevice() , и пользователь вручную выбирает, какое устройство он хочет предоставить, или отклоняет запрос разрешения. После этого приложение выводит список уже одобренных и подключенных устройств через navigator.usb.getDevices() .

Сначала я пытался использовать requestDevice() непосредственно в реализации обработчика get_device_list . Однако отображение запроса разрешения со списком подключенных устройств считается конфиденциальной операцией и должно запускаться при взаимодействии с пользователем (например, при нажатии кнопки на странице), в противном случае оно всегда возвращает отклоненное обещание. Приложения libusb часто могут захотеть получить список подключенных устройств при запуске приложения, поэтому использование requestDevice() было невозможным.

Вместо этого мне пришлось оставить вызов navigator.usb.requestDevice() конечному разработчику и предоставлять доступ только к уже одобренным устройствам из 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;
}

Большая часть внутреннего кода использует val и promise_result аналогично тому, как уже было показано выше. В коде обработки передачи данных есть еще несколько интересных хаков, но эти детали реализации менее важны для целей этой статьи. Обязательно проверьте код и комментарии на Github, если вам интересно.

Перенос циклов событий в Интернет

Еще одна часть порта libusb, которую я хочу обсудить, — это обработка событий. Как описано в предыдущей статье, большинство API в системных языках, таких как C, являются синхронными, и обработка событий не является исключением. Обычно это реализуется посредством бесконечного цикла, который «опрашивает» (пытается прочитать данные или блокирует выполнение до тех пор, пока некоторые данные не станут доступны) из набора внешних источников ввода-вывода, и, когда хотя бы один из них отвечает, передает это как событие. соответствующему обработчику. После завершения работы обработчика элемент управления возвращается в цикл и приостанавливается для следующего опроса.

В Интернете есть несколько проблем с этим подходом.

Во-первых, WebUSB не предоставляет и не может предоставлять необработанные дескрипторы базовых устройств, поэтому их прямой опрос невозможен. Во-вторых, libusb использует API-интерфейсы eventfd и pipe для других событий, а также для обработки передач в операционных системах без необработанных дескрипторов устройств, но eventfd в настоящее время не поддерживается в Emscripten, а pipe , хотя и поддерживается, в настоящее время не соответствует спецификации и может Жду событий.

Наконец, самая большая проблема заключается в том, что в сети существует собственный цикл событий. Этот глобальный цикл событий используется для любых внешних операций ввода-вывода (включая fetch() , таймеры или, в данном случае, WebUSB) и вызывает обработчики событий или Promise всякий раз, когда соответствующие операции завершаются. Выполнение другого, вложенного, бесконечного цикла событий заблокирует дальнейшее выполнение цикла событий браузера, а это означает, что не только пользовательский интерфейс перестанет отвечать на запросы, но и код никогда не будет получать уведомления о тех же самых событиях ввода-вывода, которых он ожидает. Обычно это приводит к тупику, и то же самое произошло, когда я попытался использовать libusb в демо-версии. Страница замерла.

Как и в случае с другими блокирующими операциями ввода-вывода, для переноса таких циклов событий в Интернет разработчикам необходимо найти способ запускать эти циклы без блокировки основного потока. Один из способов — провести рефакторинг приложения для обработки событий ввода-вывода в отдельном потоке и передачи результатов обратно в основной поток. Другой вариант — использовать Asyncify для приостановки цикла и ожидания событий неблокирующим способом.

Я не хотел вносить существенные изменения ни в libusb, ни в gPhoto2, и я уже использовал Asyncify для интеграции Promise , поэтому я выбрал именно этот путь. Чтобы смоделировать вариант блокировки poll() , для первоначальной проверки концепции я использовал цикл, как показано ниже:

#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

Что он делает:

  1. Вызывает poll() чтобы проверить, сообщило ли бэкэнд о каких-либо событиях. Если они есть, цикл останавливается. В противном случае реализация poll() в Emscripten немедленно вернет значение 0 .
  2. Вызывает emscripten_sleep(0) . Эта функция использует Asyncify и setTimeout() под капотом и используется здесь для возврата управления основному циклу событий браузера. Это позволяет браузеру обрабатывать любые взаимодействия с пользователем и события ввода-вывода, включая WebUSB.
  3. Проверьте, истек ли еще указанный тайм-аут, и если нет, продолжите цикл.

Как упоминается в комментарии, этот подход не был оптимальным, поскольку он продолжал сохранять и восстанавливать весь стек вызовов с помощью Asyncify, даже когда еще не было событий USB для обработки (что происходит в большинстве случаев), и поскольку сам setTimeout() имеет минимальная продолжительность 4 мс в современных браузерах. Тем не менее, в ходе проверки концепции он работал достаточно хорошо, чтобы обеспечить прямую трансляцию с DSLR со скоростью 13–14 кадров в секунду.

Позже я решил улучшить его, используя систему событий браузера. Есть несколько способов дальнейшего улучшения этой реализации, но на данный момент я решил генерировать пользовательские события непосредственно в глобальном объекте, не связывая их с определенной структурой данных libusb. Я сделал это с помощью следующего механизма ожидания и уведомления, основанного на макросе 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);
 
}
});

Функция em_libusb_notify() используется всякий раз, когда libusb пытается сообщить о событии, например о завершении передачи данных:

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
}

Между тем, часть em_libusb_wait() используется для «пробуждения» из режима сна Asyncify, когда получено событие em-libusb или истекло время ожидания:

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

Благодаря значительному сокращению количества снов и пробуждений этот механизм устранил проблемы с эффективностью более ранней реализации на основе emscripten_sleep() и увеличил пропускную способность демо-версии DSLR с 13-14 кадров в секунду до стабильных 30+ кадров в секунду, что достаточно для плавного воспроизведения. живой корм.

Система сборки и первый тест

После того, как бэкэнд был готов, мне пришлось добавить его в Makefile.am и configure.ac . Единственный интересный момент здесь — это модификация флагов, специфичных для 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']"
 
;;

Во-первых, исполняемые файлы на платформах Unix обычно не имеют расширений файлов. Однако Emscripten выдает разные выходные данные в зависимости от того, какое расширение вы запрашиваете. Я использую AC_SUBST(EXEEXT, …) для изменения расширения исполняемого файла на .html , чтобы любой исполняемый файл в пакете — тесты и примеры — становился HTML с оболочкой Emscripten по умолчанию, которая отвечает за загрузку и создание экземпляров JavaScript и WebAssembly.

Во-вторых, поскольку я использую Embind и Asyncify, мне нужно включить эти функции ( --bind -s ASYNCIFY ), а также разрешить динамический рост памяти ( -s ALLOW_MEMORY_GROWTH ) через параметры компоновщика. К сожалению, библиотека не может сообщить об этих флагах компоновщику, поэтому каждое приложение, использующее этот порт libusb, также должно будет добавить те же флаги компоновщика в свою конфигурацию сборки.

Наконец, как упоминалось ранее, WebUSB требует, чтобы перечисление устройств выполнялось с помощью жестов пользователя. Примеры и тесты libusb предполагают, что они могут перечислять устройства при запуске и завершаться ошибкой без изменений. Вместо этого мне пришлось отключить автоматическое выполнение ( -s INVOKE_RUN=0 ) и предоставить ручной метод callMain() ( -s EXPORTED_RUNTIME_METHODS=... ).

Как только все это было сделано, я мог обслуживать сгенерированные файлы на статическом веб-сервере, инициализировать WebUSB и запускать эти исполняемые файлы HTML вручную с помощью DevTools.

Снимок экрана, показывающий окно Chrome с открытыми инструментами разработчика на локально обслуживаемой странице testlibusb. Консоль DevTools оценивает `navigator.usb.requestDevice({ filter: [] })`, что вызывает запрос на разрешение и в настоящее время просит пользователя выбрать USB-устройство, к которому следует предоставить общий доступ на странице. В данный момент выбрана ILCE-6600 (камера Sony).

Снимок экрана следующего шага: DevTools все еще открыт. После того, как устройство было выбрано, консоль вычислила новое выражение «Module.callMain(['-v'])», которое выполнило приложение «testlibusb» в подробном режиме. В выводе отображается различная подробная информация о ранее подключенной USB-камере: производитель Sony, продукт ILCE-6600, серийный номер, конфигурация и т. д.

Это выглядит не так уж и много, но при переносе библиотек на новую платформу добраться до этапа, когда они впервые выдают действительный результат, довольно интересно!

Использование порта

Как упоминалось выше , порт зависит от нескольких функций Emscripten, которые в настоящее время необходимо включить на этапе компоновки приложения. Если вы хотите использовать этот порт libusb в своем приложении, вот что вам нужно сделать:

  1. Загрузите последнюю версию libusb либо в виде архива как часть сборки, либо добавьте ее в качестве подмодуля git в свой проект.
  2. Запустите autoreconf -fiv в папке libusb .
  3. Запустите emconfigure ./configure –host=wasm32 –prefix=/some/installation/path чтобы инициализировать проект для кросс-компиляции и указать путь, по которому вы хотите поместить построенные артефакты.
  4. Запустите emmake make install .
  5. Направьте свое приложение или библиотеку более высокого уровня на поиск libusb по ранее выбранному пути.
  6. Добавьте следующие флаги в аргументы ссылки вашего приложения: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH .

В настоящее время библиотека имеет несколько ограничений:

  • Нет поддержки отмены перевода. Это ограничение WebUSB, которое, в свою очередь, связано с отсутствием отмены межплатформенной передачи в самой libusb.
  • Нет поддержки изохронной передачи. Добавить его не составит труда, следуя реализации существующих режимов передачи в качестве примеров, но это также довольно редкий режим, и у меня не было устройств, на которых можно было бы его протестировать, поэтому пока я оставил его как неподдерживаемый. Если у вас есть такие устройства и вы хотите внести свой вклад в библиотеку, пиар-сообщения приветствуются!
  • Ранее упомянутые кроссплатформенные ограничения . Эти ограничения накладываются операционными системами, поэтому мы мало что можем здесь сделать, кроме как попросить пользователей переопределить драйвер или разрешения. Однако, если вы портируете HID или последовательные устройства, вы можете последовать примеру libusb и портировать какую-нибудь другую библиотеку на другой API Fugu. Например, вы можете портировать hidapi библиотеки C на WebHID и вообще обойти проблемы, связанные с низкоуровневым доступом к USB.

Заключение

В этом посте я показал, как с помощью API Emscripten, Asyncify и Fugu даже низкоуровневые библиотеки, такие как libusb, можно портировать в Интернет с помощью нескольких приемов интеграции.

Портирование таких важных и широко используемых низкоуровневых библиотек особенно полезно, поскольку, в свою очередь, оно позволяет перенести в Интернет библиотеки более высокого уровня или даже целые приложения. Это открывает возможности, которые ранее были доступны только пользователям одной или двух платформ, всем типам устройств и операционных систем, делая эти возможности доступными всего лишь одним щелчком мыши.

В следующем посте я расскажу, как создать веб-демоверсию gPhoto2, которая не только извлекает информацию об устройстве, но и широко использует функцию передачи libusb. Между тем, я надеюсь, что пример libusb вас вдохновил, и вы опробуете демо-версию, поиграетесь с самой библиотекой или, возможно, даже пойдете дальше и портируете другую широко используемую библиотеку в один из API Fugu.