USB 애플리케이션을 웹에 포팅 파트 1: libusb

외부 기기와 상호작용하는 코드를 WebAssembly 및 Fugu API를 사용하여 웹으로 포팅하는 방법을 알아봅니다.

이전 게시물에서는 File System Access API, WebAssembly, Asyncify를 사용하여 파일 시스템 API를 사용하는 앱을 웹으로 포팅하는 방법을 설명했습니다. 이제 Fugu API를 WebAssembly와 통합하고 중요한 기능을 잃지 않고 앱을 웹으로 포팅하는 것과 동일한 주제를 계속 살펴보겠습니다.

C로 작성된 인기 있는 USB 라이브러리인 libusbEmscripten, Asyncify, WebUSB를 통해 WebAssembly로 포팅하여 USB 기기와 통신하는 앱을 웹으로 포팅하는 방법을 보여줍니다.

라이브러리를 포팅할 때 가장 중요한 것은 적절한 데모를 선택하는 것입니다. 포팅된 라이브러리의 기능을 보여주고 다양한 방식으로 테스트할 수 있으며 동시에 시각적으로 매력적인 데모를 선택해야 합니다.

제가 선택한 아이디어는 DSLR 리모컨이었습니다. 특히 오픈소스 프로젝트인 gPhoto2는 오랫동안 이 분야에 있어 다양한 디지털 카메라를 역엔지니어링하고 지원을 구현해 왔습니다. 여러 프로토콜을 지원하지만 가장 관심이 가는 프로토콜은 libusb를 통해 실행되는 USB 지원입니다.

이 데모를 빌드하는 단계를 두 부분으로 설명하겠습니다. 이 블로그 게시물에서는 libusb 자체를 포팅한 방법과 다른 인기 라이브러리를 Fugu API로 포팅하는 데 필요한 트릭을 설명합니다. 두 번째 게시물에서는 gPhoto2 자체의 포팅 및 통합에 대해 자세히 설명합니다.

결국 DSLR의 라이브 피드를 미리 보고 USB를 통해 설정을 제어할 수 있는 작동하는 웹 애플리케이션을 만들었습니다. 기술적 세부정보를 읽기 전에 라이브 또는 사전 녹화된 데모를 확인해 보세요.

소니 카메라에 연결된 노트북에서 실행되는 데모

카메라별 특이사항에 관한 참고사항

동영상에서 설정을 변경하는 데 시간이 걸리는 것을 눈치채셨을 수도 있습니다. 다른 대부분의 문제와 마찬가지로 이 문제는 WebAssembly 또는 WebUSB의 성능이 아니라 gPhoto2가 데모에 선택된 특정 카메라와 상호작용하는 방식으로 인해 발생합니다.

Sony a6600은 ISO, 조리개, 셔터 속도와 같은 값을 직접 설정하는 API를 노출하지 않으며 대신 지정된 단계 수만큼 값을 늘리거나 줄이는 명령어만 제공합니다. 더 복잡하게도 실제로 지원되는 값 목록도 반환하지 않습니다. 반환된 목록은 여러 소니 카메라 모델에 하드코딩된 것 같습니다.

이러한 값 중 하나를 설정할 때 gPhoto2는 다음을 실행할 수밖에 없습니다.

  1. 선택한 값의 방향으로 한 걸음 또는 여러 걸음 옮깁니다.
  2. 카메라가 설정을 업데이트할 때까지 잠시 기다립니다.
  3. 카메라가 실제로 도달한 값을 다시 읽습니다.
  4. 마지막 단계에서 원하는 값을 건너뛰거나 목록의 끝이나 시작 부분을 둘러싸지 않았는지 확인합니다.
  5. 반복

시간이 다소 걸릴 수 있지만 카메라에서 실제로 값을 지원하는 경우 값이 도달하고 지원되지 않는 경우 가장 가까운 지원되는 값에서 중지됩니다.

다른 카메라의 설정, 기본 API, 버그는 다를 수 있습니다. gPhoto2는 오픈소스 프로젝트이며 시중에 나와 있는 모든 카메라 모델을 자동 또는 수동으로 테스트하는 것은 불가능하므로 자세한 문제 신고 및 PR은 언제나 환영됩니다 (단, 먼저 공식 gPhoto2 클라이언트로 문제를 재현해야 함).

