aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIngvar Stepanyan <me@rreverser.com>2023-11-03 15:09:04 +0000
committerTormod Volden <debian.tormod@gmail.com>2023-12-08 22:17:40 +0100
commit066a77fc0b5eaa5c23e0820081769637764e1ab0 (patch)
tree7eb8b53c996999dd6d05c815d300afeea960db79
parent1ca2bc14ced22aee4e8d16759f89a06690538759 (diff)
downloadlibusb-066a77fc0b5eaa5c23e0820081769637764e1ab0.tar.gz
webusb: Wasm+WebUSB backend fixes and improvements
- Added long-awaited support for multithreading. Since WebUSB still doesn't support accessing same device from multiple threads, this works by proxying I/O operations to the main thread as discussed on the original issue. For applications that run on the main thread nothing changes and they will continue to access WebUSB via Asyncify like before, while other threads will use blocking mechanism until an asynchronous response is available from the main thread. - Rewrote notification mechanism to use atomic waiting via sync Wasm instructions or `Atomics.waitAsync` depending on the thread. This results in simpler and faster notifications than the previous `postMessage`-based approach (which was used because `Atomics.waitAsync` wasn't yet available), as well as allows to send notifications across threads for multithreading scenario described above. - Fixed notification access to only wait/notify on the event we're interested instead of using a global lock. - Rewrote descriptor reading to query device for raw device & configuration descriptors instead of re-serializing them from JavaScript object representation. This incurs slight extra cost for the initial device opening, but fixes number of issues with information that is not yet exposed via WebUSB and allows to read supplementary descriptors such as string and BOS which were previously not supported. - Fixed listing only one alternate instead of all the available ones. - Fixed device closing & re-opening which could previously error out with "device busy" error. - Added mandatory Emscripten-specific linking flags to the generated pkgconfig. - Added device speed inference. This is not yet exposed via WebUSB, but we can at least make a best effort guess based on USB version and packet size, like some other backends already do. - Simplified & fixed device session ID generation which is now guaranteed to be truly unique, whereas previously it could clash with other devices of the same type. - Prepare code to support building for the Web without mandatory multithreading (which is costly on the Web) in the future. For now those `#ifdef`s are defunct as libusb is always compiled with `-pthread`, but some upcoming changes on the Emscripten side will allow to leverage those codepaths for smaller Wasm binaries. - Require explicit `--host=wasm32-unknown-emscripten` as we might want to add non-Emscripten WebAssembly backends in the future. - Various smaller fixes and improvements. Note that this requires Emscripten 3.1.48 or newer to build for some of the features used here, namely `co_await` support for JavaScript values (without it code would be both more complex and slower) and some proxying APIs. It shouldn't be a big deal in practice as most users retrieve Emscripten via the official emsdk installer or Docker images. Closes #1339
-rw-r--r--configure.ac12
-rw-r--r--libusb/Makefile.am1
-rw-r--r--libusb/os/emscripten_webusb.cpp1260
-rw-r--r--libusb/os/events_posix.c55
-rw-r--r--libusb/os/events_posix.h3
-rw-r--r--libusb/version_nano.h2
-rw-r--r--tests/stress_mt.c4
-rw-r--r--tests/webusb-test-shim/index.js8
8 files changed, 790 insertions, 555 deletions
diff --git a/configure.ac b/configure.ac
index 776fcb0..e8204f6 100644
--- a/configure.ac
+++ b/configure.ac
@@ -83,11 +83,17 @@ case $host in
backend=haiku
platform=posix
;;
-wasm32-**)
+wasm*-emscripten)
AC_MSG_RESULT([Emscripten])
backend=emscripten
platform=posix
;;
+wasm*-unknown-none)
+ AC_MSG_ERROR([
+--host=$host_alias is not accepted as it might become ambiguous in the future.
+Please use an explicit --host=$host_cpu-emscripten instead.
+ ])
+ ;;
*-linux* | *-uclinux*)
dnl on Android Linux, some functions are in different places
case $host in
@@ -228,7 +234,9 @@ windows)
;;
emscripten)
# 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"
+ EM_LDFLAGS="--bind -s ASYNCIFY"
+ AM_LDFLAGS="${AM_LDFLAGS} ${EM_LDFLAGS} -s ASSERTIONS -s ALLOW_MEMORY_GROWTH"
+ LIBS="${LIBS} ${EM_LDFLAGS}"
;;
*)
dnl no special handling required
diff --git a/libusb/Makefile.am b/libusb/Makefile.am
index 741fa9c..640edd8 100644
--- a/libusb/Makefile.am
+++ b/libusb/Makefile.am
@@ -54,6 +54,7 @@ endif
if OS_EMSCRIPTEN
noinst_LTLIBRARIES += libusb_emscripten.la
libusb_emscripten_la_SOURCES = $(OS_EMSCRIPTEN_SRC)
+AM_CXXFLAGS += -std=c++20
libusb_1_0_la_LIBADD = libusb_emscripten.la
endif
diff --git a/libusb/os/emscripten_webusb.cpp b/libusb/os/emscripten_webusb.cpp
index 325a3a1..f19c1bd 100644
--- a/libusb/os/emscripten_webusb.cpp
+++ b/libusb/os/emscripten_webusb.cpp
@@ -1,5 +1,6 @@
/*
* Copyright © 2021 Google LLC
+ * Copyright © 2023 Ingvar Stepanyan <me@rreverser.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@@ -19,23 +20,47 @@
* Ingvar Stepanyan <me@rreverser.com>
*/
+#include <emscripten/version.h>
+
+static_assert((__EMSCRIPTEN_major__ * 100 * 100 + __EMSCRIPTEN_minor__ * 100 +
+ __EMSCRIPTEN_tiny__) >= 30148,
+ "Emscripten 3.1.48 or newer is required.");
+
+#include <assert.h>
#include <emscripten.h>
#include <emscripten/val.h>
+#include <type_traits>
+#include <utility>
+
#include "libusbi.h"
using namespace emscripten;
+#ifdef _REENTRANT
+#include <emscripten/proxying.h>
+#include <emscripten/threading.h>
+#include <pthread.h>
+
+static ProxyingQueue queue;
+#endif
+
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmissing-prototypes"
#pragma clang diagnostic ignored "-Wunused-parameter"
+#pragma clang diagnostic ignored "-Wshadow"
+
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 => {
+EM_JS(EM_VAL, usbi_em_promise_catch, (EM_VAL handle), {
+ let promise = Emval.toValue(handle);
+ promise = promise.then(
+ value => ({error : 0, value}),
+ error => {
+ console.error(error);
+ let errorCode = -99; // LIBUSB_ERROR_OTHER
+ if (error instanceof DOMException) {
const ERROR_CODES = {
// LIBUSB_ERROR_IO
NetworkError : -1,
@@ -56,571 +81,790 @@ namespace {
// 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};
+ 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);
+});
+
+EM_JS(void, usbi_em_copy_from_dataview, (void* dst, EM_VAL src), {
+ src = Emval.toValue(src);
+ src = new Uint8Array(src.buffer, src.byteOffset, src.byteLength);
+ HEAPU8.set(src, dst);
+});
+
+// Our implementation proxies operations from multiple threads to the same
+// underlying USBDevice on the main thread. This can lead to issues when
+// multiple threads try to open/close the same device at the same time.
+//
+// First, since open/close operations are asynchronous in WebUSB, we can end up
+// with multiple open/close operations in flight at the same time, which can
+// lead to unpredictable outcome (e.g. device got closed but opening succeeded
+// right before that).
+//
+// Second, since multiple threads are allowed to have their own handles to the
+// same device, we need to keep track of number of open handles and close the
+// device only when the last handle is closed.
+//
+// We fix both of these issues by using a shared promise chain that executes
+// open and close operations sequentially and keeps track of the reference count
+// in each promise's result. This way, we can ensure that only one open/close
+// operation is in flight at any given time. Note that we don't need to worry
+// about all other operations because they're preconditioned on the device being
+// open and having at least 1 reference anyway.
+EM_JS(EM_VAL, usbi_em_device_safe_open_close, (EM_VAL device, bool open), {
+ device = Emval.toValue(device);
+ const symbol = Symbol.for('libusb.open_close_chain');
+ let promiseChain = device[symbol] ?? Promise.resolve(0);
+ device[symbol] = promiseChain = promiseChain.then(async refCount => {
+ if (open) {
+ if (!refCount++) {
+ await device.open();
}
- );
- return Emval.toHandle(promise);
+ } else {
+ if (!--refCount) {
+ await device.close();
+ }
+ }
+ return refCount;
});
+ return Emval.toHandle(promiseChain);
+});
// 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);
+libusb_transfer_status getTransferStatus(const val& transfer_result) {
+ auto status = transfer_result["status"].as<std::string>();
+ if (status == "ok") {
+ return LIBUSB_TRANSFER_COMPLETED;
+ } else if (status == "stall") {
+ return LIBUSB_TRANSFER_STALL;
+ } else if (status == "babble") {
+ return LIBUSB_TRANSFER_OVERFLOW;
+ } else {
+ return LIBUSB_TRANSFER_ERROR;
+ }
+}
+
+// Note: this assumes that `dst` is valid for at least `src.byteLength` bytes.
+// This is true for all results returned from WebUSB as we pass max length to
+// the transfer APIs.
+void copyFromDataView(void* dst, const val& src) {
+ usbi_em_copy_from_dataview(dst, src.as_handle());
+}
+
+auto getUnsharedMemoryView(void* src, size_t len) {
+ auto view = typed_memory_view(len, (uint8_t*)src);
+#ifdef _REENTRANT
+ // Unfortunately, TypedArrays backed by SharedArrayBuffers are not accepted
+ // by most Web APIs, trading off guaranteed thread-safety for performance
+ // loss. The usual workaround is to copy them into a new TypedArray, which
+ // is what we do here via the `.slice()` method.
+ return val(view).call<val>("slice");
+#else
+ // Non-threaded builds can avoid the copy penalty.
+ return view;
+#endif
+}
+
+// A helper that proxies a function call to the main thread if not already
+// there. This is a wrapper around Emscripten's raw proxying API with couple of
+// high-level improvements, namely support for destroying lambda on the target
+// thread as well as custom return types.
+template <typename Func>
+auto runOnMain(Func&& func) {
+#ifdef _REENTRANT
+ if (!emscripten_is_main_runtime_thread()) {
+ if constexpr (std::is_same_v<std::invoke_result_t<Func>, void>) {
+ bool proxied =
+ queue.proxySync(emscripten_main_runtime_thread_id(), [&func] {
+ // Capture func by reference and move into a local variable
+ // to render the captured func inert on the first (and only)
+ // call. This way it can be safely destructed on the main
+ // thread instead of the current one when this call
+ // finishes. TODO: remove this when
+ // https://github.com/emscripten-core/emscripten/issues/20610
+ // is fixed.
+ auto func_ = std::move(func);
+ func_();
+ });
+ assert(proxied);
+ return;
+ } else {
+ // A storage for the result of the function call.
+ // TODO: remove when
+ // https://github.com/emscripten-core/emscripten/issues/20611 is
+ // implemented.
+ std::optional<std::invoke_result_t<Func>> result;
+ runOnMain(
+ [&result, func = std::move(func)] { result.emplace(func()); });
+ return std::move(result.value());
+ }
+ }
+#endif
+ return func();
}
-// 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()};
- }
+// C++ struct representation for `{value, error}` object used by `CaughtPromise`
+// below.
+struct PromiseResult {
+ int error;
+ val value;
+
+ PromiseResult() = delete;
+ PromiseResult(PromiseResult&&) = default;
+
+ PromiseResult(val&& result)
+ : error(result["error"].as<int>()), value(result["value"]) {}
+
+ ~PromiseResult() {
+ // make sure value is freed on the thread it exists on
+ runOnMain([value = std::move(value)] {});
+ }
};
-// We store an Embind handle to WebUSB USBDevice in "priv" metadata of
-// libusb device, this helper returns a pointer to it.
+struct CaughtPromise : val {
+ CaughtPromise(val&& promise)
+ : val(wrapPromiseWithCatch(std::move(promise))) {}
+
+ using AwaitResult = PromiseResult;
+
+private:
+
+ // Wrap promise with conversion from some value T to `{value: T, error:
+ // number}`.
+ static val wrapPromiseWithCatch(val&& promise) {
+ auto handle = promise.as_handle();
+ handle = usbi_em_promise_catch(handle);
+ return val::take_ownership(handle);
+ }
+};
+
+#define co_await_try(promise) \
+ ({ \
+ PromiseResult result = co_await CaughtPromise(promise); \
+ if (result.error) { \
+ co_return result.error; \
+ } \
+ std::move(result.value); \
+ })
+
+// A helper that runs an asynchronous callback when the promise is resolved.
+template <typename Promise, typename OnResult>
+val promiseThen(Promise&& promise, OnResult&& onResult) {
+ // Save captures from the callback while we can, or they'll be destructed.
+ // https://devblogs.microsoft.com/oldnewthing/20211103-00/?p=105870
+ auto onResult_ = std::move(onResult);
+ onResult_(co_await promise);
+ co_return val::undefined();
+}
+
+// A helper that runs an asynchronous function on the main thread and blocks the
+// current thread until the promise is resolved (via Asyncify "blocking" if
+// already on the main thread or regular blocking otherwise).
+template <typename Func>
+static std::invoke_result_t<Func>::AwaitResult awaitOnMain(Func&& func) {
+#ifdef _REENTRANT
+ if (!emscripten_is_main_runtime_thread()) {
+ // If we're on a different thread, we can't use main thread's Asyncify
+ // as multiple threads might be fighting for its state; instead, use
+ // proxying to synchronously block the current thread until the promise
+ // is complete.
+ std::optional<typename std::invoke_result_t<Func>::AwaitResult> result;
+ queue.proxySyncWithCtx(
+ emscripten_main_runtime_thread_id(),
+ [&result, &func](ProxyingQueue::ProxyingCtx ctx) {
+ // Same as `func` in `runOnMain`, move to destruct on the first
+ // call.
+ auto func_ = std::move(func);
+ promiseThen(
+ func_(),
+ [&result, ctx = std::move(ctx)](auto&& result_) mutable {
+ result.emplace(std::move(result_));
+ ctx.finish();
+ });
+ });
+ return std::move(result.value());
+ }
+#endif
+ // If we're already on the main thread, use Asyncify to block until the
+ // promise is resolved.
+ return func().await();
+}
+
+// A helper that makes a control transfer given a setup pointer (assumed to be
+// followed by data payload for out-transfers).
+val makeControlTransferPromise(const val& dev, libusb_control_setup* setup) {
+ auto params = val::object();
+
+ const char* 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:
+ request_type = "standard";
+ break;
+ case LIBUSB_REQUEST_TYPE_CLASS:
+ request_type = "class";
+ break;
+ case LIBUSB_REQUEST_TYPE_VENDOR:
+ request_type = "vendor";
+ break;
+ }
+ params.set("requestType", 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;
+ }
+ params.set("recipient", recipient);
+
+ params.set("request", setup->bRequest);
+ params.set("value", setup->wValue);
+ params.set("index", setup->wIndex);
+
+ if (setup->bmRequestType & LIBUSB_ENDPOINT_IN) {
+ return dev.call<val>("controlTransferIn", params, setup->wLength);
+ } else {
+ return dev.call<val>("controlTransferOut", params,
+ getUnsharedMemoryView(setup + 1, setup->wLength));
+ }
+}
+
+// Smart pointer for managing pointers to places allocated by libusb inside its
+// backend structures.
+template <typename T>
struct ValPtr {
- public:
- void init_to(val &&value) { new (ptr) val(std::move(value)); }
+ template <typename... Args>
+ void emplace(Args&&... args) {
+ new (ptr) T(std::forward<Args>(args)...);
+ }
+
+ const T& operator*() const { return *ptr; }
+ T& operator*() { return *ptr; }
+
+ const T* operator->() const { return ptr; }
+ T* operator->() { return ptr; }
+
+ void free() { ptr->~T(); }
- val &get() { return *ptr; }
- val take() { return std::move(get()); }
+ T take() {
+ auto value = std::move(*ptr);
+ free();
+ return value;
+ }
- protected:
- ValPtr(val *ptr) : ptr(ptr) {}
+protected:
- private:
- val *ptr;
+ ValPtr(void* ptr) : ptr(static_cast<T*>(ptr)) {}
+
+private:
+
+ // Note: this is not a heap-allocated pointer, but a pointer to a part
+ // of the backend structure allocated by libusb itself.
+ T* ptr;
};
-struct WebUsbDevicePtr : ValPtr {
- public:
- WebUsbDevicePtr(libusb_device *dev)
- : ValPtr(static_cast<val *>(usbi_get_device_priv(dev))) {}
+struct CachedDevice;
+
+struct WebUsbDevicePtr : ValPtr<CachedDevice> {
+public:
+
+ WebUsbDevicePtr(libusb_device* dev) : ValPtr(usbi_get_device_priv(dev)) {}
+ WebUsbDevicePtr(libusb_device_handle* handle)
+ : WebUsbDevicePtr(handle->dev) {}
};
-val &get_web_usb_device(libusb_device *dev) {
- return WebUsbDevicePtr(dev).get();
-}
+struct WebUsbTransferPtr : ValPtr<PromiseResult> {
+public:
-struct WebUsbTransferPtr : ValPtr {
- public:
- WebUsbTransferPtr(usbi_transfer *itransfer)
- : ValPtr(static_cast<val *>(usbi_get_transfer_priv(itransfer))) {}
+ WebUsbTransferPtr(usbi_transfer* itransfer)
+ : ValPtr(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);
-}
+enum class OpenClose : bool {
+ Open = true,
+ Close = false,
+};
+
+struct CachedDevice {
+ CachedDevice() = delete;
+ CachedDevice(CachedDevice&&) = delete;
+
+ // Fill in the device descriptor and configurations by reading them from the
+ // WebUSB device.
+ static val initFromDevice(val&& web_usb_dev, libusb_device* libusb_dev) {
+ auto cachedDevicePtr = WebUsbDevicePtr(libusb_dev);
+ cachedDevicePtr.emplace(std::move(web_usb_dev));
+ bool must_close = false;
+ val result = co_await cachedDevicePtr->initFromDeviceWithoutClosing(
+ libusb_dev, must_close);
+ if (must_close) {
+ co_await_try(cachedDevicePtr->safeOpenCloseAssumingMainThread(
+ OpenClose::Close));
+ }
+ co_return std::move(result);
+ }
+
+ const val& getDeviceAssumingMainThread() const { return device; }
+
+ uint8_t getActiveConfigValue() const {
+ return runOnMain([&] {
+ auto web_usb_config = device["configuration"];
+ return web_usb_config.isNull()
+ ? 0
+ : web_usb_config["configurationValue"].as<uint8_t>();
+ });
+ }
+
+ usbi_configuration_descriptor* getConfigDescriptor(uint8_t config_id) {
+ return config_id < configurations.size()
+ ? configurations[config_id].get()
+ : nullptr;
+ }
+
+ usbi_configuration_descriptor* findConfigDescriptorByValue(
+ uint8_t config_id) const {
+ for (auto& config : configurations) {
+ if (config->bConfigurationValue == config_id) {
+ return config.get();
+ }
+ }
+ return nullptr;
+ }
+
+ int copyConfigDescriptor(const usbi_configuration_descriptor* config,
+ void* buf,
+ size_t buf_len) {
+ auto len = std::min(buf_len, (size_t)config->wTotalLength);
+ memcpy(buf, config, len);
+ return len;
+ }
+
+ template <typename... Args>
+ int awaitOnMain(const char* methodName, Args&&... args) const {
+ return ::awaitOnMain([&] {
+ return CaughtPromise(device.call<val>(
+ methodName, std::forward<Args>(args)...));
+ })
+ .error;
+ }
+
+ ~CachedDevice() {
+ runOnMain([device = std::move(device)] {});
+ }
+
+ CaughtPromise safeOpenCloseAssumingMainThread(OpenClose open) {
+ return val::take_ownership(usbi_em_device_safe_open_close(
+ device.as_handle(), static_cast<bool>(open)));
+ }
+
+ int safeOpenCloseOnMain(OpenClose open) {
+ return ::awaitOnMain([this, open] {
+ return safeOpenCloseAssumingMainThread(open);
+ })
+ .error;
+ }
+
+private:
+
+ val device;
+ std::vector<std::unique_ptr<usbi_configuration_descriptor>> configurations;
+
+ CaughtPromise requestDescriptor(libusb_descriptor_type desc_type,
+ uint8_t desc_index,
+ uint16_t max_length) const {
+ libusb_control_setup setup = {
+ .bmRequestType = LIBUSB_ENDPOINT_IN,
+ .bRequest = LIBUSB_REQUEST_GET_DESCRIPTOR,
+ .wValue = (uint16_t)((desc_type << 8) | desc_index),
+ .wIndex = 0,
+ .wLength = max_length,
+ };
+ return makeControlTransferPromise(device, &setup);
+ }
+
+ // Implementation of the `CachedDevice::initFromDevice` above. This is a
+ // separate function just because we need to close the device on exit if
+ // we opened it successfully, and we can't use an async operation (`close`)
+ // in RAII destructor.
+ val initFromDeviceWithoutClosing(libusb_device* dev, bool& must_close) {
+ co_await_try(safeOpenCloseAssumingMainThread(OpenClose::Open));
+
+ // Can't use RAII to close on exit as co_await is not permitted in
+ // destructors (yet:
+ // https://github.com/cplusplus/papers/issues/445), so use a good
+ // old boolean + a wrapper instead.
+ must_close = true;
+
+ {
+ auto result = co_await_try(
+ requestDescriptor(LIBUSB_DT_DEVICE, 0, LIBUSB_DT_DEVICE_SIZE));
+ if (auto error = getTransferStatus(result)) {
+ co_return error;
+ }
+ copyFromDataView(&dev->device_descriptor, result["data"]);
+ }
+
+ // Infer the device speed (which is not yet provided by WebUSB) from
+ // the descriptor.
+ if (dev->device_descriptor.bMaxPacketSize0 ==
+ /* actually means 2^9, only valid for superspeeds */ 9) {
+ dev->speed = dev->device_descriptor.bcdUSB >= 0x0310
+ ? LIBUSB_SPEED_SUPER_PLUS
+ : LIBUSB_SPEED_SUPER;
+ } else if (dev->device_descriptor.bcdUSB >= 0x0200) {
+ dev->speed = LIBUSB_SPEED_HIGH;
+ } else if (dev->device_descriptor.bMaxPacketSize0 > 8) {
+ dev->speed = LIBUSB_SPEED_FULL;
+ } else {
+ dev->speed = LIBUSB_SPEED_LOW;
+ }
+
+ if (auto error = usbi_sanitize_device(dev)) {
+ co_return error;
+ }
+
+ auto configurations_len = dev->device_descriptor.bNumConfigurations;
+ configurations.reserve(configurations_len);
+ for (uint8_t j = 0; j < configurations_len; j++) {
+ // Note: requesting more than (platform-specific limit) bytes
+ // here will cause the transfer to fail, see
+ // https://crbug.com/1489414. Use the most common limit of 4096
+ // bytes for now.
+ constexpr uint16_t MAX_CTRL_BUFFER_LENGTH = 4096;
+ auto result = co_await_try(
+ requestDescriptor(LIBUSB_DT_CONFIG, j, MAX_CTRL_BUFFER_LENGTH));
+ if (auto error = getTransferStatus(result)) {
+ co_return error;
+ }
+ auto configVal = result["data"];
+ auto configLen = configVal["byteLength"].as<size_t>();
+ auto& config = configurations.emplace_back(
+ (usbi_configuration_descriptor*)::operator new(configLen));
+ copyFromDataView(config.get(), configVal);
+ }
-// Store the global `navigator.usb` once upon initialisation.
-thread_local const val web_usb = val::global("navigator")["usb"];
+ co_return (int) LIBUSB_SUCCESS;
+ }
-enum StringId : uint8_t {
- Manufacturer = 1,
- Product = 2,
- SerialNumber = 3,
+ CachedDevice(val device) : device(std::move(device)) {}
+
+ friend struct ValPtr<CachedDevice>;
};
-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;
-}
+unsigned long getDeviceSessionId(val& web_usb_device) {
+ thread_local const val SessionIdSymbol =
+ val::global("Symbol")(val("libusb.session_id"));
+
+ val session_id_val = web_usb_device[SessionIdSymbol];
+ if (!session_id_val.isUndefined()) {
+ return session_id_val.as<unsigned long>();
+ }
+
+ // If the device doesn't have a session ID, it means we haven't seen
+ // it before. Generate a new session ID for it. We can associate an
+ // incrementing ID with the `USBDevice` object itself. It's
+ // guaranteed to be alive and, thus, stable as long as the device is
+ // connected, even between different libusb invocations. See
+ // https://github.com/WICG/webusb/issues/241.
+
+ static unsigned long next_session_id = 0;
-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;
+ web_usb_device.set(SessionIdSymbol, next_session_id);
+ return next_session_id++;
}
-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");
+val getDeviceList(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 web_usb_devices =
+ co_await_try(val::global("navigator")["usb"].call<val>("getDevices"));
+ for (auto&& web_usb_device : web_usb_devices) {
+ auto session_id = getDeviceSessionId(web_usb_device);
+
+ 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;
+ }
+
+ auto statusVal = co_await CachedDevice::initFromDevice(
+ std::move(web_usb_device), dev);
+ if (auto error = statusVal.as<int>()) {
+ usbi_err(ctx, "failed to read device information: %s",
+ libusb_error_name(error));
+ libusb_unref_device(dev);
+ continue;
+ }
+
+ // We don't have real buses in WebUSB, just pretend everything
+ // is on bus 1.
+ dev->bus_number = 1;
+ // This can wrap around but it's the best approximation of a stable
+ // device address and port number we can provide.
+ dev->device_address = dev->port_number = (uint8_t)session_id;
+ }
+ *devs = discovered_devs_append(*devs, dev);
+ libusb_unref_device(dev);
+ }
+ co_return (int) LIBUSB_SUCCESS;
}
-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_device_list(libusb_context* ctx, discovered_devs** devs) {
+ // No need to wrap into CaughtPromise as we catch all individual ops in the
+ // inner implementation and return just the error code. We do need a custom
+ // promise type to ensure conversion to int happens on the main thread
+ // though.
+ struct IntPromise : val {
+ IntPromise(val&& promise) : val(std::move(promise)) {}
+
+ struct AwaitResult {
+ int error;
+
+ AwaitResult(val&& result) : error(result.as<int>()) {}
+ };
+ };
+
+ return awaitOnMain(
+ [ctx, devs] { return IntPromise(getDeviceList(ctx, devs)); })
+ .error;
}
-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_open(libusb_device_handle* handle) {
+ return WebUsbDevicePtr(handle)->safeOpenCloseOnMain(OpenClose::Open);
}
-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);
+void em_close(libusb_device_handle* handle) {
+ // LibUSB API doesn't allow us to handle an error here, but we still need to
+ // wait for the promise to make sure that subsequent attempt to reopen the
+ // same device doesn't fail with a "device busy" error.
+ if (auto error =
+ WebUsbDevicePtr(handle)->safeOpenCloseOnMain(OpenClose::Close)) {
+ usbi_err(handle->dev->ctx, "failed to close device: %s",
+ libusb_error_name(error));
+ }
}
-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_get_active_config_descriptor(libusb_device* dev, void* buf, size_t len) {
+ auto& cached_device = *WebUsbDevicePtr(dev);
+ auto config_value = cached_device.getActiveConfigValue();
+ if (auto config = cached_device.findConfigDescriptorByValue(config_value)) {
+ return cached_device.copyConfigDescriptor(config, buf, len);
+ } else {
+ return LIBUSB_ERROR_NOT_FOUND;
+ }
}
-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_get_config_descriptor(libusb_device* dev,
+ uint8_t config_id,
+ void* buf,
+ size_t len) {
+ auto& cached_device = *WebUsbDevicePtr(dev);
+ if (auto config = cached_device.getConfigDescriptor(config_id)) {
+ return cached_device.copyConfigDescriptor(config, buf, len);
+ } else {
+ return LIBUSB_ERROR_NOT_FOUND;
+ }
}
-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_get_configuration(libusb_device_handle* dev_handle,
+ uint8_t* config_value) {
+ *config_value = WebUsbDevicePtr(dev_handle)->getActiveConfigValue();
+ return LIBUSB_SUCCESS;
}
-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_get_config_descriptor_by_value(libusb_device* dev,
+ uint8_t config_value,
+ void** buf) {
+ auto& cached_device = *WebUsbDevicePtr(dev);
+ if (auto config = cached_device.findConfigDescriptorByValue(config_value)) {
+ *buf = config;
+ return config->wTotalLength;
+ } else {
+ return LIBUSB_ERROR_NOT_FOUND;
+ }
}
-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_set_configuration(libusb_device_handle* dev_handle, int config) {
+ return WebUsbDevicePtr(dev_handle)->awaitOnMain("setConfiguration", config);
}
-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;
+int em_claim_interface(libusb_device_handle* handle, uint8_t iface) {
+ return WebUsbDevicePtr(handle)->awaitOnMain("claimInterface", iface);
+}
- return promise_result::await(get_web_usb_device(handle->dev)
- .call<val>("clearHalt", direction, endpoint))
- .error;
+int em_release_interface(libusb_device_handle* handle, uint8_t iface) {
+ return WebUsbDevicePtr(handle)->awaitOnMain("releaseInterface", iface);
}
-int em_reset_device(libusb_device_handle *handle) {
- return promise_result::await(
- get_web_usb_device(handle->dev).call<val>("reset"))
- .error;
+int em_set_interface_altsetting(libusb_device_handle* handle,
+ uint8_t iface,
+ uint8_t altsetting) {
+ return WebUsbDevicePtr(handle)->awaitOnMain("selectAlternateInterface",
+ iface, altsetting);
}
-void em_destroy_device(libusb_device *dev) { WebUsbDevicePtr(dev).take(); }
+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;
-thread_local const val Uint8Array = val::global("Uint8Array");
+ return WebUsbDevicePtr(handle)->awaitOnMain("clearHalt", direction,
+ endpoint);
+}
-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));
+int em_reset_device(libusb_device_handle* handle) {
+ return WebUsbDevicePtr(handle)->awaitOnMain("reset");
}
-// 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_destroy_device(libusb_device* dev) {
+ WebUsbDevicePtr(dev).free();
+}
+
+int em_submit_transfer(usbi_transfer* itransfer) {
+ return runOnMain([itransfer] {
+ auto transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);
+ auto& web_usb_device = WebUsbDevicePtr(transfer->dev_handle)
+ ->getDeviceAssumingMainThread();
+ val transfer_promise;
+ switch (transfer->type) {
+ case LIBUSB_TRANSFER_TYPE_CONTROL: {
+ transfer_promise = makeControlTransferPromise(
+ web_usb_device,
+ libusb_control_transfer_get_setup(transfer));
+ break;
+ }
+ case LIBUSB_TRANSFER_TYPE_BULK:
+ case LIBUSB_TRANSFER_TYPE_INTERRUPT: {
+ auto endpoint =
+ transfer->endpoint & LIBUSB_ENDPOINT_ADDRESS_MASK;
+
+ if (IS_XFERIN(transfer)) {
+ transfer_promise = web_usb_device.call<val>(
+ "transferIn", endpoint, transfer->length);
+ } else {
+ auto data = getUnsharedMemoryView(transfer->buffer,
+ transfer->length);
+ transfer_promise =
+ web_usb_device.call<val>("transferOut", endpoint, data);
+ }
-void em_start_transfer(usbi_transfer *itransfer, val &&promise) {
- promise = em_promise_catch(std::move(promise));
- em_start_transfer_impl(itransfer, promise.as_handle());
+ break;
+ }
+ // TODO: add implementation for isochronous transfers too.
+ default:
+ return LIBUSB_ERROR_NOT_SUPPORTED;
+ }
+ // Not a coroutine because we don't want to block on this promise, just
+ // schedule an asynchronous callback.
+ promiseThen(CaughtPromise(std::move(transfer_promise)),
+ [itransfer](auto&& result) {
+ WebUsbTransferPtr(itransfer).emplace(std::move(result));
+ usbi_signal_transfer_completion(itransfer);
+ });
+ return LIBUSB_SUCCESS;
+ });
}
-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).free();
}
-void em_clear_transfer_priv(usbi_transfer *itransfer) {
- WebUsbTransferPtr(itransfer).take();
+int em_cancel_transfer(usbi_transfer* itransfer) {
+ return LIBUSB_SUCCESS;
}
-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);
+int em_handle_transfer_completion(usbi_transfer* itransfer) {
+ libusb_transfer_status status = runOnMain([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 = WebUsbTransferPtr(itransfer).take();
+
+ if (itransfer->state_flags & USBI_TRANSFER_CANCELLING) {
+ return LIBUSB_TRANSFER_CANCELLED;
+ }
+
+ if (result.error) {
+ return LIBUSB_TRANSFER_ERROR;
+ }
+
+ auto& value = result.value;
+
+ void* dataDest;
+ unsigned char endpointDir;
+
+ if (transfer->type == LIBUSB_TRANSFER_TYPE_CONTROL) {
+ dataDest = libusb_control_transfer_get_data(transfer);
+ endpointDir =
+ libusb_control_transfer_get_setup(transfer)->bmRequestType;
+ } else {
+ dataDest = transfer->buffer;
+ endpointDir = transfer->endpoint;
+ }
+
+ if (endpointDir & LIBUSB_ENDPOINT_IN) {
+ auto data = value["data"];
+ if (!data.isNull()) {
+ itransfer->transferred = data["byteLength"].as<int>();
+ copyFromDataView(dataDest, data);
+ }
+ } else {
+ itransfer->transferred = value["bytesWritten"].as<int>();
+ }
+
+ return getTransferStatus(value);
+ });
+
+ // Invoke user's handlers outside of the main thread to reduce pressure.
+ return status == LIBUSB_TRANSFER_CANCELLED
+ ? usbi_handle_transfer_cancellation(itransfer)
+ : 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 ignored "-Wmissing-field-initializers"
+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_config_descriptor_by_value = em_get_config_descriptor_by_value,
+ .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(CachedDevice),
+ .transfer_priv_size = sizeof(PromiseResult),
};
-}
+
#pragma clang diagnostic pop
diff --git a/libusb/os/events_posix.c b/libusb/os/events_posix.c
index 2ba0103..4056dae 100644
--- a/libusb/os/events_posix.c
+++ b/libusb/os/events_posix.c
@@ -37,26 +37,21 @@
*
* Therefore use a custom event system based on browser event emitters. */
#include <emscripten.h>
+#include <emscripten/atomic.h>
+#include <emscripten/threading.h>
-EM_JS(void, em_libusb_notify, (void), {
- dispatchEvent(new Event("em-libusb"));
+EM_ASYNC_JS(void, em_libusb_wait_async, (const _Atomic int* ptr, int expected_value, int timeout), {
+ await Atomics.waitAsync(HEAP32, ptr >> 2, expected_value, timeout).value;
});
-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);
+static void em_libusb_wait(const _Atomic int *ptr, int expected_value, int timeout)
+{
+ if (emscripten_is_main_runtime_thread()) {
+ em_libusb_wait_async(ptr, expected_value, timeout);
+ } else {
+ emscripten_atomic_wait_u32((int*)ptr, expected_value, 1000000LL * timeout);
}
-});
+}
#endif
#include <unistd.h>
@@ -162,7 +157,8 @@ void usbi_signal_event(usbi_event_t *event)
if (r != sizeof(dummy))
usbi_warn(NULL, "event write failed");
#ifdef __EMSCRIPTEN__
- em_libusb_notify();
+ event->has_event = 1;
+ emscripten_atomic_notify(&event->has_event, EMSCRIPTEN_NOTIFY_ALL_WAITERS);
#endif
}
@@ -174,6 +170,9 @@ void usbi_clear_event(usbi_event_t *event)
r = read(EVENT_READ_FD(event), &dummy, sizeof(dummy));
if (r != sizeof(dummy))
usbi_warn(NULL, "event read failed");
+#ifdef __EMSCRIPTEN__
+ event->has_event = 0;
+#endif
}
#ifdef HAVE_TIMERFD
@@ -257,22 +256,14 @@ int usbi_wait_for_events(struct libusb_context *ctx,
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);
+ // Emscripten's poll doesn't actually block, so we need to use an out-of-band
+ // waiting signal.
+ em_libusb_wait(&ctx->event.has_event, 0, timeout_ms);
+ // Emscripten ignores timeout_ms, but set it to 0 for future-proofing in case
+ // they ever implement real poll.
+ timeout_ms = 0;
#endif
+ num_ready = poll(fds, nfds, timeout_ms);
usbi_dbg(ctx, "poll() returned %d", num_ready);
if (num_ready == 0) {
if (usbi_using_timer(ctx))
diff --git a/libusb/os/events_posix.h b/libusb/os/events_posix.h
index d81b5c4..4bd7f0f 100644
--- a/libusb/os/events_posix.h
+++ b/libusb/os/events_posix.h
@@ -36,6 +36,9 @@ typedef struct usbi_event {
#else
typedef struct usbi_event {
int pipefd[2];
+#ifdef __EMSCRIPTEN__
+ _Atomic int has_event;
+#endif
} usbi_event_t;
#define USBI_EVENT_OS_HANDLE(e) ((e)->pipefd[0])
#define USBI_EVENT_POLL_EVENTS POLLIN
diff --git a/libusb/version_nano.h b/libusb/version_nano.h
index bc099f4..f13292a 100644
--- a/libusb/version_nano.h
+++ b/libusb/version_nano.h
@@ -1 +1 @@
-#define LIBUSB_NANO 11842
+#define LIBUSB_NANO 11843
diff --git a/tests/stress_mt.c b/tests/stress_mt.c
index 7de4fb0..0830401 100644
--- a/tests/stress_mt.c
+++ b/tests/stress_mt.c
@@ -249,12 +249,8 @@ int main(void)
printf("Running multithreaded init/exit test...\n");
errs += test_multi_init(0);
-#ifdef __EMSCRIPTEN__
- printf("Skipping enumeration test on Emscripten. Multithreading is not supported yet.\n");
-#else
printf("Running multithreaded init/exit test with enumeration...\n");
errs += test_multi_init(1);
-#endif
printf("All done, %d errors\n", errs);
return errs != 0;
diff --git a/tests/webusb-test-shim/index.js b/tests/webusb-test-shim/index.js
index dde1d27..703aefd 100644
--- a/tests/webusb-test-shim/index.js
+++ b/tests/webusb-test-shim/index.js
@@ -10,11 +10,3 @@ globalThis.navigator = {
allowAllDevices: true
})
};
-
-// events_posix uses Web events on the global scope (for now), but Node.js doesn't have them.
-
-const fakeEventTarget = new EventTarget();
-
-for (let method in fakeEventTarget) {
- globalThis[method] = fakeEventTarget[method].bind(fakeEventTarget);
-}