Узнайте, как перенести код, взаимодействующий с внешними устройствами, в веб-среду с помощью WebAssembly и API Project Fugu.
В предыдущей статье я рассказал, как перенести приложения, использующие API файловой системы, в веб с помощью File System Access API, WebAssembly и Asyncify. Теперь я хочу продолжить ту же тему интеграции Fugu API с WebAssembly и переноса приложений в Интернет без потери важных функций.
Я покажу, как перенести приложения, которые взаимодействуют с USB-устройствами, в веб. Для этого я перенесу libusb, популярную USB-библиотеку, написанную на C, в WebAssembly (с помощью Emscripten), Asyncify и WebUSB.
Сначала о главном: демонстрация
Самое важное при переносе библиотеки – выбрать правильную демоверсию, которая продемонстрирует возможности перенесенной библиотеки, позволит протестировать ее различными способами и в то же время будет визуально привлекательной.
Я выбрал идею пульта дистанционного управления для зеркальной камеры. В частности, проект с открытым исходным кодом gPhoto2 существует достаточно давно, чтобы в нем была реализована поддержка широкого спектра цифровых камер. Он поддерживает несколько протоколов, но больше всего меня интересовала поддержка USB, которая осуществляется через libusb.
Я опишу шаги по созданию этой демонстрации в двух частях. В этой записи блога я опишу, как я портировал саму libusb и какие уловки могут потребоваться для портирования других популярных библиотек в API Fugu. Во второй записи я подробно расскажу о переносе и интеграции gPhoto2.
В итоге у меня получилось работающее веб-приложение, которое показывает изображение с зеркальной камеры и позволяет управлять ее настройками через USB. Прежде чем переходить к техническим деталям, вы можете посмотреть демонстрацию в реальном времени или ее запись.
Особенности некоторых камер
Возможно, вы заметили, что изменение настроек в видео занимает некоторое время. Как и в большинстве других случаев, это связано не с производительностью WebAssembly или WebUSB, а с тем, как gPhoto2 взаимодействует с конкретной камерой, выбранной для демонстрации.
Sony a6600 не предоставляет API для непосредственной настройки таких значений, как ISO, диафрагма или выдержка, а вместо этого предоставляет только команды для их увеличения или уменьшения на указанное количество шагов. Кроме того, он не возвращает список поддерживаемых значений. Похоже, что возвращаемый список жестко закодирован для многих моделей камер Sony.
При установке одного из этих значений у gPhoto2 нет другого выбора, кроме как:
- Сделайте шаг (или несколько) в направлении выбранной ценности.
- Подождите немного, пока настройки камеры обновятся.
- Прочитайте значение, на котором остановилась камера.
- Убедитесь, что на последнем шаге вы не пропустили нужное значение и не перешли в конец или начало списка.
- с единомышленниками.
Это может занять некоторое время, но если значение действительно поддерживается камерой, оно будет достигнуто, а если нет, то остановится на ближайшем допустимом значении.
У других камер, скорее всего, будут другие наборы настроек, базовые API и особенности. Помните, что gPhoto2 – это проект с открытым исходным кодом, и автоматическое или ручное тестирование всех моделей камер просто невозможно, поэтому подробные отчеты о проблемах и запросы на включение изменений всегда приветствуются (но сначала обязательно воспроизведите проблемы с официальным клиентом gPhoto2).
Важные примечания о кросс-платформенной совместимости
К сожалению, в Windows всем известным устройствам, включая цифровые зеркальные камеры, назначается системный драйвер, несовместимый с WebUSB. Если вы хотите попробовать демоверсию на Windows, вам придется использовать такой инструмент, как Zadig, чтобы переопределить драйвер для подключенной цифровой зеркальной камеры на WinUSB или libusb. Этот подход хорошо работает для меня и многих других пользователей, но вы должны использовать его на свой страх и риск.
На Linux, скорее всего, потребуется настроить специальные разрешения, чтобы разрешить доступ к цифровой зеркальной камере через WebUSB. Это зависит от дистрибутива.
На устройствах macOS и Android демоверсия должна работать без дополнительных настроек. Если вы используете телефон Android, переключитесь в горизонтальный режим, так как я не уделил много внимания адаптивности (буду рад вашим предложениям по улучшению).
Подробное руководство по использованию WebUSB на разных платформах можно найти в разделе Platform-specific considerations (Особенности разных платформ) статьи Building a device for WebUSB (Создание устройства для WebUSB).
Добавление нового бэкенда в libusb
Теперь перейдем к техническим деталям. Хотя можно предоставить API-интерфейс shim, аналогичный 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, являются синхронными, и обработка событий не является исключением. Обычно он реализуется через бесконечный цикл, который опрашивает (пытается прочитать данные или блокирует выполнение, пока не появятся данные) набор внешних источников I/O и, когда хотя бы один из них отвечает, передает это как событие соответствующему обработчику. После завершения работы обработчика управление возвращается в цикл, и он приостанавливается для следующего опроса.
Однако в интернете у такого подхода есть несколько недостатков.
Во-первых, WebUSB не предоставляет и не может предоставлять необработанные дескрипторы базовых устройств, поэтому опрос их напрямую невозможен. Во-вторых, libusb использует API eventfd и pipe для других событий, а также для обработки передач в операционных системах без необработанных дескрипторов устройств, но eventfd в настоящее время не поддерживается в Emscripten, а pipe, хотя и поддерживается, в настоящее время не соответствует спецификации и не может ждать событий.
Наконец, самая большая проблема заключается в том, что у веб-страниц есть собственный цикл событий. Этот глобальный цикл событий используется для любых внешних операций I/O (включая fetch(), таймеры или, в данном случае, WebUSB) и вызывает обработчики событий или Promise, когда соответствующие операции завершаются. Выполнение другого, вложенного, бесконечного цикла событий заблокирует цикл событий браузера, что приведет не только к зависанию интерфейса, но и к тому, что код никогда не получит уведомления о тех самых событиях I/O, которых он ожидает. Обычно это приводит к взаимоблокировке, что и произошло, когда я попытался использовать libusb в демоверсии. Страница зависла.
Как и в случае с другими блокирующими операциями I/O, чтобы перенести такие циклы событий в веб, разработчикам необходимо найти способ запускать эти циклы, не блокируя основной поток. Например, можно провести рефакторинг приложения, чтобы события I/O обрабатывались в отдельном потоке, а результаты передавались обратно в основной. Другой способ – использовать 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
Вот что она делает:
- Вызывает
poll(), чтобы проверить, были ли уже зарегистрированы какие-либо события. Если они есть, цикл останавливается. В противном случае реализацияpoll()в Emscripten сразу вернет0. - Звонки
emscripten_sleep(0). Эта функция использует Asyncify иsetTimeout(), чтобы вернуть управление основному циклу событий браузера. Это позволяет браузеру обрабатывать любые взаимодействия с пользователем и события ввода-вывода, включая WebUSB. - Проверьте, истекло ли указанное время ожидания. Если нет, продолжите цикл.
Как упоминается в комментарии, этот подход не был оптимальным, поскольку он продолжал сохранять и восстанавливать весь стек вызовов с помощью Asyncify, даже когда ещё не было событий USB для обработки (что происходит большую часть времени), и потому что setTimeout() сам по себе имеет минимальную продолжительность 4 мс в современных браузерах. Тем не менее, этого было достаточно, чтобы получить 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-файлы с помощью Инструментов разработчика.
![Скриншот окна Chrome с открытыми инструментами разработчика на локальной странице testlibusb. Консоль DevTools оценивает `navigator.usb.requestDevice({ filters: [] })`, что вызвало запрос разрешения и в настоящее время просит пользователя выбрать USB-устройство, которое должно быть доступно для страницы. Сейчас выбрана камера ILCE-6600 (Sony).](https://web.developers.google.cn/static/articles/porting-libusb-to-webusb/image/screenshot-showing-chrom-b19a9e4e1b916.png?hl=ru)
![Скриншот следующего шага с открытыми инструментами разработчика. После выбора устройства консоль оценила новое выражение Module.callMain(['-v']), которое запустило приложение testlibusb в подробном режиме. В выходных данных содержится подробная информация о ранее подключенной USB-камере: производитель Sony, продукт ILCE-6600, серийный номер, конфигурация и т. д.](https://web.developers.google.cn/static/articles/porting-libusb-to-webusb/image/screenshot-the-next-step-bd4e4c7f85be7.png?hl=ru)
Это не так уж много, но когда вы переносите библиотеки на новую платформу, то достижение этапа, на котором она впервые выдает действительный результат, довольно увлекательно!
Как использовать порт
Как упоминалось выше, порт зависит от нескольких функций Emscripten, которые в настоящее время необходимо включить на этапе связывания приложения. Чтобы использовать этот порт libusb в своем приложении, выполните следующие действия:
- Скачайте последнюю версию libusb в виде архива в составе сборки или добавьте ее в качестве подмодуля git в проект.
- Запустите файл
autoreconf -fivв папкеlibusb. - Выполните команду
emconfigure ./configure –host=wasm32 –prefix=/some/installation/path, чтобы инициализировать проект для кросс-компиляции и задать путь, по которому будут размещаться созданные артефакты. - Выполните команду
emmake make install. - Укажите в приложении или библиотеке более высокого уровня путь к libusb, выбранный ранее.
- Добавьте в аргументы ссылки приложения следующие флаги:
--bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.
В настоящее время у библиотеки есть несколько ограничений:
- Отмена переноса не поддерживается. Это ограничение WebUSB, которое, в свою очередь, связано с отсутствием кроссплатформенной отмены передачи в libusb.
- Изохронная передача не поддерживается. Добавить его несложно, если использовать в качестве примеров существующие режимы передачи, но он довольно редкий, и у меня не было устройств, на которых можно было бы его протестировать, поэтому пока я оставил его неподдерживаемым. Если у вас есть такие устройства и вы хотите внести свой вклад в библиотеку, мы будем рады вашим запросам на включение кода.
- Ранее упомянутые межплатформенные ограничения. Эти ограничения накладываются операционными системами, поэтому мы мало что можем сделать, кроме как попросить пользователей переопределить драйвер или разрешения. Однако если вы переносите HID-устройства или устройства с последовательным интерфейсом, вы можете последовать примеру libusb и перенести другую библиотеку в другой Fugu API. Например, вы можете перенести библиотеку C hidapi в WebHID и полностью обойти проблемы, связанные с низкоуровневым доступом к USB.
Заключение
В этой статье я показал, как с помощью Emscripten, Asyncify и Fugu API даже низкоуровневые библиотеки, такие как libusb, можно перенести в Интернет с помощью нескольких приемов интеграции.
Перенос таких важных и широко используемых низкоуровневых библиотек особенно полезен, поскольку, в свою очередь, он позволяет переносить в Интернет и библиотеки более высокого уровня или даже целые приложения. Это открывает возможности, которые ранее были ограничены пользователями одной или двух платформ, для всех видов устройств и операционных систем, делая эти возможности доступными всего лишь по щелчку ссылки.
В следующей статье я расскажу, как создать демоверсию gPhoto2 для интернета, которая не только получает информацию об устройстве, но и активно использует функцию передачи libusb. Надеюсь, пример с libusb вдохновил вас и вы попробуете демоверсию, поработаете с библиотекой или даже перенесете другую популярную библиотеку в один из API Fugu.