중요한 교차 플랫폼 호환성 참고사항

안타깝게도 Windows에서는 DSLR 카메라를 비롯한 '잘 알려진' 기기에 WebUSB와 호환되지 않는 시스템 드라이버가 할당됩니다. Windows에서 데모를 사용해 보려면 Zadig와 같은 도구를 사용하여 연결된 DSLR의 드라이버를 WinUSB 또는 libusb로 재정의해야 합니다. 이 접근 방식은 저와 다른 많은 사용자에게 잘 작동하지만, 이 방법을 사용할 때는 위험을 감수해야 합니다.

Linux에서는 배포에 따라 다르지만 WebUSB를 통해 DSLR에 액세스할 수 있도록 맞춤 권한을 설정해야 할 수 있습니다.

macOS 및 Android에서는 데모가 즉시 작동합니다. Android 휴대전화에서 사용해 보려면 가로 모드로 전환하세요. 반응형으로 만들기 위해 많은 노력을 기울이지 않았습니다 (PR 환영).

USB-C 케이블을 통해 Canon 카메라에 연결된 Android 휴대전화
Android 휴대전화에서 실행되는 동일한 데모 Surma님 제공 사진

WebUSB의 교차 플랫폼 사용에 관한 자세한 가이드는 'WebUSB용 기기 빌드'의 '플랫폼별 고려사항' 섹션을 참고하세요.

libusb에 새 백엔드 추가

이제 기술 세부정보를 살펴보겠습니다. libusb와 유사한 shim API를 제공하고 (이전에는 다른 사용자가 수행함) 다른 애플리케이션을 이에 연결할 수는 있지만, 이 접근 방식은 오류가 발생하기 쉽고 추가 확장이나 유지보수가 더 어려워집니다. 향후 업스트림에 다시 기여하고 libusb에 병합할 수 있는 방식으로 올바르게 처리하고 싶었습니다.

다행히 libusb 리드미에 다음과 같이 설명되어 있습니다.

"libusb는 다른 운영체제로 포팅될 수 있도록 내부적으로 추상화됩니다. 자세한 내용은 PORTING 파일을 참고하세요."

libusb는 공개 API가 '백엔드'와 분리된 방식으로 구성됩니다. 이러한 백엔드는 운영체제의 하위 수준 API를 통해 기기를 나열, 열기, 닫기, 실제로 통신하는 역할을 합니다. 이러한 방식으로 libusb는 이미 Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku, Solaris 간의 차이점을 추상화하고 이러한 모든 플랫폼에서 작동합니다.

Emscripten+WebUSB '운영체제'의 백엔드를 하나 더 추가해야 했습니다. 이러한 백엔드의 구현은 libusb/os 폴더에 있습니다.

~/w/d/libusb $ ls libusb/os
darwin_usb
.c           haiku_usb_raw.h  threads_posix.lo
darwin_usb
.h           linux_netlink.c  threads_posix.o
events_posix
.c         linux_udev.c     threads_windows.c
events_posix
.h         linux_usbfs.c    threads_windows.h
events_posix
.lo        linux_usbfs.h    windows_common.c
events_posix
.o         netbsd_usb.c     windows_common.h
events_windows
.c       null_usb.c       windows_usbdk.c
events_windows
.h       openbsd_usb.c    windows_usbdk.h
haiku_pollfs
.cpp       sunos_usb.c      windows_winusb.c
haiku_usb_backend
.cpp  sunos_usb.h      windows_winusb.h
haiku_usb
.h            threads_posix.c
haiku_usb_raw
.cpp      threads_posix.h

각 백엔드에는 공통 유형 및 도우미가 포함된 libusbi.h 헤더가 포함되며 usbi_os_backend 유형의 usbi_backend 변수를 노출해야 합니다. 예를 들어 Windows 백엔드는 다음과 같습니다.

