xref: /aosp_15_r20/external/libusb/libusb/os/emscripten_webusb.cpp (revision 86b64dcb59b3a0b37502ecd56e119234366a6f7e)
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