summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIngvar Stepanyan <rreverser@google.com>2020-09-16 20:05:51 +0100
committerTormod Volden <debian.tormod@gmail.com>2022-06-26 20:44:54 +0200
commitff8fe9397d4c61f072d209703f854c9f62ed17e3 (patch)
tree988089c29a668f19ff7a0e49800befd66010177b
parent4689bd50d03fa436ea552ec3937b0b5fbdaff90c (diff)
downloadlibusb-ff8fe9397d4c61f072d209703f854c9f62ed17e3.tar.gz
Add Emscripten backend for WebAssembly + WebUSB support
Fixes #501 Closes #1057
-rw-r--r--.gitignore3
-rw-r--r--README3
-rw-r--r--configure.ac18
-rw-r--r--libusb/Makefile.am7
-rw-r--r--libusb/os/emscripten_webusb.cpp626
-rw-r--r--libusb/os/events_posix.c49
-rw-r--r--libusb/version_nano.h2
7 files changed, 705 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore
index 343a446..c4988ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,9 @@ Makefile.in
*.la
*.lo
*.o
+*.js
+*.wasm
+*.html
libtool
ltmain.sh
missing
diff --git a/README b/README
index a47b4bd..bf24733 100644
--- a/README
+++ b/README
@@ -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