/* * Copyright © 2021 Google LLC * Copyright © 2023 Ingvar Stepanyan * * 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 */ #include static_assert((__EMSCRIPTEN_major__ * 100 * 100 + __EMSCRIPTEN_minor__ * 100 + __EMSCRIPTEN_tiny__) >= 30148, "Emscripten 3.1.48 or newer is required."); #include #include #include #include #include #include "libusbi.h" using namespace emscripten; #ifdef _REENTRANT #include #include #include 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(); 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("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 auto runOnMain(Func&& func) { #ifdef _REENTRANT if (!emscripten_is_main_runtime_thread()) { if constexpr (std::is_same_v, 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> 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()), 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 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 static std::invoke_result_t::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::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("controlTransferIn", params, setup->wLength); } else { return dev.call("controlTransferOut", params, getUnsharedMemoryView(setup + 1, setup->wLength)); } } // Smart pointer for managing pointers to places allocated by libusb inside its // backend structures. template struct ValPtr { template void emplace(Args&&... args) { new (ptr) T(std::forward(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(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 { public: WebUsbDevicePtr(libusb_device* dev) : ValPtr(usbi_get_device_priv(dev)) {} WebUsbDevicePtr(libusb_device_handle* handle) : WebUsbDevicePtr(handle->dev) {} }; struct WebUsbTransferPtr : ValPtr { 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(); }); } 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 int awaitOnMain(const char* methodName, Args&&... args) const { return ::awaitOnMain([&] { return CaughtPromise(device.call( methodName, std::forward(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(open))); } int safeOpenCloseOnMain(OpenClose open) { return ::awaitOnMain([this, open] { return safeOpenCloseAssumingMainThread(open); }) .error; } private: val device; std::vector> 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(); 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; }; 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(); } // 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("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()) { 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()) {} }; }; 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( "transferIn", endpoint, transfer->length); } else { auto data = getUnsharedMemoryView(transfer->buffer, transfer->length); transfer_promise = web_usb_device.call("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(); copyFromDataView(dataDest, data); } } else { itransfer->transferred = value["bytesWritten"].as(); } 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