const struct usbi_os_backend usbi_backend = {
 
"Windows",
  USBI_CAP_HAS_HID_ACCESS
,
  windows_init
,
  windows_exit
,
  windows_set_option
,
  windows_get_device_list
,
  NULL
,   /* hotplug_poll */
  NULL
,   /* wrap_sys_device */
  windows_open
,
  windows_close
,
  windows_get_active_config_descriptor
,
  windows_get_config_descriptor
,
  windows_get_config_descriptor_by_value
,
  windows_get_configuration
,
  windows_set_configuration
,
  windows_claim_interface
,
  windows_release_interface
,
  windows_set_interface_altsetting
,
  windows_clear_halt
,
  windows_reset_device
,
  NULL
,   /* alloc_streams */
  NULL
,   /* free_streams */
  NULL
,   /* dev_mem_alloc */
  NULL
,   /* dev_mem_free */
  NULL
,   /* kernel_driver_active */
  NULL
,   /* detach_kernel_driver */
  NULL
,   /* attach_kernel_driver */
  windows_destroy_device
,
  windows_submit_transfer
,
  windows_cancel_transfer
,
  NULL
,   /* clear_transfer_priv */
  NULL
,   /* handle_events */
  windows_handle_transfer_completion
,
 
sizeof(struct windows_context_priv),
 
sizeof(union windows_device_priv),
 
sizeof(struct windows_device_handle_priv),
 
sizeof(struct windows_transfer_priv),
};

속성을 살펴보면 구조체에 백엔드 이름, 기능 집합, 함수 포인터 형식의 다양한 하위 수준 USB 작업 핸들러, 마지막으로 비공개 기기/컨텍스트/전송 수준 데이터 저장을 위해 할당할 크기가 포함되어 있습니다.

비공개 데이터 필드는 적어도 이러한 모든 항목의 OS 핸들을 저장하는 데 유용합니다. 핸들이 없으면 특정 작업이 적용되는 항목을 알 수 없기 때문입니다. 웹 구현에서 OS 핸들은 기본 WebUSB JavaScript 객체입니다. Emscripten에서 이를 나타내고 저장하는 자연스러운 방법은 Embind (Emscripten의 바인딩 시스템)의 일부로 제공되는 emscripten::val 클래스를 사용하는 것입니다.

폴더의 대부분의 백엔드는 C로 구현되지만 일부는 C++로 구현됩니다. Embind는 C++에서만 작동하므로 선택사항이 자동으로 선택되었으며 필수 구조가 있는 libusb/libusb/os/emscripten_webusb.cpp와 비공개 데이터 필드의 sizeof(val)를 추가했습니다.

#include <emscripten.h>
#include <emscripten/val.h>

#include "libusbi.h"

using namespace emscripten;

// …function implementations

const usbi_os_backend usbi_backend = {
 
.name = "Emscripten + WebUSB backend",
 
.caps = LIBUSB_CAP_HAS_CAPABILITY,
 
// …handlers—function pointers to implementations above
 
.device_priv_size = sizeof(val),
 
.transfer_priv_size = sizeof(val),
};

WebUSB 객체를 기기 핸들로 저장

libusb는 비공개 데이터에 할당된 영역을 가리키는 즉시 사용할 수 있는 포인터를 제공합니다. 이러한 포인터를 val 인스턴스로 사용하려면 포인터를 인플레이스에서 생성하고 참조로 검색하고 값을 외부로 이동하는 작은 도우미를 추가했습니다.

// We store an Embind handle to WebUSB USBDevice in "priv" metadata of
// libusb device, this helper returns a pointer to it.
struct ValPtr {
 
public:
 
void init_to(val &&value) { new (ptr) val(std::move(value)); }

  val
&get() { return *ptr; }
  val take
() { return std::move(get()); }

 
protected:
 
ValPtr(val *ptr) : ptr(ptr) {}

 
private:
  val
*ptr;
};

struct WebUsbDevicePtr : ValPtr {
 
public:
 
WebUsbDevicePtr(libusb_device *dev)
     
: ValPtr(static_cast<val *>(usbi_get_device_priv(dev))) {}
};

val
&get_web_usb_device(libusb_device *dev) {
 
return WebUsbDevicePtr(dev).get();
}

struct WebUsbTransferPtr : ValPtr {
 
public:
 
WebUsbTransferPtr(usbi_transfer *itransfer)
     
: ValPtr(static_cast<val *>(usbi_get_transfer_priv(itransfer))) {}
};

동기식 C 컨텍스트의 비동기 웹 API

