1 /*
2 * Copyright © 2021 Google LLC
3 * Copyright © 2023 Ingvar Stepanyan <[email protected]>
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2.1 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with this library; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 *
19 * Authors:
20 * Ingvar Stepanyan <[email protected]>
21 */
22
23 #include <emscripten/version.h>
24
25 static_assert((__EMSCRIPTEN_major__ * 100 * 100 + __EMSCRIPTEN_minor__ * 100 +
26 __EMSCRIPTEN_tiny__) >= 30148,
27 "Emscripten 3.1.48 or newer is required.");
28
29 #include <assert.h>
30 #include <emscripten.h>
31 #include <emscripten/val.h>
32
33 #include <type_traits>
34 #include <utility>
35
36 #include "libusbi.h"
37
38 using namespace emscripten;
39
40 #ifdef _REENTRANT
41 #include <emscripten/proxying.h>
42 #include <emscripten/threading.h>
43 #include <pthread.h>
44
45 static ProxyingQueue queue;
46 #endif
47
48 #pragma clang diagnostic push
49 #pragma clang diagnostic ignored "-Wmissing-prototypes"
50 #pragma clang diagnostic ignored "-Wunused-parameter"
51 #pragma clang diagnostic ignored "-Wshadow"
52
53 namespace {
54
55 // clang-format off
56 EM_JS(EM_VAL, usbi_em_promise_catch, (EM_VAL handle), {
57 let promise = Emval.toValue(handle);
58 promise = promise.then(
59 value => ({error : 0, value}),
60 error => {
61 console.error(error);
62 let errorCode = -99; // LIBUSB_ERROR_OTHER
63 if (error instanceof DOMException) {
64 const ERROR_CODES = {
65 // LIBUSB_ERROR_IO
66 NetworkError : -1,
67 // LIBUSB_ERROR_INVALID_PARAM
68 DataError : -2,
69 TypeMismatchError : -2,
70 IndexSizeError : -2,
71 // LIBUSB_ERROR_ACCESS
72 SecurityError : -3,
73 // LIBUSB_ERROR_NOT_FOUND
74 NotFoundError : -5,
75 // LIBUSB_ERROR_BUSY
76 InvalidStateError : -6,
77 // LIBUSB_ERROR_TIMEOUT
78 TimeoutError : -7,
79 // LIBUSB_ERROR_INTERRUPTED
80 AbortError : -10,
81 // LIBUSB_ERROR_NOT_SUPPORTED
82 NotSupportedError : -12,
83 };
84 errorCode = ERROR_CODES[error.name] ?? errorCode;
85 } else if (error instanceof RangeError || error instanceof TypeError) {
86 errorCode = -2; // LIBUSB_ERROR_INVALID_PARAM
87 }
88 return {error: errorCode, value: undefined};
89 }
90 );
91 return Emval.toHandle(promise);
92 });
93
94 EM_JS(void, usbi_em_copy_from_dataview, (void* dst, EM_VAL src), {
95 src = Emval.toValue(src);
96 src = new Uint8Array(src.buffer, src.byteOffset, src.byteLength);
97 HEAPU8.set(src, dst);
98 });
99
100 // Our implementation proxies operations from multiple threads to the same
101 // underlying USBDevice on the main thread. This can lead to issues when
102 // multiple threads try to open/close the same device at the same time.
103 //
104 // First, since open/close operations are asynchronous in WebUSB, we can end up
105 // with multiple open/close operations in flight at the same time, which can
106 // lead to unpredictable outcome (e.g. device got closed but opening succeeded
107 // right before that).
108 //
109 // Second, since multiple threads are allowed to have their own handles to the
110 // same device, we need to keep track of number of open handles and close the
111 // device only when the last handle is closed.
112 //
113 // We fix both of these issues by using a shared promise chain that executes
114 // open and close operations sequentially and keeps track of the reference count
115 // in each promise's result. This way, we can ensure that only one open/close
116 // operation is in flight at any given time. Note that we don't need to worry
117 // about all other operations because they're preconditioned on the device being
118 // open and having at least 1 reference anyway.
119 EM_JS(EM_VAL, usbi_em_device_safe_open_close, (EM_VAL device, bool open), {
120 device = Emval.toValue(device);
121 const symbol = Symbol.for('libusb.open_close_chain');
122 let promiseChain = device[symbol] ?? Promise.resolve(0);
123 device[symbol] = promiseChain = promiseChain.then(async refCount => {
124 if (open) {
125 if (!refCount++) {
126 await device.open();
127 }
128 } else {
129 if (!--refCount) {
130 await device.close();
131 }
132 }
133 return refCount;
134 });
135 return Emval.toHandle(promiseChain);
136 });
137 // clang-format on
138
getTransferStatus(const val & transfer_result)139 libusb_transfer_status getTransferStatus(const val& transfer_result) {
140 auto status = transfer_result["status"].as<std::string>();
141 if (status == "ok") {
142 return LIBUSB_TRANSFER_COMPLETED;
143 } else if (status == "stall") {
144 return LIBUSB_TRANSFER_STALL;
145 } else if (status == "babble") {
146 return LIBUSB_TRANSFER_OVERFLOW;
147 } else {
148 return LIBUSB_TRANSFER_ERROR;
149 }
150 }
151
152 // Note: this assumes that `dst` is valid for at least `src.byteLength` bytes.
153 // This is true for all results returned from WebUSB as we pass max length to
154 // the transfer APIs.
copyFromDataView(void * dst,const val & src)155 void copyFromDataView(void* dst, const val& src) {
156 usbi_em_copy_from_dataview(dst, src.as_handle());
157 }
158
getUnsharedMemoryView(void * src,size_t len)159 auto getUnsharedMemoryView(void* src, size_t len) {
160 auto view = typed_memory_view(len, (uint8_t*)src);
161 #ifdef _REENTRANT
162 // Unfortunately, TypedArrays backed by SharedArrayBuffers are not accepted
163 // by most Web APIs, trading off guaranteed thread-safety for performance
164 // loss. The usual workaround is to copy them into a new TypedArray, which
165 // is what we do here via the `.slice()` method.
166 return val(view).call<val>("slice");
167 #else
168 // Non-threaded builds can avoid the copy penalty.
169 return view;
170 #endif
171 }
172
173 // A helper that proxies a function call to the main thread if not already
174 // there. This is a wrapper around Emscripten's raw proxying API with couple of
175 // high-level improvements, namely support for destroying lambda on the target
176 // thread as well as custom return types.
177 template <typename Func>
runOnMain(Func && func)178 auto runOnMain(Func&& func) {
179 #ifdef _REENTRANT
180 if (!emscripten_is_main_runtime_thread()) {
181 if constexpr (std::is_same_v<std::invoke_result_t<Func>, void>) {
182 bool proxied =
183 queue.proxySync(emscripten_main_runtime_thread_id(), [&func] {
184 // Capture func by reference and move into a local variable
185 // to render the captured func inert on the first (and only)
186 // call. This way it can be safely destructed on the main
187 // thread instead of the current one when this call
188 // finishes. TODO: remove this when
189 // https://github.com/emscripten-core/emscripten/issues/20610
190 // is fixed.
191 auto func_ = std::move(func);
192 func_();
193 });
194 assert(proxied);
195 return;
196 } else {
197 // A storage for the result of the function call.
198 // TODO: remove when
199 // https://github.com/emscripten-core/emscripten/issues/20611 is
200 // implemented.
201 std::optional<std::invoke_result_t<Func>> result;
202 runOnMain(
203 [&result, func = std::move(func)] { result.emplace(func()); });
204 return std::move(result.value());
205 }
206 }
207 #endif
208 return func();
209 }
210
211 // C++ struct representation for `{value, error}` object used by `CaughtPromise`
212 // below.
213 struct PromiseResult {
214 int error;
215 val value;
216
217 PromiseResult() = delete;
218 PromiseResult(PromiseResult&&) = default;
219
PromiseResult__anon2d610b6a0111::PromiseResult220 PromiseResult(val&& result)
221 : error(result["error"].as<int>()), value(result["value"]) {}
222
~PromiseResult__anon2d610b6a0111::PromiseResult223 ~PromiseResult() {
224 // make sure value is freed on the thread it exists on
225 runOnMain([value = std::move(value)] {});
226 }
227 };
228
229 struct CaughtPromise : val {
CaughtPromise__anon2d610b6a0111::CaughtPromise230 CaughtPromise(val&& promise)
231 : val(wrapPromiseWithCatch(std::move(promise))) {}
232
233 using AwaitResult = PromiseResult;
234
235 private:
236
237 // Wrap promise with conversion from some value T to `{value: T, error:
238 // number}`.
wrapPromiseWithCatch__anon2d610b6a0111::CaughtPromise239 static val wrapPromiseWithCatch(val&& promise) {
240 auto handle = promise.as_handle();
241 handle = usbi_em_promise_catch(handle);
242 return val::take_ownership(handle);
243 }
244 };
245
246 #define co_await_try(promise) \
247 ({ \
248 PromiseResult result = co_await CaughtPromise(promise); \
249 if (result.error) { \
250 co_return result.error; \
251 } \
252 std::move(result.value); \
253 })
254
255 // A helper that runs an asynchronous callback when the promise is resolved.
256 template <typename Promise, typename OnResult>
promiseThen(Promise && promise,OnResult && onResult)257 val promiseThen(Promise&& promise, OnResult&& onResult) {
258 // Save captures from the callback while we can, or they'll be destructed.
259 // https://devblogs.microsoft.com/oldnewthing/20211103-00/?p=105870
260 auto onResult_ = std::move(onResult);
261 onResult_(co_await promise);
262 co_return val::undefined();
263 }
264
265 // A helper that runs an asynchronous function on the main thread and blocks the
266 // current thread until the promise is resolved (via Asyncify "blocking" if
267 // already on the main thread or regular blocking otherwise).
268 template <typename Func>
awaitOnMain(Func && func)269 static std::invoke_result_t<Func>::AwaitResult awaitOnMain(Func&& func) {
270 #ifdef _REENTRANT
271 if (!emscripten_is_main_runtime_thread()) {
272 // If we're on a different thread, we can't use main thread's Asyncify
273 // as multiple threads might be fighting for its state; instead, use
274 // proxying to synchronously block the current thread until the promise
275 // is complete.
276 std::optional<typename std::invoke_result_t<Func>::AwaitResult> result;
277 queue.proxySyncWithCtx(
278 emscripten_main_runtime_thread_id(),
279 [&result, &func](ProxyingQueue::ProxyingCtx ctx) {
280 // Same as `func` in `runOnMain`, move to destruct on the first
281 // call.
282 auto func_ = std::move(func);
283 promiseThen(
284 func_(),
285 [&result, ctx = std::move(ctx)](auto&& result_) mutable {
286 result.emplace(std::move(result_));
287 ctx.finish();
288 });
289 });
290 return std::move(result.value());
291 }
292 #endif
293 // If we're already on the main thread, use Asyncify to block until the
294 // promise is resolved.
295 return func().await();
296 }
297
298 // A helper that makes a control transfer given a setup pointer (assumed to be
299 // followed by data payload for out-transfers).
makeControlTransferPromise(const val & dev,libusb_control_setup * setup)300 val makeControlTransferPromise(const val& dev, libusb_control_setup* setup) {
301 auto params = val::object();
302
303 const char* request_type = "unknown";
304 // See LIBUSB_REQ_TYPE in windows_winusb.h (or docs for `bmRequestType`).
305 switch (setup->bmRequestType & (0x03 << 5)) {
306 case LIBUSB_REQUEST_TYPE_STANDARD:
307 request_type = "standard";
308 break;
309 case LIBUSB_REQUEST_TYPE_CLASS:
310 request_type = "class";
311 break;
312 case LIBUSB_REQUEST_TYPE_VENDOR:
313 request_type = "vendor";
314 break;
315 }
316 params.set("requestType", request_type);
317
318 const char* recipient = "other";
319 switch (setup->bmRequestType & 0x0f) {
320 case LIBUSB_RECIPIENT_DEVICE:
321 recipient = "device";
322 break;
323 case LIBUSB_RECIPIENT_INTERFACE:
324 recipient = "interface";
325 break;
326 case LIBUSB_RECIPIENT_ENDPOINT:
327 recipient = "endpoint";
328 break;
329 }
330 params.set("recipient", recipient);
331
332 params.set("request", setup->bRequest);
333 params.set("value", setup->wValue);
334 params.set("index", setup->wIndex);
335
336 if (setup->bmRequestType & LIBUSB_ENDPOINT_IN) {
337 return dev.call<val>("controlTransferIn", params, setup->wLength);
338 } else {
339 return dev.call<val>("controlTransferOut", params,
340 getUnsharedMemoryView(setup + 1, setup->wLength));
341 }
342 }
343
344 // Smart pointer for managing pointers to places allocated by libusb inside its
345 // backend structures.
346 template <typename T>
347 struct ValPtr {
348 template <typename... Args>
emplace__anon2d610b6a0111::ValPtr349 void emplace(Args&&... args) {
350 new (ptr) T(std::forward<Args>(args)...);
351 }
352
operator *__anon2d610b6a0111::ValPtr353 const T& operator*() const { return *ptr; }
operator *__anon2d610b6a0111::ValPtr354 T& operator*() { return *ptr; }
355
operator ->__anon2d610b6a0111::ValPtr356 const T* operator->() const { return ptr; }
operator ->__anon2d610b6a0111::ValPtr357 T* operator->() { return ptr; }
358
free__anon2d610b6a0111::ValPtr359 void free() { ptr->~T(); }
360
take__anon2d610b6a0111::ValPtr361 T take() {
362 auto value = std::move(*ptr);
363 free();
364 return value;
365 }
366
367 protected:
368
ValPtr__anon2d610b6a0111::ValPtr369 ValPtr(void* ptr) : ptr(static_cast<T*>(ptr)) {}
370
371 private:
372
373 // Note: this is not a heap-allocated pointer, but a pointer to a part
374 // of the backend structure allocated by libusb itself.
375 T* ptr;
376 };
377
378 struct CachedDevice;
379
380 struct WebUsbDevicePtr : ValPtr<CachedDevice> {
381 public:
382
WebUsbDevicePtr__anon2d610b6a0111::WebUsbDevicePtr383 WebUsbDevicePtr(libusb_device* dev) : ValPtr(usbi_get_device_priv(dev)) {}
WebUsbDevicePtr__anon2d610b6a0111::WebUsbDevicePtr384 WebUsbDevicePtr(libusb_device_handle* handle)
385 : WebUsbDevicePtr(handle->dev) {}
386 };
387
388 struct WebUsbTransferPtr : ValPtr<PromiseResult> {
389 public:
390
WebUsbTransferPtr__anon2d610b6a0111::WebUsbTransferPtr391 WebUsbTransferPtr(usbi_transfer* itransfer)
392 : ValPtr(usbi_get_transfer_priv(itransfer)) {}
393 };
394
395 enum class OpenClose : bool {
396 Open = true,
397 Close = false,
398 };
399
400 struct CachedDevice {
401 CachedDevice() = delete;
402 CachedDevice(CachedDevice&&) = delete;
403
404 // Fill in the device descriptor and configurations by reading them from the
405 // WebUSB device.
initFromDevice__anon2d610b6a0111::CachedDevice406 static val initFromDevice(val&& web_usb_dev, libusb_device* libusb_dev) {
407 auto cachedDevicePtr = WebUsbDevicePtr(libusb_dev);
408 cachedDevicePtr.emplace(std::move(web_usb_dev));
409 bool must_close = false;
410 val result = co_await cachedDevicePtr->initFromDeviceWithoutClosing(
411 libusb_dev, must_close);
412 if (must_close) {
413 co_await_try(cachedDevicePtr->safeOpenCloseAssumingMainThread(
414 OpenClose::Close));
415 }
416 co_return std::move(result);
417 }
418
getDeviceAssumingMainThread__anon2d610b6a0111::CachedDevice419 const val& getDeviceAssumingMainThread() const { return device; }
420
getActiveConfigValue__anon2d610b6a0111::CachedDevice421 uint8_t getActiveConfigValue() const {
422 return runOnMain([&] {
423 auto web_usb_config = device["configuration"];
424 return web_usb_config.isNull()
425 ? 0
426 : web_usb_config["configurationValue"].as<uint8_t>();
427 });
428 }
429
getConfigDescriptor__anon2d610b6a0111::CachedDevice430 usbi_configuration_descriptor* getConfigDescriptor(uint8_t config_id) {
431 return config_id < configurations.size()
432 ? configurations[config_id].get()
433 : nullptr;
434 }
435
findConfigDescriptorByValue__anon2d610b6a0111::CachedDevice436 usbi_configuration_descriptor* findConfigDescriptorByValue(
437 uint8_t config_id) const {
438 for (auto& config : configurations) {
439 if (config->bConfigurationValue == config_id) {
440 return config.get();
441 }
442 }
443 return nullptr;
444 }
445
copyConfigDescriptor__anon2d610b6a0111::CachedDevice446 int copyConfigDescriptor(const usbi_configuration_descriptor* config,
447 void* buf,
448 size_t buf_len) {
449 auto len = std::min(buf_len, (size_t)config->wTotalLength);
450 memcpy(buf, config, len);
451 return len;
452 }
453
454 template <typename... Args>
awaitOnMain__anon2d610b6a0111::CachedDevice455 int awaitOnMain(const char* methodName, Args&&... args) const {
456 return ::awaitOnMain([&] {
457 return CaughtPromise(device.call<val>(
458 methodName, std::forward<Args>(args)...));
459 })
460 .error;
461 }
462
~CachedDevice__anon2d610b6a0111::CachedDevice463 ~CachedDevice() {
464 runOnMain([device = std::move(device)] {});
465 }
466
safeOpenCloseAssumingMainThread__anon2d610b6a0111::CachedDevice467 CaughtPromise safeOpenCloseAssumingMainThread(OpenClose open) {
468 return val::take_ownership(usbi_em_device_safe_open_close(
469 device.as_handle(), static_cast<bool>(open)));
470 }
471
safeOpenCloseOnMain__anon2d610b6a0111::CachedDevice472 int safeOpenCloseOnMain(OpenClose open) {
473 return ::awaitOnMain([this, open] {
474 return safeOpenCloseAssumingMainThread(open);
475 })
476 .error;
477 }
478
479 private:
480
481 val device;
482 std::vector<std::unique_ptr<usbi_configuration_descriptor>> configurations;
483
requestDescriptor__anon2d610b6a0111::CachedDevice484 CaughtPromise requestDescriptor(libusb_descriptor_type desc_type,
485 uint8_t desc_index,
486 uint16_t max_length) const {
487 libusb_control_setup setup = {
488 .bmRequestType = LIBUSB_ENDPOINT_IN,
489 .bRequest = LIBUSB_REQUEST_GET_DESCRIPTOR,
490 .wValue = (uint16_t)((desc_type << 8) | desc_index),
491 .wIndex = 0,
492 .wLength = max_length,
493 };
494 return makeControlTransferPromise(device, &setup);
495 }
496
497 // Implementation of the `CachedDevice::initFromDevice` above. This is a
498 // separate function just because we need to close the device on exit if
499 // we opened it successfully, and we can't use an async operation (`close`)
500 // in RAII destructor.
initFromDeviceWithoutClosing__anon2d610b6a0111::CachedDevice501 val initFromDeviceWithoutClosing(libusb_device* dev, bool& must_close) {
502 co_await_try(safeOpenCloseAssumingMainThread(OpenClose::Open));
503
504 // Can't use RAII to close on exit as co_await is not permitted in
505 // destructors (yet:
506 // https://github.com/cplusplus/papers/issues/445), so use a good
507 // old boolean + a wrapper instead.
508 must_close = true;
509
510 {
511 auto result = co_await_try(
512 requestDescriptor(LIBUSB_DT_DEVICE, 0, LIBUSB_DT_DEVICE_SIZE));
513 if (auto error = getTransferStatus(result)) {
514 co_return error;
515 }
516 copyFromDataView(&dev->device_descriptor, result["data"]);
517 }
518
519 // Infer the device speed (which is not yet provided by WebUSB) from
520 // the descriptor.
521 if (dev->device_descriptor.bMaxPacketSize0 ==
522 /* actually means 2^9, only valid for superspeeds */ 9) {
523 dev->speed = dev->device_descriptor.bcdUSB >= 0x0310
524 ? LIBUSB_SPEED_SUPER_PLUS
525 : LIBUSB_SPEED_SUPER;
526 } else if (dev->device_descriptor.bcdUSB >= 0x0200) {
527 dev->speed = LIBUSB_SPEED_HIGH;
528 } else if (dev->device_descriptor.bMaxPacketSize0 > 8) {
529 dev->speed = LIBUSB_SPEED_FULL;
530 } else {
531 dev->speed = LIBUSB_SPEED_LOW;
532 }
533
534 if (auto error = usbi_sanitize_device(dev)) {
535 co_return error;
536 }
537
538 auto configurations_len = dev->device_descriptor.bNumConfigurations;
539 configurations.reserve(configurations_len);
540 for (uint8_t j = 0; j < configurations_len; j++) {
541 // Note: requesting more than (platform-specific limit) bytes
542 // here will cause the transfer to fail, see
543 // https://crbug.com/1489414. Use the most common limit of 4096
544 // bytes for now.
545 constexpr uint16_t MAX_CTRL_BUFFER_LENGTH = 4096;
546 auto result = co_await_try(
547 requestDescriptor(LIBUSB_DT_CONFIG, j, MAX_CTRL_BUFFER_LENGTH));
548 if (auto error = getTransferStatus(result)) {
549 co_return error;
550 }
551 auto configVal = result["data"];
552 auto configLen = configVal["byteLength"].as<size_t>();
553 auto& config = configurations.emplace_back(
554 (usbi_configuration_descriptor*)::operator new(configLen));
555 copyFromDataView(config.get(), configVal);
556 }
557
558 co_return (int) LIBUSB_SUCCESS;
559 }
560
CachedDevice__anon2d610b6a0111::CachedDevice561 CachedDevice(val device) : device(std::move(device)) {}
562
563 friend struct ValPtr<CachedDevice>;
564 };
565
getDeviceSessionId(val & web_usb_device)566 unsigned long getDeviceSessionId(val& web_usb_device) {
567 thread_local const val SessionIdSymbol =
568 val::global("Symbol")(val("libusb.session_id"));
569
570 val session_id_val = web_usb_device[SessionIdSymbol];
571 if (!session_id_val.isUndefined()) {
572 return session_id_val.as<unsigned long>();
573 }
574
575 // If the device doesn't have a session ID, it means we haven't seen
576 // it before. Generate a new session ID for it. We can associate an
577 // incrementing ID with the `USBDevice` object itself. It's
578 // guaranteed to be alive and, thus, stable as long as the device is
579 // connected, even between different libusb invocations. See
580 // https://github.com/WICG/webusb/issues/241.
581
582 static unsigned long next_session_id = 0;
583
584 web_usb_device.set(SessionIdSymbol, next_session_id);
585 return next_session_id++;
586 }
587
getDeviceList(libusb_context * ctx,discovered_devs ** devs)588 val getDeviceList(libusb_context* ctx, discovered_devs** devs) {
589 // Check if browser supports USB
590 val navigator_usb = val::global("navigator")["usb"];
591 if (navigator_usb == val::undefined()) {
592 co_return (int) LIBUSB_ERROR_NOT_SUPPORTED;
593 }
594 // C++ equivalent of `await navigator.usb.getDevices()`. Note: at this point
595 // we must already have some devices exposed - caller must have called
596 // `await navigator.usb.requestDevice(...)` in response to user interaction
597 // before going to LibUSB. Otherwise this list will be empty.
598 auto web_usb_devices =
599 co_await_try(navigator_usb.call<val>("getDevices"));
600 for (auto&& web_usb_device : web_usb_devices) {
601 auto session_id = getDeviceSessionId(web_usb_device);
602
603 auto dev = usbi_get_device_by_session_id(ctx, session_id);
604 if (dev == NULL) {
605 dev = usbi_alloc_device(ctx, session_id);
606 if (dev == NULL) {
607 usbi_err(ctx, "failed to allocate a new device structure");
608 continue;
609 }
610
611 auto statusVal = co_await CachedDevice::initFromDevice(
612 std::move(web_usb_device), dev);
613 if (auto error = statusVal.as<int>()) {
614 usbi_err(ctx, "failed to read device information: %s",
615 libusb_error_name(error));
616 libusb_unref_device(dev);
617 continue;
618 }
619
620 // We don't have real buses in WebUSB, just pretend everything
621 // is on bus 1.
622 dev->bus_number = 1;
623 // This can wrap around but it's the best approximation of a stable
624 // device address and port number we can provide.
625 dev->device_address = dev->port_number = (uint8_t)session_id;
626 }
627 *devs = discovered_devs_append(*devs, dev);
628 libusb_unref_device(dev);
629 }
630 co_return (int) LIBUSB_SUCCESS;
631 }
632
em_get_device_list(libusb_context * ctx,discovered_devs ** devs)633 int em_get_device_list(libusb_context* ctx, discovered_devs** devs) {
634 // No need to wrap into CaughtPromise as we catch all individual ops in the
635 // inner implementation and return just the error code. We do need a custom
636 // promise type to ensure conversion to int happens on the main thread
637 // though.
638 struct IntPromise : val {
639 IntPromise(val&& promise) : val(std::move(promise)) {}
640
641 struct AwaitResult {
642 int error;
643
644 AwaitResult(val&& result) : error(result.as<int>()) {}
645 };
646 };
647
648 return awaitOnMain(
649 [ctx, devs] { return IntPromise(getDeviceList(ctx, devs)); })
650 .error;
651 }
652
em_open(libusb_device_handle * handle)653 int em_open(libusb_device_handle* handle) {
654 return WebUsbDevicePtr(handle)->safeOpenCloseOnMain(OpenClose::Open);
655 }
656
em_close(libusb_device_handle * handle)657 void em_close(libusb_device_handle* handle) {
658 // LibUSB API doesn't allow us to handle an error here, but we still need to
659 // wait for the promise to make sure that subsequent attempt to reopen the
660 // same device doesn't fail with a "device busy" error.
661 if (auto error =
662 WebUsbDevicePtr(handle)->safeOpenCloseOnMain(OpenClose::Close)) {
663 usbi_err(handle->dev->ctx, "failed to close device: %s",
664 libusb_error_name(error));
665 }
666 }
667
em_get_active_config_descriptor(libusb_device * dev,void * buf,size_t len)668 int em_get_active_config_descriptor(libusb_device* dev, void* buf, size_t len) {
669 auto& cached_device = *WebUsbDevicePtr(dev);
670 auto config_value = cached_device.getActiveConfigValue();
671 if (auto config = cached_device.findConfigDescriptorByValue(config_value)) {
672 return cached_device.copyConfigDescriptor(config, buf, len);
673 } else {
674 return LIBUSB_ERROR_NOT_FOUND;
675 }
676 }
677
em_get_config_descriptor(libusb_device * dev,uint8_t config_id,void * buf,size_t len)678 int em_get_config_descriptor(libusb_device* dev,
679 uint8_t config_id,
680 void* buf,
681 size_t len) {
682 auto& cached_device = *WebUsbDevicePtr(dev);
683 if (auto config = cached_device.getConfigDescriptor(config_id)) {
684 return cached_device.copyConfigDescriptor(config, buf, len);
685 } else {
686 return LIBUSB_ERROR_NOT_FOUND;
687 }
688 }
689
em_get_configuration(libusb_device_handle * dev_handle,uint8_t * config_value)690 int em_get_configuration(libusb_device_handle* dev_handle,
691 uint8_t* config_value) {
692 *config_value = WebUsbDevicePtr(dev_handle)->getActiveConfigValue();
693 return LIBUSB_SUCCESS;
694 }
695
em_get_config_descriptor_by_value(libusb_device * dev,uint8_t config_value,void ** buf)696 int em_get_config_descriptor_by_value(libusb_device* dev,
697 uint8_t config_value,
698 void** buf) {
699 auto& cached_device = *WebUsbDevicePtr(dev);
700 if (auto config = cached_device.findConfigDescriptorByValue(config_value)) {
701 *buf = config;
702 return config->wTotalLength;
703 } else {
704 return LIBUSB_ERROR_NOT_FOUND;
705 }
706 }
707
em_set_configuration(libusb_device_handle * dev_handle,int config)708 int em_set_configuration(libusb_device_handle* dev_handle, int config) {
709 return WebUsbDevicePtr(dev_handle)->awaitOnMain("setConfiguration", config);
710 }
711
em_claim_interface(libusb_device_handle * handle,uint8_t iface)712 int em_claim_interface(libusb_device_handle* handle, uint8_t iface) {
713 return WebUsbDevicePtr(handle)->awaitOnMain("claimInterface", iface);
714 }
715
em_release_interface(libusb_device_handle * handle,uint8_t iface)716 int em_release_interface(libusb_device_handle* handle, uint8_t iface) {
717 return WebUsbDevicePtr(handle)->awaitOnMain("releaseInterface", iface);
718 }
719
em_set_interface_altsetting(libusb_device_handle * handle,uint8_t iface,uint8_t altsetting)720 int em_set_interface_altsetting(libusb_device_handle* handle,
721 uint8_t iface,
722 uint8_t altsetting) {
723 return WebUsbDevicePtr(handle)->awaitOnMain("selectAlternateInterface",
724 iface, altsetting);
725 }
726
em_clear_halt(libusb_device_handle * handle,unsigned char endpoint)727 int em_clear_halt(libusb_device_handle* handle, unsigned char endpoint) {
728 std::string direction = endpoint & LIBUSB_ENDPOINT_IN ? "in" : "out";
729 endpoint &= LIBUSB_ENDPOINT_ADDRESS_MASK;
730
731 return WebUsbDevicePtr(handle)->awaitOnMain("clearHalt", direction,
732 endpoint);
733 }
734
em_reset_device(libusb_device_handle * handle)735 int em_reset_device(libusb_device_handle* handle) {
736 return WebUsbDevicePtr(handle)->awaitOnMain("reset");
737 }
738
em_destroy_device(libusb_device * dev)739 void em_destroy_device(libusb_device* dev) {
740 WebUsbDevicePtr(dev).free();
741 }
742
em_submit_transfer(usbi_transfer * itransfer)743 int em_submit_transfer(usbi_transfer* itransfer) {
744 return runOnMain([itransfer] {
745 auto transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);
746 auto& web_usb_device = WebUsbDevicePtr(transfer->dev_handle)
747 ->getDeviceAssumingMainThread();
748 val transfer_promise;
749 switch (transfer->type) {
750 case LIBUSB_TRANSFER_TYPE_CONTROL: {
751 transfer_promise = makeControlTransferPromise(
752 web_usb_device,
753 libusb_control_transfer_get_setup(transfer));
754 break;
755 }
756 case LIBUSB_TRANSFER_TYPE_BULK:
757 case LIBUSB_TRANSFER_TYPE_INTERRUPT: {
758 auto endpoint =
759 transfer->endpoint & LIBUSB_ENDPOINT_ADDRESS_MASK;
760
761 if (IS_XFERIN(transfer)) {
762 transfer_promise = web_usb_device.call<val>(
763 "transferIn", endpoint, transfer->length);
764 } else {
765 auto data = getUnsharedMemoryView(transfer->buffer,
766 transfer->length);
767 transfer_promise =
768 web_usb_device.call<val>("transferOut", endpoint, data);
769 }
770
771 break;
772 }
773 // TODO: add implementation for isochronous transfers too.
774 default:
775 return LIBUSB_ERROR_NOT_SUPPORTED;
776 }
777 // Not a coroutine because we don't want to block on this promise, just
778 // schedule an asynchronous callback.
779 promiseThen(CaughtPromise(std::move(transfer_promise)),
780 [itransfer](auto&& result) {
781 WebUsbTransferPtr(itransfer).emplace(std::move(result));
782 usbi_signal_transfer_completion(itransfer);
783 });
784 return LIBUSB_SUCCESS;
785 });
786 }
787
em_clear_transfer_priv(usbi_transfer * itransfer)788 void em_clear_transfer_priv(usbi_transfer* itransfer) {
789 WebUsbTransferPtr(itransfer).free();
790 }
791
em_cancel_transfer(usbi_transfer * itransfer)792 int em_cancel_transfer(usbi_transfer* itransfer) {
793 return LIBUSB_SUCCESS;
794 }
795
em_handle_transfer_completion(usbi_transfer * itransfer)796 int em_handle_transfer_completion(usbi_transfer* itransfer) {
797 libusb_transfer_status status = runOnMain([itransfer] {
798 auto transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);
799
800 // Take ownership of the transfer result, as `em_clear_transfer_priv` is
801 // not called automatically for completed transfers and we must free it
802 // to avoid leaks.
803
804 auto result = WebUsbTransferPtr(itransfer).take();
805
806 if (itransfer->state_flags & USBI_TRANSFER_CANCELLING) {
807 return LIBUSB_TRANSFER_CANCELLED;
808 }
809
810 if (result.error) {
811 return LIBUSB_TRANSFER_ERROR;
812 }
813
814 auto& value = result.value;
815
816 void* dataDest;
817 unsigned char endpointDir;
818
819 if (transfer->type == LIBUSB_TRANSFER_TYPE_CONTROL) {
820 dataDest = libusb_control_transfer_get_data(transfer);
821 endpointDir =
822 libusb_control_transfer_get_setup(transfer)->bmRequestType;
823 } else {
824 dataDest = transfer->buffer;
825 endpointDir = transfer->endpoint;
826 }
827
828 if (endpointDir & LIBUSB_ENDPOINT_IN) {
829 auto data = value["data"];
830 if (!data.isNull()) {
831 itransfer->transferred = data["byteLength"].as<int>();
832 copyFromDataView(dataDest, data);
833 }
834 } else {
835 itransfer->transferred = value["bytesWritten"].as<int>();
836 }
837
838 return getTransferStatus(value);
839 });
840
841 // Invoke user's handlers outside of the main thread to reduce pressure.
842 return status == LIBUSB_TRANSFER_CANCELLED
843 ? usbi_handle_transfer_cancellation(itransfer)
844 : usbi_handle_transfer_completion(itransfer, status);
845 }
846
847 } // namespace
848
849 #pragma clang diagnostic ignored "-Wmissing-field-initializers"
850 extern "C" const usbi_os_backend usbi_backend = {
851 .name = "Emscripten + WebUSB backend",
852 .caps = 0,
853 .get_device_list = em_get_device_list,
854 .open = em_open,
855 .close = em_close,
856 .get_active_config_descriptor = em_get_active_config_descriptor,
857 .get_config_descriptor = em_get_config_descriptor,
858 .get_config_descriptor_by_value = em_get_config_descriptor_by_value,
859 .get_configuration = em_get_configuration,
860 .set_configuration = em_set_configuration,
861 .claim_interface = em_claim_interface,
862 .release_interface = em_release_interface,
863 .set_interface_altsetting = em_set_interface_altsetting,
864 .clear_halt = em_clear_halt,
865 .reset_device = em_reset_device,
866 .destroy_device = em_destroy_device,
867 .submit_transfer = em_submit_transfer,
868 .cancel_transfer = em_cancel_transfer,
869 .clear_transfer_priv = em_clear_transfer_priv,
870 .handle_transfer_completion = em_handle_transfer_completion,
871 .device_priv_size = sizeof(CachedDevice),
872 .transfer_priv_size = sizeof(PromiseResult),
873 };
874
875 #pragma clang diagnostic pop
876