/* * Copyright © 2021 Google LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Authors: * Ingvar Stepanyan */ #include #include #include "libusbi.h" using namespace emscripten; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wmissing-prototypes" #pragma clang diagnostic ignored "-Wunused-parameter" namespace { // clang-format off EM_JS(EM_VAL, em_promise_catch_impl, (EM_VAL handle), { let promise = Emval.toValue(handle); promise = promise.then( value => ({error : 0, value}), error => { const ERROR_CODES = { // LIBUSB_ERROR_IO NetworkError : -1, // LIBUSB_ERROR_INVALID_PARAM DataError : -2, TypeMismatchError : -2, IndexSizeError : -2, // LIBUSB_ERROR_ACCESS SecurityError : -3, // LIBUSB_ERROR_NOT_FOUND NotFoundError : -5, // LIBUSB_ERROR_BUSY InvalidStateError : -6, // LIBUSB_ERROR_TIMEOUT TimeoutError : -7, // LIBUSB_ERROR_INTERRUPTED AbortError : -10, // LIBUSB_ERROR_NOT_SUPPORTED NotSupportedError : -12, }; console.error(error); let errorCode = -99; // LIBUSB_ERROR_OTHER if (error instanceof DOMException) { errorCode = ERROR_CODES[error.name] ?? errorCode; } else if ((error instanceof RangeError) || (error instanceof TypeError)) { errorCode = -2; // LIBUSB_ERROR_INVALID_PARAM } return {error: errorCode, value: undefined}; } ); return Emval.toHandle(promise); }); // clang-format on val em_promise_catch(val &&promise) { EM_VAL handle = promise.as_handle(); handle = em_promise_catch_impl(handle); return val::take_ownership(handle); } // C++ struct representation for {value, error} object from above // (performs conversion in the constructor). struct promise_result { libusb_error error; val value; promise_result(val &&result) : error(static_cast(result["error"].as())), value(result["value"]) {} // C++ counterpart of the promise helper above that takes a promise, catches // its error, converts to a libusb status and returns the whole thing as // `promise_result` struct for easier handling. static promise_result await(val &&promise) { promise = em_promise_catch(std::move(promise)); return {promise.await()}; } }; // We store an Embind handle to WebUSB USBDevice in "priv" metadata of // libusb device, this helper returns a pointer to it. struct ValPtr { public: void init_to(val &&value) { new (ptr) val(std::move(value)); } val &get() { return *ptr; } val take() { return std::move(get()); } protected: ValPtr(val *ptr) : ptr(ptr) {} private: val *ptr; }; struct WebUsbDevicePtr : ValPtr { public: WebUsbDevicePtr(libusb_device *dev) : ValPtr(static_cast(usbi_get_device_priv(dev))) {} }; val &get_web_usb_device(libusb_device *dev) { return WebUsbDevicePtr(dev).get(); } struct WebUsbTransferPtr : ValPtr { public: WebUsbTransferPtr(usbi_transfer *itransfer) : ValPtr(static_cast(usbi_get_transfer_priv(itransfer))) {} }; void em_signal_transfer_completion_impl(usbi_transfer *itransfer, val &&result) { WebUsbTransferPtr(itransfer).init_to(std::move(result)); usbi_signal_transfer_completion(itransfer); } // Store the global `navigator.usb` once upon initialisation. thread_local const val web_usb = val::global("navigator")["usb"]; enum StringId : uint8_t { Manufacturer = 1, Product = 2, SerialNumber = 3, }; int em_get_device_list(libusb_context *ctx, discovered_devs **devs) { // C++ equivalent of `await navigator.usb.getDevices()`. // Note: at this point we must already have some devices exposed - // caller must have called `await navigator.usb.requestDevice(...)` // in response to user interaction before going to LibUSB. // Otherwise this list will be empty. auto result = promise_result::await(web_usb.call("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(); 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(); auto product_id = web_usb_device["productId"].as(); // 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( (web_usb_device["usbVersionMajor"].as() << 8) | (web_usb_device["usbVersionMinor"].as() << 4) | web_usb_device["usbVersionSubminor"].as()), .bDeviceClass = web_usb_device["deviceClass"].as(), .bDeviceSubClass = web_usb_device["deviceSubclass"].as(), .bDeviceProtocol = web_usb_device["deviceProtocol"].as(), .bMaxPacketSize0 = 64, // yolo .idVendor = vendor_id, .idProduct = product_id, .bcdDevice = static_cast( (web_usb_device["deviceVersionMajor"].as() << 8) | (web_usb_device["deviceVersionMinor"].as() << 4) | web_usb_device["deviceVersionSubminor"].as()), // 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(), }; if (usbi_sanitize_device(dev) < 0) { libusb_unref_device(dev); continue; } WebUsbDevicePtr(dev).init_to(std::move(web_usb_device)); } *devs = discovered_devs_append(*devs, dev); } return LIBUSB_SUCCESS; } int em_open(libusb_device_handle *handle) { auto web_usb_device = get_web_usb_device(handle->dev); return promise_result::await(web_usb_device.call("open")).error; } void em_close(libusb_device_handle *handle) { // LibUSB API doesn't allow us to handle an error here, so ignore the Promise // altogether. return get_web_usb_device(handle->dev).call("close"); } int em_get_config_descriptor_impl(val &&web_usb_config, void *buf, size_t len) { const auto buf_start = static_cast(buf); auto web_usb_interfaces = web_usb_config["interfaces"]; auto num_interfaces = web_usb_interfaces["length"].as(); auto config = static_cast(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(), .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(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(); config->wTotalLength += LIBUSB_DT_INTERFACE_SIZE + num_endpoints * LIBUSB_DT_ENDPOINT_SIZE; if (config->wTotalLength > len) { continue; } auto interface = static_cast(buf); *interface = { .bLength = LIBUSB_DT_INTERFACE_SIZE, .bDescriptorType = LIBUSB_DT_INTERFACE, .bInterfaceNumber = web_usb_interface["interfaceNumber"].as(), .bAlternateSetting = web_usb_alternate["alternateSetting"].as(), .bNumEndpoints = web_usb_endpoints["length"].as(), .bInterfaceClass = web_usb_alternate["interfaceClass"].as(), .bInterfaceSubClass = web_usb_alternate["interfaceSubclass"].as(), .bInterfaceProtocol = web_usb_alternate["interfaceProtocol"].as(), .iInterface = 0, // Not exposed in WebUSB, don't assign any string. }; buf = static_cast(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(buf); auto web_usb_endpoint_type = web_usb_endpoint["type"].as(); 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() == "in") << 7) | web_usb_endpoint["endpointNumber"].as(); endpoint->bmAttributes = transfer_type; endpoint->wMaxPacketSize = web_usb_endpoint["packetSize"].as(); endpoint->bInterval = 1; buf = static_cast(buf) + LIBUSB_DT_ENDPOINT_SIZE; } } return static_cast(buf) - buf_start; } int em_get_active_config_descriptor(libusb_device *dev, void *buf, size_t len) { auto web_usb_config = get_web_usb_device(dev)["configuration"]; if (web_usb_config.isNull()) { return LIBUSB_ERROR_NOT_FOUND; } return em_get_config_descriptor_impl(std::move(web_usb_config), buf, len); } int em_get_config_descriptor(libusb_device *dev, uint8_t idx, void *buf, size_t len) { return em_get_config_descriptor_impl( get_web_usb_device(dev)["configurations"][idx], buf, len); } int em_get_configuration(libusb_device_handle *dev_handle, uint8_t *config) { auto web_usb_config = get_web_usb_device(dev_handle->dev)["configuration"]; if (!web_usb_config.isNull()) { *config = web_usb_config["configurationValue"].as(); } return LIBUSB_SUCCESS; } int em_set_configuration(libusb_device_handle *handle, int config) { return promise_result::await(get_web_usb_device(handle->dev) .call("selectConfiguration", config)) .error; } int em_claim_interface(libusb_device_handle *handle, uint8_t iface) { return promise_result::await( get_web_usb_device(handle->dev).call("claimInterface", iface)) .error; } int em_release_interface(libusb_device_handle *handle, uint8_t iface) { return promise_result::await(get_web_usb_device(handle->dev) .call("releaseInterface", iface)) .error; } int em_set_interface_altsetting(libusb_device_handle *handle, uint8_t iface, uint8_t altsetting) { return promise_result::await( get_web_usb_device(handle->dev) .call("selectAlternateInterface", iface, altsetting)) .error; } int em_clear_halt(libusb_device_handle *handle, unsigned char endpoint) { std::string direction = endpoint & LIBUSB_ENDPOINT_IN ? "in" : "out"; endpoint &= LIBUSB_ENDPOINT_ADDRESS_MASK; return promise_result::await(get_web_usb_device(handle->dev) .call("clearHalt", direction, endpoint)) .error; } int em_reset_device(libusb_device_handle *handle) { return promise_result::await( get_web_usb_device(handle->dev).call("reset")) .error; } void em_destroy_device(libusb_device *dev) { WebUsbDevicePtr(dev).take(); } thread_local const val Uint8Array = val::global("Uint8Array"); EMSCRIPTEN_KEEPALIVE extern "C" void em_signal_transfer_completion(usbi_transfer *itransfer, EM_VAL result_handle) { em_signal_transfer_completion_impl(itransfer, val::take_ownership(result_handle)); } // clang-format off EM_JS(void, em_start_transfer_impl, (usbi_transfer *transfer, EM_VAL handle), { // Right now the handle value should be a `Promise<{value, error}>`. // Subscribe to its result to unwrap the promise to `{value, error}` // and signal transfer completion. // Catch the error to transform promise of `value` into promise of `{value, // error}`. Emval.toValue(handle).then(result => { _em_signal_transfer_completion(transfer, Emval.toHandle(result)); }); }); // clang-format on void em_start_transfer(usbi_transfer *itransfer, val &&promise) { promise = em_promise_catch(std::move(promise)); em_start_transfer_impl(itransfer, promise.as_handle()); } int em_submit_transfer(usbi_transfer *itransfer) { auto transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); auto web_usb_device = get_web_usb_device(transfer->dev_handle->dev); switch (transfer->type) { case LIBUSB_TRANSFER_TYPE_CONTROL: { auto setup = libusb_control_transfer_get_setup(transfer); auto web_usb_control_transfer_params = val::object(); const char *web_usb_request_type = "unknown"; // See LIBUSB_REQ_TYPE in windows_winusb.h (or docs for `bmRequestType`). switch (setup->bmRequestType & (0x03 << 5)) { case LIBUSB_REQUEST_TYPE_STANDARD: if (setup->bRequest == LIBUSB_REQUEST_GET_DESCRIPTOR && setup->wValue >> 8 == LIBUSB_DT_STRING) { // For string descriptors we provide custom implementation that // doesn't require an actual transfer, but just retrieves the value // from JS, stores that string handle as transfer data (instead of a // Promise) and immediately signals completion. const char *propName = nullptr; switch (setup->wValue & 0xFF) { case StringId::Manufacturer: propName = "manufacturerName"; break; case StringId::Product: propName = "productName"; break; case StringId::SerialNumber: propName = "serialNumber"; break; } if (propName != nullptr) { val str = web_usb_device[propName]; if (str.isNull()) { str = val(""); } em_signal_transfer_completion_impl(itransfer, std::move(str)); return LIBUSB_SUCCESS; } } web_usb_request_type = "standard"; break; case LIBUSB_REQUEST_TYPE_CLASS: web_usb_request_type = "class"; break; case LIBUSB_REQUEST_TYPE_VENDOR: web_usb_request_type = "vendor"; break; } web_usb_control_transfer_params.set("requestType", web_usb_request_type); const char *recipient = "other"; switch (setup->bmRequestType & 0x0f) { case LIBUSB_RECIPIENT_DEVICE: recipient = "device"; break; case LIBUSB_RECIPIENT_INTERFACE: recipient = "interface"; break; case LIBUSB_RECIPIENT_ENDPOINT: recipient = "endpoint"; break; } web_usb_control_transfer_params.set("recipient", recipient); web_usb_control_transfer_params.set("request", setup->bRequest); web_usb_control_transfer_params.set("value", setup->wValue); web_usb_control_transfer_params.set("index", setup->wIndex); if (setup->bmRequestType & LIBUSB_ENDPOINT_IN) { em_start_transfer( itransfer, web_usb_device.call("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("slice"); em_start_transfer( itransfer, web_usb_device.call( "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("transferIn", endpoint, transfer->length)); } else { auto data = val(typed_memory_view(transfer->length, transfer->buffer)) .call("slice"); em_start_transfer( itransfer, web_usb_device.call("transferOut", endpoint, data)); } break; } // TODO: add implementation for isochronous transfers too. default: return LIBUSB_ERROR_NOT_SUPPORTED; } return LIBUSB_SUCCESS; } void em_clear_transfer_priv(usbi_transfer *itransfer) { WebUsbTransferPtr(itransfer).take(); } int em_cancel_transfer(usbi_transfer *itransfer) { return LIBUSB_SUCCESS; } int em_handle_transfer_completion(usbi_transfer *itransfer) { auto transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer); // Take ownership of the transfer result, as `em_clear_transfer_priv` // is not called automatically for completed transfers and we must // free it to avoid leaks. auto result_val = WebUsbTransferPtr(itransfer).take(); if (itransfer->state_flags & USBI_TRANSFER_CANCELLING) { return usbi_handle_transfer_cancellation(itransfer); } libusb_transfer_status status = LIBUSB_TRANSFER_ERROR; // If this was a LIBUSB_DT_STRING request, then the value will be a string // handle instead of a promise. if (result_val.isString()) { int written = EM_ASM_INT( { // There's no good way to get UTF-16 output directly from JS string, // so again reach out to internals via JS snippet. return stringToUTF16(Emval.toValue($0), $1, $2); }, result_val.as_handle(), transfer->buffer + LIBUSB_CONTROL_SETUP_SIZE + 2, transfer->length - LIBUSB_CONTROL_SETUP_SIZE - 2); itransfer->transferred = transfer->buffer[LIBUSB_CONTROL_SETUP_SIZE] = 2 + written; transfer->buffer[LIBUSB_CONTROL_SETUP_SIZE + 1] = LIBUSB_DT_STRING; status = LIBUSB_TRANSFER_COMPLETED; } else { // Otherwise we should have a `{value, error}` object by now (see // `em_start_transfer_impl` callback). promise_result result(std::move(result_val)); if (!result.error) { auto web_usb_transfer_status = result.value["status"].as(); 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(); val(typed_memory_view(transfer->length - skip, transfer->buffer + skip)) .call("set", Uint8Array.new_(data["buffer"])); } } else { itransfer->transferred = result.value["bytesWritten"].as(); } } } return usbi_handle_transfer_completion(itransfer, status); } } // namespace extern "C" { const usbi_os_backend usbi_backend = { .name = "Emscripten + WebUSB backend", .caps = LIBUSB_CAP_HAS_CAPABILITY, .get_device_list = em_get_device_list, .open = em_open, .close = em_close, .get_active_config_descriptor = em_get_active_config_descriptor, .get_config_descriptor = em_get_config_descriptor, .get_configuration = em_get_configuration, .set_configuration = em_set_configuration, .claim_interface = em_claim_interface, .release_interface = em_release_interface, .set_interface_altsetting = em_set_interface_altsetting, .clear_halt = em_clear_halt, .reset_device = em_reset_device, .destroy_device = em_destroy_device, .submit_transfer = em_submit_transfer, .cancel_transfer = em_cancel_transfer, .clear_transfer_priv = em_clear_transfer_priv, .handle_transfer_completion = em_handle_transfer_completion, .device_priv_size = sizeof(val), .transfer_priv_size = sizeof(val), }; } #pragma clang diagnostic pop