이제 libusb가 동기 작업을 예상하는 비동기 WebUSB API를 처리하는 방법이 필요했습니다. 이를 위해 Asyncify 또는 더 구체적으로 val::await()를 통한 Embind 통합을 사용할 수 있습니다.

WebUSB 오류를 올바르게 처리하고 libusb 오류 코드로 변환하고 싶었지만 현재 Embind에는 C++ 측에서 JavaScript 예외 또는 Promise 거부를 처리할 방법이 없습니다. 이 문제는 JavaScript 측에서 거부를 포착하고 결과를 C++ 측에서 안전하게 파싱할 수 있는 { error, value } 객체로 변환하여 해결할 수 있습니다. EM_JS 매크로와 Emval.to{Handle, Value} API를 조합하여 이 작업을 실행했습니다.

EM_JS(EM_VAL, em_promise_catch_impl, (EM_VAL handle), {
  let promise
= Emval.toValue(handle);
  promise
= promise.then(
    value
=> ({error : 0, value}),
    error
=> {
     
const ERROR_CODES = {
       
// LIBUSB_ERROR_IO
       
NetworkError : -1,
       
// LIBUSB_ERROR_INVALID_PARAM
       
DataError : -2,
       
TypeMismatchError : -2,
       
IndexSizeError : -2,
       
// LIBUSB_ERROR_ACCESS
       
SecurityError : -3,
       

     
};
      console
.error(error);
      let errorCode
= -99; // LIBUSB_ERROR_OTHER
     
if (error instanceof DOMException)
     
{
        errorCode
= ERROR_CODES[error.name] ?? errorCode;
     
}
     
else if (error instanceof RangeError || error instanceof TypeError)
     
{
        errorCode
= -2; // LIBUSB_ERROR_INVALID_PARAM
     
}
     
return {error: errorCode, value: undefined};
   
}
 
);
 
return Emval.toHandle(promise);
});

val em_promise_catch
(val &&promise) {
  EM_VAL handle
= promise.as_handle();
  handle
= em_promise_catch_impl(handle);
 
return val::take_ownership(handle);
}

// C++ struct representation for {value, error} object from above
// (performs conversion in the constructor).
struct promise_result {
  libusb_error error
;
  val value
;

  promise_result
(val &&result)
     
: error(static_cast<libusb_error>(result["error"].as<int>())),
        value
(result["value"]) {}

 
// C++ counterpart of the promise helper above that takes a promise, catches
 
// its error, converts to a libusb status and returns the whole thing as
 
// `promise_result` struct for easier handling.
 
static promise_result await(val &&promise) {
    promise
= em_promise_catch(std::move(promise));
   
return {promise.await()};
 
}
};

이제 WebUSB 작업에서 반환된 모든 Promisepromise_result::await()를 사용하고 errorvalue 필드를 별도로 검사할 수 있습니다.

예를 들어 libusb_device_handle에서 USBDevice를 나타내는 val를 검색하고, open() 메서드를 호출하고, 결과를 기다린 후 오류 코드를 libusb 상태 코드로 반환하는 작업은 다음과 같습니다.

int em_open(libusb_device_handle *handle) {
 
auto web_usb_device = get_web_usb_device(handle->dev);
 
return promise_result::await(web_usb_device.call<val>("open")).error;
}

기기 열거

물론 기기를 열기 전에 libusb는 사용 가능한 기기 목록을 가져와야 합니다. 백엔드는 get_device_list 핸들러를 통해 이 작업을 구현해야 합니다.

문제는 다른 플랫폼과 달리 보안상의 이유로 웹에서 연결된 모든 USB 기기를 열거할 방법이 없다는 것입니다. 대신 흐름이 두 부분으로 나뉩니다. 먼저 웹 애플리케이션이 navigator.usb.requestDevice()를 통해 특정 속성이 있는 기기를 요청하고 사용자는 노출할 기기를 수동으로 선택하거나 권한 메시지를 거부합니다. 그런 다음 애플리케이션은 navigator.usb.getDevices()를 통해 이미 승인되고 연결된 기기를 표시합니다.

