diff options
author | Ingvar Stepanyan <rreverser@google.com> | 2020-09-16 20:05:51 +0100 |
---|---|---|
committer | Tormod Volden <debian.tormod@gmail.com> | 2022-06-26 20:44:54 +0200 |
commit | ff8fe9397d4c61f072d209703f854c9f62ed17e3 (patch) | |
tree | 988089c29a668f19ff7a0e49800befd66010177b | |
parent | 4689bd50d03fa436ea552ec3937b0b5fbdaff90c (diff) | |
download | libusb-ff8fe9397d4c61f072d209703f854c9f62ed17e3.tar.gz |
Add Emscripten backend for WebAssembly + WebUSB support
Fixes #501
Closes #1057
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | README | 3 | ||||
-rw-r--r-- | configure.ac | 18 | ||||
-rw-r--r-- | libusb/Makefile.am | 7 | ||||
-rw-r--r-- | libusb/os/emscripten_webusb.cpp | 626 | ||||
-rw-r--r-- | libusb/os/events_posix.c | 49 | ||||
-rw-r--r-- | libusb/version_nano.h | 2 |
7 files changed, 705 insertions, 3 deletions
@@ -6,6 +6,9 @@ Makefile.in *.la *.lo *.o +*.js +*.wasm +*.html libtool ltmain.sh missing @@ -5,7 +5,8 @@ [![Coverity Scan Build Status](https://scan.coverity.com/projects/2180/badge.svg)](https://scan.coverity.com/projects/libusb-libusb) libusb is a library for USB device access from Linux, macOS, -Windows, OpenBSD/NetBSD, Haiku and Solaris userspace. +Windows, OpenBSD/NetBSD, Haiku, Solaris userspace, and WebAssembly +via WebUSB. It is written in C (Haiku backend in C++) and licensed under the GNU Lesser General Public License version 2.1 or, at your option, any later version (see [COPYING](COPYING)). diff --git a/configure.ac b/configure.ac index bf423d1..e05faf4 100644 --- a/configure.ac +++ b/configure.ac @@ -83,6 +83,11 @@ case $host in backend=haiku platform=posix ;; +wasm32-**) + AC_MSG_RESULT([Emscripten]) + backend=emscripten + platform=posix + ;; *-linux* | *-uclinux*) dnl on Android Linux, some functions are in different places case $host in @@ -138,7 +143,11 @@ esac if test "x$platform" = xposix; then AC_DEFINE([PLATFORM_POSIX], [1], [Define to 1 if compiling for a POSIX platform.]) AC_CHECK_TYPES([nfds_t], [], [], [[#include <poll.h>]]) - AC_CHECK_FUNCS([pipe2]) + if test "x$backend" != xemscripten; then + # pipe2 is detected as present on Emscripten, but it isn't actually ported and always + # returns an error. https://github.com/emscripten-core/emscripten/issues/14824 + AC_CHECK_FUNCS([pipe2]) + fi dnl Some compilers do not support the '-pthread' option so check for it here saved_CFLAGS="${CFLAGS}" CFLAGS="-Wall -Werror -pthread" @@ -217,6 +226,11 @@ windows) AC_DEFINE([_WIN32_WINNT], [_WIN32_WINNT_VISTA], [Define to the oldest supported Windows version.]) LT_LDFLAGS="${LT_LDFLAGS} -avoid-version -Wl,--add-stdcall-alias" ;; +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']" + ;; *) dnl no special handling required ;; @@ -372,6 +386,7 @@ AM_CONDITIONAL([OS_NULL], [test "x$backend" = xnull]) AM_CONDITIONAL([OS_OPENBSD], [test "x$backend" = xopenbsd]) AM_CONDITIONAL([OS_SUNOS], [test "x$backend" = xsunos]) AM_CONDITIONAL([OS_WINDOWS], [test "x$backend" = xwindows]) +AM_CONDITIONAL([OS_EMSCRIPTEN], [test "x$backend" = xemscripten]) AM_CONDITIONAL([PLATFORM_POSIX], [test "x$platform" = xposix]) AM_CONDITIONAL([PLATFORM_WINDOWS], [test "x$platform" = xwindows]) AM_CONDITIONAL([USE_UDEV], [test "x$use_udev" = xyes]) @@ -399,6 +414,7 @@ AM_CXXFLAGS="-std=${c_dialect}++11 ${EXTRA_CFLAGS} ${SHARED_CFLAGS} -Wmissing-de AC_SUBST(AM_CXXFLAGS) AC_SUBST(LT_LDFLAGS) +AC_SUBST(AM_LDFLAGS) AC_SUBST([EXTRA_LDFLAGS]) diff --git a/libusb/Makefile.am b/libusb/Makefile.am index 3475c9a..30d3547 100644 --- a/libusb/Makefile.am +++ b/libusb/Makefile.am @@ -20,6 +20,7 @@ OS_DARWIN_SRC = os/darwin_usb.h os/darwin_usb.c OS_HAIKU_SRC = os/haiku_usb.h os/haiku_usb_backend.cpp \ os/haiku_pollfs.cpp os/haiku_usb_raw.h os/haiku_usb_raw.cpp OS_LINUX_SRC = os/linux_usbfs.h os/linux_usbfs.c +OS_EMSCRIPTEN_SRC = os/emscripten_webusb.cpp OS_NETBSD_SRC = os/netbsd_usb.c OS_NULL_SRC = os/null_usb.c OS_OPENBSD_SRC = os/openbsd_usb.c @@ -48,6 +49,12 @@ OS_SRC += os/linux_netlink.c endif endif +if OS_EMSCRIPTEN +noinst_LTLIBRARIES = libusb_emscripten.la +libusb_emscripten_la_SOURCES = $(OS_EMSCRIPTEN_SRC) +libusb_1_0_la_LIBADD = libusb_emscripten.la +endif + if OS_NETBSD OS_SRC = $(OS_NETBSD_SRC) endif diff --git a/libusb/os/emscripten_webusb.cpp b/libusb/os/emscripten_webusb.cpp new file mode 100644 index 0000000..325a3a1 --- /dev/null +++ b/libusb/os/emscripten_webusb.cpp @@ -0,0 +1,626 @@ +/* + * Copyright © 2021 Google LLC + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Authors: + * Ingvar Stepanyan <me@rreverser.com> + */ + +#include <emscripten.h> +#include <emscripten/val.h> + +#include "libusbi.h" + +using namespace emscripten; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wmissing-prototypes" +#pragma clang diagnostic ignored "-Wunused-parameter" +namespace { +// clang-format off + 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, + // LIBUSB_ERROR_NOT_FOUND + NotFoundError : -5, + // LIBUSB_ERROR_BUSY + InvalidStateError : -6, + // LIBUSB_ERROR_TIMEOUT + TimeoutError : -7, + // LIBUSB_ERROR_INTERRUPTED + AbortError : -10, + // LIBUSB_ERROR_NOT_SUPPORTED + NotSupportedError : -12, + }; + 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); + }); +// clang-format on + +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()}; + } +}; + +// 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))) {} +}; + +void em_signal_transfer_completion_impl(usbi_transfer *itransfer, + val &&result) { + WebUsbTransferPtr(itransfer).init_to(std::move(result)); + usbi_signal_transfer_completion(itransfer); +} + +// Store the global `navigator.usb` once upon initialisation. +thread_local const val web_usb = val::global("navigator")["usb"]; + +enum StringId : uint8_t { + Manufacturer = 1, + Product = 2, + SerialNumber = 3, +}; + +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]; + auto vendor_id = web_usb_device["vendorId"].as<uint16_t>(); + auto product_id = web_usb_device["productId"].as<uint16_t>(); + // TODO: this has to be a unique ID for the device in libusb structs. + // We can't really rely on the index in the list, and otherwise + // I can't think of a good way to assign permanent IDs to those + // devices, so here goes best-effort attempt... + unsigned long session_id = (vendor_id << 16) | product_id; + // LibUSB uses that ID to check if this device is already in its own + // list. As long as there are no two instances of same device + // connected and exposed to the page, we should be fine... + auto dev = usbi_get_device_by_session_id(ctx, session_id); + if (dev == NULL) { + dev = usbi_alloc_device(ctx, session_id); + if (dev == NULL) { + usbi_err(ctx, "failed to allocate a new device structure"); + continue; + } + + dev->device_descriptor = { + .bLength = LIBUSB_DT_DEVICE_SIZE, + .bDescriptorType = LIBUSB_DT_DEVICE, + .bcdUSB = static_cast<uint16_t>( + (web_usb_device["usbVersionMajor"].as<uint8_t>() << 8) | + (web_usb_device["usbVersionMinor"].as<uint8_t>() << 4) | + web_usb_device["usbVersionSubminor"].as<uint8_t>()), + .bDeviceClass = web_usb_device["deviceClass"].as<uint8_t>(), + .bDeviceSubClass = web_usb_device["deviceSubclass"].as<uint8_t>(), + .bDeviceProtocol = web_usb_device["deviceProtocol"].as<uint8_t>(), + .bMaxPacketSize0 = 64, // yolo + .idVendor = vendor_id, + .idProduct = product_id, + .bcdDevice = static_cast<uint16_t>( + (web_usb_device["deviceVersionMajor"].as<uint8_t>() << 8) | + (web_usb_device["deviceVersionMinor"].as<uint8_t>() << 4) | + web_usb_device["deviceVersionSubminor"].as<uint8_t>()), + // Those are supposed to be indices for USB string descriptors. + // Normally they're part of the raw USB descriptor structure, but in + // our case we don't have it. Luckily, libusb provides hooks for that + // (to accomodate for other systems in similar position) so we can + // just assign constant IDs we can recognise later and then handle + // them in `em_submit_transfer` when there is a request to get string + // descriptor value. + .iManufacturer = StringId::Manufacturer, + .iProduct = StringId::Product, + .iSerialNumber = StringId::SerialNumber, + .bNumConfigurations = + web_usb_device["configurations"]["length"].as<uint8_t>(), + }; + + if (usbi_sanitize_device(dev) < 0) { + libusb_unref_device(dev); + continue; + } + + WebUsbDevicePtr(dev).init_to(std::move(web_usb_device)); + } + *devs = discovered_devs_append(*devs, dev); + } + return LIBUSB_SUCCESS; +} + +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; +} + +void em_close(libusb_device_handle *handle) { + // LibUSB API doesn't allow us to handle an error here, so ignore the Promise + // altogether. + return get_web_usb_device(handle->dev).call<void>("close"); +} + +int em_get_config_descriptor_impl(val &&web_usb_config, void *buf, size_t len) { + const auto buf_start = static_cast<uint8_t *>(buf); + auto web_usb_interfaces = web_usb_config["interfaces"]; + auto num_interfaces = web_usb_interfaces["length"].as<uint8_t>(); + auto config = static_cast<usbi_configuration_descriptor *>(buf); + *config = { + .bLength = LIBUSB_DT_CONFIG_SIZE, + .bDescriptorType = LIBUSB_DT_CONFIG, + .wTotalLength = LIBUSB_DT_CONFIG_SIZE, + .bNumInterfaces = num_interfaces, + .bConfigurationValue = web_usb_config["configurationValue"].as<uint8_t>(), + .iConfiguration = + 0, // TODO: assign some index and handle `configurationName` + .bmAttributes = + 1 << 7, // bus powered (should be always set according to docs) + .bMaxPower = 0, // yolo + }; + buf = static_cast<uint8_t *>(buf) + LIBUSB_DT_CONFIG_SIZE; + for (uint8_t i = 0; i < num_interfaces; i++) { + auto web_usb_interface = web_usb_interfaces[i]; + // TODO: update to `web_usb_interface["alternate"]` once + // fix for https://bugs.chromium.org/p/chromium/issues/detail?id=1093502 is + // stable. + auto web_usb_alternate = web_usb_interface["alternates"][0]; + auto web_usb_endpoints = web_usb_alternate["endpoints"]; + auto num_endpoints = web_usb_endpoints["length"].as<uint8_t>(); + config->wTotalLength += + LIBUSB_DT_INTERFACE_SIZE + num_endpoints * LIBUSB_DT_ENDPOINT_SIZE; + if (config->wTotalLength > len) { + continue; + } + auto interface = static_cast<usbi_interface_descriptor *>(buf); + *interface = { + .bLength = LIBUSB_DT_INTERFACE_SIZE, + .bDescriptorType = LIBUSB_DT_INTERFACE, + .bInterfaceNumber = web_usb_interface["interfaceNumber"].as<uint8_t>(), + .bAlternateSetting = + web_usb_alternate["alternateSetting"].as<uint8_t>(), + .bNumEndpoints = web_usb_endpoints["length"].as<uint8_t>(), + .bInterfaceClass = web_usb_alternate["interfaceClass"].as<uint8_t>(), + .bInterfaceSubClass = + web_usb_alternate["interfaceSubclass"].as<uint8_t>(), + .bInterfaceProtocol = + web_usb_alternate["interfaceProtocol"].as<uint8_t>(), + .iInterface = 0, // Not exposed in WebUSB, don't assign any string. + }; + buf = static_cast<uint8_t *>(buf) + LIBUSB_DT_INTERFACE_SIZE; + for (uint8_t j = 0; j < num_endpoints; j++) { + auto web_usb_endpoint = web_usb_endpoints[j]; + auto endpoint = static_cast<libusb_endpoint_descriptor *>(buf); + + auto web_usb_endpoint_type = web_usb_endpoint["type"].as<std::string>(); + auto transfer_type = LIBUSB_ENDPOINT_TRANSFER_TYPE_CONTROL; + + if (web_usb_endpoint_type == "bulk") { + transfer_type = LIBUSB_ENDPOINT_TRANSFER_TYPE_BULK; + } else if (web_usb_endpoint_type == "interrupt") { + transfer_type = LIBUSB_ENDPOINT_TRANSFER_TYPE_INTERRUPT; + } else if (web_usb_endpoint_type == "isochronous") { + transfer_type = LIBUSB_ENDPOINT_TRANSFER_TYPE_ISOCHRONOUS; + } + + // Can't use struct-init syntax here because there is no + // `usbi_endpoint_descriptor` unlike for other descriptors, so we use + // `libusb_endpoint_descriptor` instead which has extra libusb-specific + // fields and might overflow the provided buffer. + endpoint->bLength = LIBUSB_DT_ENDPOINT_SIZE; + endpoint->bDescriptorType = LIBUSB_DT_ENDPOINT; + endpoint->bEndpointAddress = + ((web_usb_endpoint["direction"].as<std::string>() == "in") << 7) | + web_usb_endpoint["endpointNumber"].as<uint8_t>(); + endpoint->bmAttributes = transfer_type; + endpoint->wMaxPacketSize = web_usb_endpoint["packetSize"].as<uint16_t>(); + endpoint->bInterval = 1; + + buf = static_cast<uint8_t *>(buf) + LIBUSB_DT_ENDPOINT_SIZE; + } + } + return static_cast<uint8_t *>(buf) - buf_start; +} + +int em_get_active_config_descriptor(libusb_device *dev, void *buf, size_t len) { + auto web_usb_config = get_web_usb_device(dev)["configuration"]; + if (web_usb_config.isNull()) { + return LIBUSB_ERROR_NOT_FOUND; + } + return em_get_config_descriptor_impl(std::move(web_usb_config), buf, len); +} + +int em_get_config_descriptor(libusb_device *dev, uint8_t idx, void *buf, + size_t len) { + return em_get_config_descriptor_impl( + get_web_usb_device(dev)["configurations"][idx], buf, len); +} + +int em_get_configuration(libusb_device_handle *dev_handle, uint8_t *config) { + auto web_usb_config = get_web_usb_device(dev_handle->dev)["configuration"]; + if (!web_usb_config.isNull()) { + *config = web_usb_config["configurationValue"].as<uint8_t>(); + } + return LIBUSB_SUCCESS; +} + +int em_set_configuration(libusb_device_handle *handle, int config) { + return promise_result::await(get_web_usb_device(handle->dev) + .call<val>("selectConfiguration", config)) + .error; +} + +int em_claim_interface(libusb_device_handle *handle, uint8_t iface) { + return promise_result::await( + get_web_usb_device(handle->dev).call<val>("claimInterface", iface)) + .error; +} + +int em_release_interface(libusb_device_handle *handle, uint8_t iface) { + return promise_result::await(get_web_usb_device(handle->dev) + .call<val>("releaseInterface", iface)) + .error; +} + +int em_set_interface_altsetting(libusb_device_handle *handle, uint8_t iface, + uint8_t altsetting) { + return promise_result::await( + get_web_usb_device(handle->dev) + .call<val>("selectAlternateInterface", iface, altsetting)) + .error; +} + +int em_clear_halt(libusb_device_handle *handle, unsigned char endpoint) { + std::string direction = endpoint & LIBUSB_ENDPOINT_IN ? "in" : "out"; + endpoint &= LIBUSB_ENDPOINT_ADDRESS_MASK; + + return promise_result::await(get_web_usb_device(handle->dev) + .call<val>("clearHalt", direction, endpoint)) + .error; +} + +int em_reset_device(libusb_device_handle *handle) { + return promise_result::await( + get_web_usb_device(handle->dev).call<val>("reset")) + .error; +} + +void em_destroy_device(libusb_device *dev) { WebUsbDevicePtr(dev).take(); } + +thread_local const val Uint8Array = val::global("Uint8Array"); + +EMSCRIPTEN_KEEPALIVE +extern "C" void em_signal_transfer_completion(usbi_transfer *itransfer, + EM_VAL result_handle) { + em_signal_transfer_completion_impl(itransfer, + val::take_ownership(result_handle)); +} + +// clang-format off +EM_JS(void, em_start_transfer_impl, (usbi_transfer *transfer, EM_VAL handle), { + // Right now the handle value should be a `Promise<{value, error}>`. + // Subscribe to its result to unwrap the promise to `{value, error}` + // and signal transfer completion. + // Catch the error to transform promise of `value` into promise of `{value, + // error}`. + Emval.toValue(handle).then(result => { + _em_signal_transfer_completion(transfer, Emval.toHandle(result)); + }); +}); +// clang-format on + +void em_start_transfer(usbi_transfer *itransfer, val &&promise) { + promise = em_promise_catch(std::move(promise)); + em_start_transfer_impl(itransfer, promise.as_handle()); +} + +int em_submit_transfer(usbi_transfer *itransfer) { + auto transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + auto web_usb_device = get_web_usb_device(transfer->dev_handle->dev); + switch (transfer->type) { + case LIBUSB_TRANSFER_TYPE_CONTROL: { + auto setup = libusb_control_transfer_get_setup(transfer); + auto web_usb_control_transfer_params = val::object(); + + const char *web_usb_request_type = "unknown"; + // See LIBUSB_REQ_TYPE in windows_winusb.h (or docs for `bmRequestType`). + switch (setup->bmRequestType & (0x03 << 5)) { + case LIBUSB_REQUEST_TYPE_STANDARD: + if (setup->bRequest == LIBUSB_REQUEST_GET_DESCRIPTOR && + setup->wValue >> 8 == LIBUSB_DT_STRING) { + // For string descriptors we provide custom implementation that + // doesn't require an actual transfer, but just retrieves the value + // from JS, stores that string handle as transfer data (instead of a + // Promise) and immediately signals completion. + const char *propName = nullptr; + switch (setup->wValue & 0xFF) { + case StringId::Manufacturer: + propName = "manufacturerName"; + break; + case StringId::Product: + propName = "productName"; + break; + case StringId::SerialNumber: + propName = "serialNumber"; + break; + } + if (propName != nullptr) { + val str = web_usb_device[propName]; + if (str.isNull()) { + str = val(""); + } + em_signal_transfer_completion_impl(itransfer, std::move(str)); + return LIBUSB_SUCCESS; + } + } + web_usb_request_type = "standard"; + break; + case LIBUSB_REQUEST_TYPE_CLASS: + web_usb_request_type = "class"; + break; + case LIBUSB_REQUEST_TYPE_VENDOR: + web_usb_request_type = "vendor"; + break; + } + web_usb_control_transfer_params.set("requestType", web_usb_request_type); + + const char *recipient = "other"; + switch (setup->bmRequestType & 0x0f) { + case LIBUSB_RECIPIENT_DEVICE: + recipient = "device"; + break; + case LIBUSB_RECIPIENT_INTERFACE: + recipient = "interface"; + break; + case LIBUSB_RECIPIENT_ENDPOINT: + recipient = "endpoint"; + break; + } + web_usb_control_transfer_params.set("recipient", recipient); + + web_usb_control_transfer_params.set("request", setup->bRequest); + web_usb_control_transfer_params.set("value", setup->wValue); + web_usb_control_transfer_params.set("index", setup->wIndex); + + if (setup->bmRequestType & LIBUSB_ENDPOINT_IN) { + em_start_transfer( + itransfer, + web_usb_device.call<val>("controlTransferIn", + std::move(web_usb_control_transfer_params), + setup->wLength)); + } else { + auto data = + val(typed_memory_view(setup->wLength, + libusb_control_transfer_get_data(transfer))) + .call<val>("slice"); + em_start_transfer( + itransfer, web_usb_device.call<val>( + "controlTransferOut", + std::move(web_usb_control_transfer_params), data)); + } + + break; + } + case LIBUSB_TRANSFER_TYPE_BULK: + case LIBUSB_TRANSFER_TYPE_INTERRUPT: { + auto endpoint = transfer->endpoint & LIBUSB_ENDPOINT_ADDRESS_MASK; + + if (IS_XFERIN(transfer)) { + em_start_transfer( + itransfer, + web_usb_device.call<val>("transferIn", endpoint, transfer->length)); + } else { + auto data = val(typed_memory_view(transfer->length, transfer->buffer)) + .call<val>("slice"); + em_start_transfer( + itransfer, web_usb_device.call<val>("transferOut", endpoint, data)); + } + + break; + } + // TODO: add implementation for isochronous transfers too. + default: + return LIBUSB_ERROR_NOT_SUPPORTED; + } + return LIBUSB_SUCCESS; +} + +void em_clear_transfer_priv(usbi_transfer *itransfer) { + WebUsbTransferPtr(itransfer).take(); +} + +int em_cancel_transfer(usbi_transfer *itransfer) { return LIBUSB_SUCCESS; } + +int em_handle_transfer_completion(usbi_transfer *itransfer) { + auto transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); + + // Take ownership of the transfer result, as `em_clear_transfer_priv` + // is not called automatically for completed transfers and we must + // free it to avoid leaks. + + auto result_val = WebUsbTransferPtr(itransfer).take(); + + if (itransfer->state_flags & USBI_TRANSFER_CANCELLING) { + return usbi_handle_transfer_cancellation(itransfer); + } + + libusb_transfer_status status = LIBUSB_TRANSFER_ERROR; + + // If this was a LIBUSB_DT_STRING request, then the value will be a string + // handle instead of a promise. + if (result_val.isString()) { + int written = EM_ASM_INT( + { + // There's no good way to get UTF-16 output directly from JS string, + // so again reach out to internals via JS snippet. + return stringToUTF16(Emval.toValue($0), $1, $2); + }, + result_val.as_handle(), + transfer->buffer + LIBUSB_CONTROL_SETUP_SIZE + 2, + transfer->length - LIBUSB_CONTROL_SETUP_SIZE - 2); + itransfer->transferred = transfer->buffer[LIBUSB_CONTROL_SETUP_SIZE] = + 2 + written; + transfer->buffer[LIBUSB_CONTROL_SETUP_SIZE + 1] = LIBUSB_DT_STRING; + status = LIBUSB_TRANSFER_COMPLETED; + } else { + // Otherwise we should have a `{value, error}` object by now (see + // `em_start_transfer_impl` callback). + promise_result result(std::move(result_val)); + + if (!result.error) { + auto web_usb_transfer_status = result.value["status"].as<std::string>(); + if (web_usb_transfer_status == "ok") { + status = LIBUSB_TRANSFER_COMPLETED; + } else if (web_usb_transfer_status == "stall") { + status = LIBUSB_TRANSFER_STALL; + } else if (web_usb_transfer_status == "babble") { + status = LIBUSB_TRANSFER_OVERFLOW; + } + + int skip; + unsigned char endpointDir; + + if (transfer->type == LIBUSB_TRANSFER_TYPE_CONTROL) { + skip = LIBUSB_CONTROL_SETUP_SIZE; + endpointDir = + libusb_control_transfer_get_setup(transfer)->bmRequestType; + } else { + skip = 0; + endpointDir = transfer->endpoint; + } + + if (endpointDir & LIBUSB_ENDPOINT_IN) { + auto data = result.value["data"]; + if (!data.isNull()) { + itransfer->transferred = data["byteLength"].as<int>(); + val(typed_memory_view(transfer->length - skip, + transfer->buffer + skip)) + .call<void>("set", Uint8Array.new_(data["buffer"])); + } + } else { + itransfer->transferred = result.value["bytesWritten"].as<int>(); + } + } + } + + return usbi_handle_transfer_completion(itransfer, status); +} +} // namespace + +extern "C" { +const usbi_os_backend usbi_backend = { + .name = "Emscripten + WebUSB backend", + .caps = LIBUSB_CAP_HAS_CAPABILITY, + .get_device_list = em_get_device_list, + .open = em_open, + .close = em_close, + .get_active_config_descriptor = em_get_active_config_descriptor, + .get_config_descriptor = em_get_config_descriptor, + .get_configuration = em_get_configuration, + .set_configuration = em_set_configuration, + .claim_interface = em_claim_interface, + .release_interface = em_release_interface, + .set_interface_altsetting = em_set_interface_altsetting, + .clear_halt = em_clear_halt, + .reset_device = em_reset_device, + .destroy_device = em_destroy_device, + .submit_transfer = em_submit_transfer, + .cancel_transfer = em_cancel_transfer, + .clear_transfer_priv = em_clear_transfer_priv, + .handle_transfer_completion = em_handle_transfer_completion, + .device_priv_size = sizeof(val), + .transfer_priv_size = sizeof(val), +}; +} +#pragma clang diagnostic pop diff --git a/libusb/os/events_posix.c b/libusb/os/events_posix.c index 715a2d5..2ba0103 100644 --- a/libusb/os/events_posix.c +++ b/libusb/os/events_posix.c @@ -28,6 +28,36 @@ #ifdef HAVE_TIMERFD #include <sys/timerfd.h> #endif + +#ifdef __EMSCRIPTEN__ +/* On Emscripten `pipe` does not conform to the spec and does not block + * until events are available, which makes it unusable for event system + * and often results in deadlocks when `pipe` is in a loop like it is + * in libusb. + * + * Therefore use a custom event system based on browser event emitters. */ +#include <emscripten.h> + +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); + } +}); +#endif #include <unistd.h> #ifdef HAVE_EVENTFD @@ -131,6 +161,9 @@ void usbi_signal_event(usbi_event_t *event) 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 } void usbi_clear_event(usbi_event_t *event) @@ -223,7 +256,23 @@ int usbi_wait_for_events(struct libusb_context *ctx, int internal_fds, num_ready; usbi_dbg(ctx, "poll() %u fds with timeout in %dms", (unsigned int)nfds, timeout_ms); +#ifdef __EMSCRIPTEN__ + /* TODO: improve event system to watch only for fd events we're interested in + * (although a scenario where we have multiple watchers in parallel is very rare + * in real world anyway). */ + 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; + } +#else num_ready = poll(fds, nfds, timeout_ms); +#endif usbi_dbg(ctx, "poll() returned %d", num_ready); if (num_ready == 0) { if (usbi_using_timer(ctx)) diff --git a/libusb/version_nano.h b/libusb/version_nano.h index 57cec9e..1c218ef 100644 --- a/libusb/version_nano.h +++ b/libusb/version_nano.h @@ -1 +1 @@ -#define LIBUSB_NANO 11741 +#define LIBUSB_NANO 11742 |