yaes/deps/libusb/libusb/os/emscripten_webusb.cpp
torque eace0bf8db
init
the readme is a ramble and there's no functionality (yet). couldn't ask
for a better start.
2024-06-28 22:05:14 -07:00

871 lines
27 KiB
C++

/*
* 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
* 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/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, 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,
// 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,
};
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();
}
} else {
if (!--refCount) {
await device.close();
}
}
return refCount;
});
return Emval.toHandle(promiseChain);
});
// clang-format on
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 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)] {});
}
};
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 {
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(); }
T take() {
auto value = std::move(*ptr);
free();
return value;
}
protected:
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 CachedDevice;
struct WebUsbDevicePtr : ValPtr<CachedDevice> {
public:
WebUsbDevicePtr(libusb_device* dev) : ValPtr(usbi_get_device_priv(dev)) {}
WebUsbDevicePtr(libusb_device_handle* handle)
: WebUsbDevicePtr(handle->dev) {}
};
struct WebUsbTransferPtr : ValPtr<PromiseResult> {
public:
WebUsbTransferPtr(usbi_transfer* itransfer)
: ValPtr(usbi_get_transfer_priv(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);
}
co_return (int) LIBUSB_SUCCESS;
}
CachedDevice(val device) : device(std::move(device)) {}
friend struct ValPtr<CachedDevice>;
};
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;
web_usb_device.set(SessionIdSymbol, next_session_id);
return next_session_id++;
}
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_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_open(libusb_device_handle* handle) {
return WebUsbDevicePtr(handle)->safeOpenCloseOnMain(OpenClose::Open);
}
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_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_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_get_configuration(libusb_device_handle* dev_handle,
uint8_t* config_value) {
*config_value = WebUsbDevicePtr(dev_handle)->getActiveConfigValue();
return LIBUSB_SUCCESS;
}
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_configuration(libusb_device_handle* dev_handle, int config) {
return WebUsbDevicePtr(dev_handle)->awaitOnMain("setConfiguration", config);
}
int em_claim_interface(libusb_device_handle* handle, uint8_t iface) {
return WebUsbDevicePtr(handle)->awaitOnMain("claimInterface", iface);
}
int em_release_interface(libusb_device_handle* handle, uint8_t iface) {
return WebUsbDevicePtr(handle)->awaitOnMain("releaseInterface", iface);
}
int em_set_interface_altsetting(libusb_device_handle* handle,
uint8_t iface,
uint8_t altsetting) {
return WebUsbDevicePtr(handle)->awaitOnMain("selectAlternateInterface",
iface, altsetting);
}
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 WebUsbDevicePtr(handle)->awaitOnMain("clearHalt", direction,
endpoint);
}
int em_reset_device(libusb_device_handle* handle) {
return WebUsbDevicePtr(handle)->awaitOnMain("reset");
}
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);
}
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;
});
}
void em_clear_transfer_priv(usbi_transfer* itransfer) {
WebUsbTransferPtr(itransfer).free();
}
int em_cancel_transfer(usbi_transfer* itransfer) {
return LIBUSB_SUCCESS;
}
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
#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