처음에는 get_device_list 핸들러 구현에서 requestDevice()를 직접 사용하려고 했습니다. 그러나 연결된 기기 목록이 포함된 권한 메시지를 표시하는 것은 민감한 작업으로 간주되며 사용자 상호작용 (예: 페이지의 버튼 클릭)에 의해 트리거되어야 합니다. 그러지 않으면 항상 거부된 약속을 반환합니다. libusb 애플리케이션은 애플리케이션 시작 시 연결된 기기를 나열해야 하는 경우가 많으므로 requestDevice()를 사용할 수 없습니다.

대신 navigator.usb.requestDevice() 호출을 최종 개발자에게 맡기고 navigator.usb.getDevices()에서 이미 승인된 기기만 노출해야 했습니다.

// Store the global `navigator.usb` once upon initialisation.
thread_local
const val web_usb = val::global("navigator")["usb"];

int em_get_device_list(libusb_context *ctx, discovered_devs **devs) {
 
// C++ equivalent of `await navigator.usb.getDevices()`.
 
// Note: at this point we must already have some devices exposed -
 
// caller must have called `await navigator.usb.requestDevice(...)`
 
// in response to user interaction before going to LibUSB.
 
// Otherwise this list will be empty.
 
auto result = promise_result::await(web_usb.call<val>("getDevices"));
 
if (result.error) {
   
return result.error;
 
}
 
auto &web_usb_devices = result.value;
 
// Iterate over the exposed devices.
  uint8_t devices_num
= web_usb_devices["length"].as<uint8_t>();
 
for (uint8_t i = 0; i < devices_num; i++) {
   
auto web_usb_device = web_usb_devices[i];
   
// …
   
*devs = discovered_devs_append(*devs, dev);
 
}
 
return LIBUSB_SUCCESS;
}

대부분의 백엔드 코드는 위에 이미 표시된 것과 유사한 방식으로 valpromise_result를 사용합니다. 데이터 전송 처리 코드에는 몇 가지 더 흥미로운 해킹이 있지만 이러한 구현 세부정보는 이 도움말의 목적상 그다지 중요하지 않습니다. 관심이 있으시면 GitHub에서 코드와 댓글을 확인하세요.

이벤트 루프를 웹으로 포팅

libusb 포트에서 한 가지 더 논의하고 싶은 부분은 이벤트 처리입니다. 이전 도움말에서 설명한 것처럼 C와 같은 시스템 언어의 대부분의 API는 동기식이며 이벤트 처리도 예외가 아닙니다. 일반적으로 일련의 외부 I/O 소스에서 '폴링' (데이터를 읽으려고 시도하거나 일부 데이터를 사용할 수 있을 때까지 실행을 차단)하고, 이러한 소스 중 하나 이상이 응답하면 해당 핸들러에 이벤트로 전달하는 무한 루프를 통해 구현됩니다. 핸들러가 완료되면 컨트롤이 루프로 돌아가고 다른 폴링을 위해 일시중지됩니다.

웹에서 이 접근 방식을 사용하면 몇 가지 문제가 발생합니다.

첫째, WebUSB는 기본 기기의 원시 핸들을 노출하지 않으며 노출할 수도 없으므로 이를 직접 폴링하는 것은 불가능합니다. 둘째, libusb는 다른 이벤트와 원시 기기 핸들 없이 운영체제에서 전송을 처리하는 데 eventfdpipe API를 사용하지만 eventfd는 현재 Emscripten에서 지원되지 않으며 pipe는 지원되지만 현재 사양을 준수하지 않으며 이벤트를 기다릴 수 없습니다.

마지막으로 가장 큰 문제는 웹에 자체 이벤트 루프가 있다는 점입니다. 이 전역 이벤트 루프는 모든 외부 I/O 작업 (fetch(), 타이머, 이 경우 WebUSB 포함)에 사용되며, 상응하는 작업이 완료될 때마다 이벤트 또는 Promise 핸들러를 호출합니다. 중첩된 다른 무한 이벤트 루프를 실행하면 브라우저의 이벤트 루프가 진행되지 않도록 차단됩니다. 즉, UI가 응답하지 않을 뿐만 아니라 코드가 기다리는 동일한 I/O 이벤트에 대한 알림을 받지 못하게 됩니다. 이렇게 하면 일반적으로 교착 상태가 발생하며, 데모에서 libusb를 사용하려고 할 때도 이러한 문제가 발생했습니다. 페이지가 정지되었습니다.

