add broken ass source for now
This commit is contained in:
@@ -0,0 +1,896 @@
|
||||
/*
|
||||
* 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++) {
|
||||
// Read descriptor header first to discover target length.
|
||||
auto config_header_result = co_await_try(
|
||||
requestDescriptor(LIBUSB_DT_CONFIG, j, LIBUSB_DT_CONFIG_SIZE));
|
||||
if (auto error = getTransferStatus(config_header_result)) {
|
||||
co_return error;
|
||||
}
|
||||
union usbi_config_desc_buf config_header = {};
|
||||
copyFromDataView(config_header.buf, config_header_result["data"]);
|
||||
if (config_header.desc.bDescriptorType != LIBUSB_DT_CONFIG ||
|
||||
config_header.desc.bLength < LIBUSB_DT_CONFIG_SIZE) {
|
||||
co_return LIBUSB_ERROR_IO;
|
||||
}
|
||||
|
||||
auto config_total_length =
|
||||
libusb_le16_to_cpu(config_header.desc.wTotalLength);
|
||||
if (config_total_length < LIBUSB_DT_CONFIG_SIZE) {
|
||||
co_return LIBUSB_ERROR_IO;
|
||||
}
|
||||
|
||||
auto config_result = co_await_try(
|
||||
requestDescriptor(LIBUSB_DT_CONFIG, j, config_total_length));
|
||||
if (auto error = getTransferStatus(config_result)) {
|
||||
co_return error;
|
||||
}
|
||||
|
||||
auto configVal = config_result["data"];
|
||||
if (configVal["byteLength"].as<size_t>() < config_total_length) {
|
||||
co_return LIBUSB_ERROR_IO;
|
||||
}
|
||||
|
||||
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) {
|
||||
// Check if browser supports USB
|
||||
val navigator_usb = val::global("navigator")["usb"];
|
||||
if (navigator_usb == val::undefined()) {
|
||||
co_return (int) LIBUSB_ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
// 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(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;
|
||||
|
||||
usbi_connect_device(dev);
|
||||
}
|
||||
*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("selectConfiguration", 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 = 0,
|
||||
.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
|
||||
Reference in New Issue
Block a user