1 /*
2 * Copyright © 2021 Google LLC
3 * Copyright © 2023 Ingvar Stepanyan <me@rreverser.com>
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 <me@rreverser.com>
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__anon1c2375080111::PromiseResult220 PromiseResult(val&& result)
221 : error(result["error"].as<int>()), value(result["value"]) {}
222
~PromiseResult__anon1c2375080111::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__anon1c2375080111::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__anon1c2375080111::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__anon1c2375080111::ValPtr349 void emplace(Args&&... args) {
350 new (ptr) T(std::forward<Args>(args)...);
351 }
352
operator *__anon1c2375080111::ValPtr353 const T& operator*() const { return *ptr; }
operator *__anon1c2375080111::ValPtr354 T& operator*() { return *ptr; }
355
operator ->__anon1c2375080111::ValPtr356 const T* operator->() const { return ptr; }
operator ->__anon1c2375080111::ValPtr357 T* operator->() { return ptr; }
358
free__anon1c2375080111::ValPtr359 void free() { ptr->~T(); }
360
take__anon1c2375080111::ValPtr361 T take() {
362 auto value = std::move(*ptr);
363 free();
364 return value;
365 }
366
367 protected:
368
ValPtr__anon1c2375080111::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__anon1c2375080111::WebUsbDevicePtr383 WebUsbDevicePtr(libusb_device* dev) : ValPtr(usbi_get_device_priv(dev)) {}
WebUsbDevicePtr__anon1c2375080111::WebUsbDevicePtr384 WebUsbDevicePtr(libusb_device_handle* handle)
385 : WebUsbDevicePtr(handle->dev) {}
386 };
387
388 struct WebUsbTransferPtr : ValPtr<PromiseResult> {
389 public:
390
WebUsbTransferPtr__anon1c2375080111::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__anon1c2375080111::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__anon1c2375080111::CachedDevice419 const val& getDeviceAssumingMainThread() const { return device; }
420
getActiveConfigValue__anon1c2375080111::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__anon1c2375080111::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__anon1c2375080111::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__anon1c2375080111::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__anon1c2375080111::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__anon1c2375080111::CachedDevice463 ~CachedDevice() {
464 runOnMain([device = std::move(device)] {});
465 }
466
safeOpenCloseAssumingMainThread__anon1c2375080111::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__anon1c2375080111::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__anon1c2375080111::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__anon1c2375080111::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__anon1c2375080111::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 // C++ equivalent of `await navigator.usb.getDevices()`. Note: at this point
590 // we must already have some devices exposed - caller must have called
591 // `await navigator.usb.requestDevice(...)` in response to user interaction
592 // before going to LibUSB. Otherwise this list will be empty.
593 auto web_usb_devices =
594 co_await_try(val::global("navigator")["usb"].call<val>("getDevices"));
595 for (auto&& web_usb_device : web_usb_devices) {
596 auto session_id = getDeviceSessionId(web_usb_device);
597
598 auto dev = usbi_get_device_by_session_id(ctx, session_id);
599 if (dev == NULL) {
600 dev = usbi_alloc_device(ctx, session_id);
601 if (dev == NULL) {
602 usbi_err(ctx, "failed to allocate a new device structure");
603 continue;
604 }
605
606 auto statusVal = co_await CachedDevice::initFromDevice(
607 std::move(web_usb_device), dev);
608 if (auto error = statusVal.as<int>()) {
609 usbi_err(ctx, "failed to read device information: %s",
610 libusb_error_name(error));
611 libusb_unref_device(dev);
612 continue;
613 }
614
615 // We don't have real buses in WebUSB, just pretend everything
616 // is on bus 1.
617 dev->bus_number = 1;
618 // This can wrap around but it's the best approximation of a stable
619 // device address and port number we can provide.
620 dev->device_address = dev->port_number = (uint8_t)session_id;
621 }
622 *devs = discovered_devs_append(*devs, dev);
623 libusb_unref_device(dev);
624 }
625 co_return (int) LIBUSB_SUCCESS;
626 }
627
em_get_device_list(libusb_context * ctx,discovered_devs ** devs)628 int em_get_device_list(libusb_context* ctx, discovered_devs** devs) {
629 // No need to wrap into CaughtPromise as we catch all individual ops in the
630 // inner implementation and return just the error code. We do need a custom
631 // promise type to ensure conversion to int happens on the main thread
632 // though.
633 struct IntPromise : val {
634 IntPromise(val&& promise) : val(std::move(promise)) {}
635
636 struct AwaitResult {
637 int error;
638
639 AwaitResult(val&& result) : error(result.as<int>()) {}
640 };
641 };
642
643 return awaitOnMain(
644 [ctx, devs] { return IntPromise(getDeviceList(ctx, devs)); })
645 .error;
646 }
647
em_open(libusb_device_handle * handle)648 int em_open(libusb_device_handle* handle) {
649 return WebUsbDevicePtr(handle)->safeOpenCloseOnMain(OpenClose::Open);
650 }
651
em_close(libusb_device_handle * handle)652 void em_close(libusb_device_handle* handle) {
653 // LibUSB API doesn't allow us to handle an error here, but we still need to
654 // wait for the promise to make sure that subsequent attempt to reopen the
655 // same device doesn't fail with a "device busy" error.
656 if (auto error =
657 WebUsbDevicePtr(handle)->safeOpenCloseOnMain(OpenClose::Close)) {
658 usbi_err(handle->dev->ctx, "failed to close device: %s",
659 libusb_error_name(error));
660 }
661 }
662
em_get_active_config_descriptor(libusb_device * dev,void * buf,size_t len)663 int em_get_active_config_descriptor(libusb_device* dev, void* buf, size_t len) {
664 auto& cached_device = *WebUsbDevicePtr(dev);
665 auto config_value = cached_device.getActiveConfigValue();
666 if (auto config = cached_device.findConfigDescriptorByValue(config_value)) {
667 return cached_device.copyConfigDescriptor(config, buf, len);
668 } else {
669 return LIBUSB_ERROR_NOT_FOUND;
670 }
671 }
672
em_get_config_descriptor(libusb_device * dev,uint8_t config_id,void * buf,size_t len)673 int em_get_config_descriptor(libusb_device* dev,
674 uint8_t config_id,
675 void* buf,
676 size_t len) {
677 auto& cached_device = *WebUsbDevicePtr(dev);
678 if (auto config = cached_device.getConfigDescriptor(config_id)) {
679 return cached_device.copyConfigDescriptor(config, buf, len);
680 } else {
681 return LIBUSB_ERROR_NOT_FOUND;
682 }
683 }
684
em_get_configuration(libusb_device_handle * dev_handle,uint8_t * config_value)685 int em_get_configuration(libusb_device_handle* dev_handle,
686 uint8_t* config_value) {
687 *config_value = WebUsbDevicePtr(dev_handle)->getActiveConfigValue();
688 return LIBUSB_SUCCESS;
689 }
690
em_get_config_descriptor_by_value(libusb_device * dev,uint8_t config_value,void ** buf)691 int em_get_config_descriptor_by_value(libusb_device* dev,
692 uint8_t config_value,
693 void** buf) {
694 auto& cached_device = *WebUsbDevicePtr(dev);
695 if (auto config = cached_device.findConfigDescriptorByValue(config_value)) {
696 *buf = config;
697 return config->wTotalLength;
698 } else {
699 return LIBUSB_ERROR_NOT_FOUND;
700 }
701 }
702
em_set_configuration(libusb_device_handle * dev_handle,int config)703 int em_set_configuration(libusb_device_handle* dev_handle, int config) {
704 return WebUsbDevicePtr(dev_handle)->awaitOnMain("setConfiguration", config);
705 }
706
em_claim_interface(libusb_device_handle * handle,uint8_t iface)707 int em_claim_interface(libusb_device_handle* handle, uint8_t iface) {
708 return WebUsbDevicePtr(handle)->awaitOnMain("claimInterface", iface);
709 }
710
em_release_interface(libusb_device_handle * handle,uint8_t iface)711 int em_release_interface(libusb_device_handle* handle, uint8_t iface) {
712 return WebUsbDevicePtr(handle)->awaitOnMain("releaseInterface", iface);
713 }
714
em_set_interface_altsetting(libusb_device_handle * handle,uint8_t iface,uint8_t altsetting)715 int em_set_interface_altsetting(libusb_device_handle* handle,
716 uint8_t iface,
717 uint8_t altsetting) {
718 return WebUsbDevicePtr(handle)->awaitOnMain("selectAlternateInterface",
719 iface, altsetting);
720 }
721
em_clear_halt(libusb_device_handle * handle,unsigned char endpoint)722 int em_clear_halt(libusb_device_handle* handle, unsigned char endpoint) {
723 std::string direction = endpoint & LIBUSB_ENDPOINT_IN ? "in" : "out";
724 endpoint &= LIBUSB_ENDPOINT_ADDRESS_MASK;
725
726 return WebUsbDevicePtr(handle)->awaitOnMain("clearHalt", direction,
727 endpoint);
728 }
729
em_reset_device(libusb_device_handle * handle)730 int em_reset_device(libusb_device_handle* handle) {
731 return WebUsbDevicePtr(handle)->awaitOnMain("reset");
732 }
733
em_destroy_device(libusb_device * dev)734 void em_destroy_device(libusb_device* dev) {
735 WebUsbDevicePtr(dev).free();
736 }
737
em_submit_transfer(usbi_transfer * itransfer)738 int em_submit_transfer(usbi_transfer* itransfer) {
739 return runOnMain([itransfer] {
740 auto transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);
741 auto& web_usb_device = WebUsbDevicePtr(transfer->dev_handle)
742 ->getDeviceAssumingMainThread();
743 val transfer_promise;
744 switch (transfer->type) {
745 case LIBUSB_TRANSFER_TYPE_CONTROL: {
746 transfer_promise = makeControlTransferPromise(
747 web_usb_device,
748 libusb_control_transfer_get_setup(transfer));
749 break;
750 }
751 case LIBUSB_TRANSFER_TYPE_BULK:
752 case LIBUSB_TRANSFER_TYPE_INTERRUPT: {
753 auto endpoint =
754 transfer->endpoint & LIBUSB_ENDPOINT_ADDRESS_MASK;
755
756 if (IS_XFERIN(transfer)) {
757 transfer_promise = web_usb_device.call<val>(
758 "transferIn", endpoint, transfer->length);
759 } else {
760 auto data = getUnsharedMemoryView(transfer->buffer,
761 transfer->length);
762 transfer_promise =
763 web_usb_device.call<val>("transferOut", endpoint, data);
764 }
765
766 break;
767 }
768 // TODO: add implementation for isochronous transfers too.
769 default:
770 return LIBUSB_ERROR_NOT_SUPPORTED;
771 }
772 // Not a coroutine because we don't want to block on this promise, just
773 // schedule an asynchronous callback.
774 promiseThen(CaughtPromise(std::move(transfer_promise)),
775 [itransfer](auto&& result) {
776 WebUsbTransferPtr(itransfer).emplace(std::move(result));
777 usbi_signal_transfer_completion(itransfer);
778 });
779 return LIBUSB_SUCCESS;
780 });
781 }
782
em_clear_transfer_priv(usbi_transfer * itransfer)783 void em_clear_transfer_priv(usbi_transfer* itransfer) {
784 WebUsbTransferPtr(itransfer).free();
785 }
786
em_cancel_transfer(usbi_transfer * itransfer)787 int em_cancel_transfer(usbi_transfer* itransfer) {
788 return LIBUSB_SUCCESS;
789 }
790
em_handle_transfer_completion(usbi_transfer * itransfer)791 int em_handle_transfer_completion(usbi_transfer* itransfer) {
792 libusb_transfer_status status = runOnMain([itransfer] {
793 auto transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);
794
795 // Take ownership of the transfer result, as `em_clear_transfer_priv` is
796 // not called automatically for completed transfers and we must free it
797 // to avoid leaks.
798
799 auto result = WebUsbTransferPtr(itransfer).take();
800
801 if (itransfer->state_flags & USBI_TRANSFER_CANCELLING) {
802 return LIBUSB_TRANSFER_CANCELLED;
803 }
804
805 if (result.error) {
806 return LIBUSB_TRANSFER_ERROR;
807 }
808
809 auto& value = result.value;
810
811 void* dataDest;
812 unsigned char endpointDir;
813
814 if (transfer->type == LIBUSB_TRANSFER_TYPE_CONTROL) {
815 dataDest = libusb_control_transfer_get_data(transfer);
816 endpointDir =
817 libusb_control_transfer_get_setup(transfer)->bmRequestType;
818 } else {
819 dataDest = transfer->buffer;
820 endpointDir = transfer->endpoint;
821 }
822
823 if (endpointDir & LIBUSB_ENDPOINT_IN) {
824 auto data = value["data"];
825 if (!data.isNull()) {
826 itransfer->transferred = data["byteLength"].as<int>();
827 copyFromDataView(dataDest, data);
828 }
829 } else {
830 itransfer->transferred = value["bytesWritten"].as<int>();
831 }
832
833 return getTransferStatus(value);
834 });
835
836 // Invoke user's handlers outside of the main thread to reduce pressure.
837 return status == LIBUSB_TRANSFER_CANCELLED
838 ? usbi_handle_transfer_cancellation(itransfer)
839 : usbi_handle_transfer_completion(itransfer, status);
840 }
841
842 } // namespace
843
844 #pragma clang diagnostic ignored "-Wmissing-field-initializers"
845 extern "C" const usbi_os_backend usbi_backend = {
846 .name = "Emscripten + WebUSB backend",
847 .caps = 0,
848 .get_device_list = em_get_device_list,
849 .open = em_open,
850 .close = em_close,
851 .get_active_config_descriptor = em_get_active_config_descriptor,
852 .get_config_descriptor = em_get_config_descriptor,
853 .get_config_descriptor_by_value = em_get_config_descriptor_by_value,
854 .get_configuration = em_get_configuration,
855 .set_configuration = em_set_configuration,
856 .claim_interface = em_claim_interface,
857 .release_interface = em_release_interface,
858 .set_interface_altsetting = em_set_interface_altsetting,
859 .clear_halt = em_clear_halt,
860 .reset_device = em_reset_device,
861 .destroy_device = em_destroy_device,
862 .submit_transfer = em_submit_transfer,
863 .cancel_transfer = em_cancel_transfer,
864 .clear_transfer_priv = em_clear_transfer_priv,
865 .handle_transfer_completion = em_handle_transfer_completion,
866 .device_priv_size = sizeof(CachedDevice),
867 .transfer_priv_size = sizeof(PromiseResult),
868 };
869
870 #pragma clang diagnostic pop
871