다른 차단 I/O와 마찬가지로 이러한 이벤트 루프를 웹으로 포팅하려면 개발자가 기본 스레드를 차단하지 않고 이러한 루프를 실행하는 방법을 찾아야 합니다. 한 가지 방법은 애플리케이션을 리팩터링하여 별도의 스레드에서 I/O 이벤트를 처리하고 결과를 기본 스레드에 다시 전달하는 것입니다. 다른 방법은 Asyncify를 사용하여 루프를 일시중지하고 비차단 방식으로 이벤트를 기다리는 것입니다.

libusb 또는 gPhoto2를 크게 변경하고 싶지 않았고 이미 Promise 통합에 Asyncify를 사용했기 때문에 이 경로를 선택했습니다. poll()의 차단 변형을 시뮬레이션하기 위해 초기 개념 증명에서는 아래와 같이 루프를 사용했습니다.

#ifdef __EMSCRIPTEN__
 
// TODO: optimize this. Right now it will keep unwinding-rewinding the stack
 
// on each short sleep until an event comes or the timeout expires.
 
// We should probably create an actual separate thread that does signaling
 
// or come up with a custom event mechanism to report events from
 
// `usbi_signal_event` and process them here.
 
double until_time = emscripten_get_now() + timeout_ms;
 
do {
   
// Emscripten `poll` ignores timeout param, but pass 0 explicitly just
   
// in case.
    num_ready
= poll(fds, nfds, 0);
   
if (num_ready != 0) break;
   
// Yield to the browser event loop to handle events.
    emscripten_sleep
(0);
 
} while (emscripten_get_now() < until_time);
#else
  num_ready
= poll(fds, nfds, timeout_ms);
#endif

다음과 같은 기능을 수행합니다.

  1. poll()를 호출하여 백엔드에서 아직 이벤트를 보고했는지 확인합니다. 값이 있으면 루프가 중지됩니다. 그렇지 않으면 Emscripten의 poll() 구현이 0과 함께 즉시 반환됩니다.
  2. emscripten_sleep(0)을 호출합니다. 이 함수는 내부적으로 Asyncify 및 setTimeout()를 사용하며 여기서는 기본 브라우저 이벤트 루프로 제어를 다시 반환하는 데 사용됩니다. 이를 통해 브라우저는 WebUSB를 비롯한 모든 사용자 상호작용 및 I/O 이벤트를 처리할 수 있습니다.
  3. 지정된 제한 시간이 아직 만료되지 않았는지 확인하고 만료되지 않은 경우 루프를 계속 진행합니다.

주석에 언급된 대로 이 접근 방식은 아직 처리할 USB 이벤트가 없는 경우에도 (대부분의 경우) Asyncify로 전체 호출 스택을 계속 저장-복원하고 최신 브라우저에서 setTimeout() 자체의 최소 기간이 4ms이기 때문에 최적화되지 않았습니다. 하지만 개념 증명에서는 DSLR에서 13~14FPS 라이브 스트림을 생성하기에 충분했습니다.

나중에 브라우저 이벤트 시스템을 활용하여 개선하기로 했습니다. 이 구현을 더 개선할 수 있는 방법은 여러 가지가 있지만 지금은 맞춤 이벤트를 특정 libusb 데이터 구조와 연결하지 않고 전역 객체에서 직접 내보내기로 했습니다. EM_ASYNC_JS 매크로를 기반으로 다음과 같은 대기 및 알림 메커니즘을 통해 이를 실행했습니다.

EM_JS(void, em_libusb_notify, (void), {
  dispatchEvent
(new Event("em-libusb"));
});

EM_ASYNC_JS
(int, em_libusb_wait, (int timeout), {
  let onEvent
, timeoutId;

 
try {
   
return await new Promise(resolve => {
      onEvent
= () => resolve(0);
      addEventListener
('em-libusb', onEvent);

      timeoutId
= setTimeout(resolve, timeout, -1);
   
});
 
} finally {
    removeEventListener
('em-libusb', onEvent);
    clearTimeout
(timeoutId);
 
}
});

em_libusb_notify() 함수는 libusb가 데이터 전송 완료와 같은 이벤트를 보고하려고 할 때마다 사용됩니다.

