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

WebAssembly 및 Fugu API를 사용하여 외부 기기와 상호작용하는 코드를 웹에 포팅하는 방법을 알아보세요.

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

C로 작성된 널리 사용되는 USB 라이브러리인 libusb를 WebAssembly (Emscripten을 통해), Asyncify 및 WebUSB로 포팅하여 USB 기기와 통신하는 앱을 웹으로 포팅하는 방법을 보여드리겠습니다.

가장 중요한 첫 번째 작업: 데모

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

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

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

결국 DSLR의 실시간 피드를 미리 보고 USB를 통해 설정을 제어할 수 있는 웹 애플리케이션을 사용할 수 있게 되었습니다. 기술적인 세부정보를 읽기 전에 언제든지 라이브 또는 사전 녹화된 데모를 확인해 보세요.

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

카메라 관련 문제 참고 사항

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

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

이러한 값 중 하나를 설정할 때 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 환영).

Android 휴대전화가 USB-C 케이블을 통해 Canon 카메라에 연결되어 있습니다.
Android 휴대전화에서 실행되는 동일한 데모 사진: Surma

WebUSB의 크로스 플랫폼 사용에 관한 자세한 내용은 'WebUSB용 기기 빌드'의 '플랫폼별 고려사항' 섹션을 참고하세요.

libusb에 새 백엔드 추가

이제 기술적인 세부사항으로 넘어갑니다. libusb와 유사한 shim API (이전의 다른 개발자도 사용함)를 제공하고 이에 대해 다른 애플리케이션을 연결할 수 있지만, 이 접근 방식은 오류가 발생하기 쉬우며 추가 확장이나 유지 관리를 어렵게 합니다. 저는 업스트림에 다시 기여하고 향후 libusb에 병합할 수 있는 방식으로 일을 올바르게 하고 싶었습니다.

다행히 libusb README의 내용은 다음과 같습니다.

“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;
}

절전 모드와 wakeup의 현저한 감소로 인해 이 메커니즘은 이전 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은 요청하는 확장 프로그램에 따라 다른 출력을 생성합니다. AC_SUBST(EXEEXT, …)를 사용하여 실행 파일 확장자를 .html로 변경합니다. 테스트 및 예시와 같이 패키지 내의 모든 실행 파일이 JavaScript 및 WebAssembly를 로드하고 인스턴스화하는 Emscripten의 기본 셸을 사용하는 HTML이 됩니다.

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

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

이 모든 작업이 완료되면 DevTools를 사용하여 생성된 파일을 정적 웹 서버로 제공하고 WebUSB를 초기화한 다음 해당 HTML 실행 파일을 수동으로 실행할 수 있습니다.

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

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 중 하나에 포팅해 보시기 바랍니다.