void usbi_signal_event(usbi_event_t *event)
{
  uint64_t dummy
= 1;
  ssize_t r
;

  r
= write(EVENT_WRITE_FD(event), &dummy, sizeof(dummy));
 
if (r != sizeof(dummy))
    usbi_warn
(NULL, "event write failed");
#ifdef __EMSCRIPTEN__
  em_libusb_notify
();
#endif
}

한편 em_libusb_wait() 부분은 em-libusb 이벤트가 수신되거나 제한 시간이 만료되었을 때 Asyncify 절전 모드에서 '깨우기'하는 데 사용됩니다.

double until_time = emscripten_get_now() + timeout_ms;
for (;;) {
 
// Emscripten `poll` ignores timeout param, but pass 0 explicitly just
 
// in case.
  num_ready
= poll(fds, nfds, 0);
 
if (num_ready != 0) break;
 
int timeout = until_time - emscripten_get_now();
 
if (timeout <= 0) break;
 
int result = em_libusb_wait(timeout);
 
if (result != 0) break;
}

절전 모드와 절전 모드 해제가 크게 줄어들면서 이 메커니즘은 이전 emscripten_sleep() 기반 구현의 효율성 문제를 해결하고 DSLR 데모 처리량을 13~14FPS에서 일관된 30FPS 이상으로 늘렸습니다. 이는 원활한 라이브 피드에 충분합니다.

빌드 시스템 및 첫 번째 테스트

백엔드가 완료된 후 Makefile.amconfigure.ac에 추가해야 했습니다. 여기서 유일하게 흥미로운 부분은 Emscripten 관련 플래그 수정입니다.

emscripten)
  AC_SUBST
(EXEEXT, [.html])
 
# Note: LT_LDFLAGS is not enough here because we need link flags for executable.
  AM_LDFLAGS
="${AM_LDFLAGS} --bind -s ASYNCIFY -s ASSERTIONS -s ALLOW_MEMORY_GROWTH -s INVOKE_RUN=0 -s EXPORTED_RUNTIME_METHODS=['callMain']"
 
;;

첫째, Unix 플랫폼의 실행 파일에는 일반적으로 파일 확장자가 없습니다. 그러나 Emscripten은 요청하는 확장 프로그램에 따라 다른 출력을 생성합니다. 패키지 내의 모든 실행 파일(테스트 및 예시)이 JavaScript 및 WebAssembly 로드 및 인스턴스화를 처리하는 Emscripten의 기본 셸이 포함된 HTML이 되도록 AC_SUBST(EXEEXT, …)를 사용하여 실행 파일 확장자를 .html로 변경합니다.

둘째, Embind와 Asyncify를 사용하고 있으므로 이러한 기능 (--bind -s ASYNCIFY)을 사용 설정하고 링커 매개변수를 통해 동적 메모리 증가 (-s ALLOW_MEMORY_GROWTH)를 허용해야 합니다. 안타깝게도 라이브러리가 이러한 플래그를 링커에 보고할 방법이 없으므로 이 libusb 포트를 사용하는 모든 애플리케이션은 빌드 구성에도 동일한 링커 플래그를 추가해야 합니다.

마지막으로 앞서 언급한 바와 같이 WebUSB는 사용자 동작을 통해 기기 열거를 실행해야 합니다. libusb 예시와 테스트는 시작 시 기기를 열거할 수 있다고 가정하고 변경 없이 오류가 발생하면 실패합니다. 대신 자동 실행 (-s INVOKE_RUN=0)을 사용 중지하고 수동 callMain() 메서드 (-s EXPORTED_RUNTIME_METHODS=...)를 노출해야 했습니다.

이 작업이 모두 완료되면 생성된 파일을 정적 웹 서버로 제공하고, WebUSB를 초기화하고, DevTools의 도움을 받아 이러한 HTML 실행 파일을 수동으로 실행할 수 있습니다.

로컬에서 제공되는 `testlibusb` 페이지에서 DevTools가 열려 있는 Chrome 창을 보여주는 스크린샷 DevTools 콘솔에서 권한 메시지를 트리거한 `navigator.usb.requestDevice({ filters: [] })`를 평가하고 있으며 현재 사용자에게 페이지와 공유할 USB 기기를 선택하라는 메시지를 표시하고 있습니다. 현재 ILCE-6600 (소니 카메라)이 선택되어 있습니다.

DevTools가 여전히 열려 있는 상태에서 다음 단계의 스크린샷 기기가 선택된 후 Console은 상세 모드로 `testlibusb` 앱을 실행하는 새 표현식 `Module.callMain([&#39;-v&#39;])`을 평가했습니다. 출력에는 이전에 연결된 USB 카메라에 관한 다양한 세부정보(예: 제조업체 Sony, 제품 ILCE-6600, 일련번호, 구성)가 표시됩니다.

별로 보이지 않지만 라이브러리를 새 플랫폼으로 포팅할 때 유효한 출력을 처음으로 생성하는 단계에 도달하면 매우 기쁩니다.

포트 사용

위에서 언급한 대로 포트는 현재 애플리케이션의 연결 단계에서 사용 설정해야 하는 몇 가지 Emscripten 기능에 종속됩니다. 자체 애플리케이션에서 이 libusb 포트를 사용하려면 다음 단계를 따르세요.

  1. 최신 libusb를 빌드의 일부로 보관 파일로 다운로드하거나 프로젝트에 git 하위 모듈로 추가합니다.
  2. libusb 폴더에서 autoreconf -fiv를 실행합니다.
  3. emconfigure ./configure –host=wasm32 –prefix=/some/installation/path를 실행하여 교차 컴파일을 위한 프로젝트를 초기화하고 빌드된 아티팩트를 배치할 경로를 설정합니다.
  4. emmake make install을 실행합니다.
  5. 애플리케이션 또는 상위 수준 라이브러리를 이전에 선택한 경로에서 libusb를 검색하도록 지정합니다.
  6. 애플리케이션의 링크 인수에 --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH 플래그를 추가합니다.

현재 이 라이브러리에는 몇 가지 제한사항이 있습니다.

  • 이전 취소는 지원되지 않습니다. 이는 WebUSB의 제한사항이며 이는 libusb 자체에 크로스 플랫폼 전송 취소가 없기 때문입니다.
  • 동기 전송 지원이 없습니다. 기존 전송 모드의 구현을 예로 따라 추가하는 것은 어렵지 않지만, 다소 드문 모드이고 테스트할 기기가 없어서 지금은 지원되지 않도록 했습니다. 이러한 기기를 보유하고 있고 라이브러리에 기여하고 싶다면 PR을 보내주세요.
  • 앞에서 언급한 교차 플랫폼 제한사항 이러한 제한사항은 운영체제에서 부과하므로 사용자에게 드라이버 또는 권한을 재정의하도록 요청하는 것 외에는 여기에서 할 수 있는 일이 많지 않습니다. 하지만 HID 또는 직렬 기기를 포팅하는 경우 libusb 예시를 따라 다른 라이브러리를 다른 Fugu API로 포팅할 수 있습니다. 예를 들어 C 라이브러리 hidapiWebHID로 포팅하고 저수준 USB 액세스와 관련된 문제를 모두 피할 수 있습니다.

결론

이 게시물에서는 Emscripten, Asyncify, Fugu API를 사용하여 libusb와 같은 하위 수준 라이브러리도 몇 가지 통합 트릭으로 웹에 포팅할 수 있는 방법을 설명했습니다.

이러한 필수적이고 널리 사용되는 하위 수준 라이브러리를 포팅하면 상위 수준 라이브러리 또는 전체 애플리케이션을 웹으로 가져올 수 있으므로 특히 유용합니다. 이를 통해 이전에는 하나 또는 두 개의 플랫폼 사용자로 제한되었던 환경을 모든 종류의 기기와 운영체제에 제공할 수 있게 되었으며, 링크를 클릭하기만 하면 이러한 환경을 이용할 수 있습니다.

다음 게시물에서는 기기 정보를 검색할 뿐만 아니라 libusb의 전송 기능도 광범위하게 사용하는 웹 gPhoto2 데모를 빌드하는 단계를 살펴봅니다. 그동안 libusb 예시가 도움이 되었기를 바라며, 데모를 사용해 보거나, 라이브러리 자체를 사용해 보거나, 널리 사용되는 다른 라이브러리를 Fugu API 중 하나로 포팅해 보시기 바랍니다.