USB アプリケーションをウェブに移植する。パート 1: Libusb

外部デバイスとやり取りするコードを、WebAssembly と Fugu API を使用してウェブに移植する方法について説明します。

以前の投稿では、File System Access API、WebAssembly、Asyncify を使用して、ファイルシステム API を使用するアプリをウェブに移植する方法を紹介しました。今回は、Fugu API を WebAssembly と統合し、重要な機能を失うことなくアプリをウェブに移植するという同じトピックを続けます。

この記事では、C 言語で記述された一般的な USB ライブラリである libusb を WebAssembly(Emscripten 経由)、Asyncify、WebUSB に移植することで、USB デバイスと通信するアプリをウェブに移植する方法を説明します。

まずはデモ

ライブラリを移植する際に最も重要なことは、適切なデモを選択することです。移植されたライブラリの機能をさまざまな方法でテストでき、同時に視覚的に魅力的なデモを選択する必要があります。

私が選んだアイデアは、デジタル一眼レフカメラのリモコンでした。特に、オープンソース プロジェクトの gPhoto2 は、この分野で長い歴史があり、リバース エンジニアリングによって幅広いデジタルカメラのサポートを実装しています。複数のプロトコルをサポートしていますが、私が最も関心を持ったのは、libusb を介して実行される USB のサポートでした。

このデモを構築する手順を 2 つのパートに分けて説明します。このブログ投稿では、libusb 自体を移植した方法と、他の一般的なライブラリを Fugu API に移植するために必要な手法について説明します。2 つ目の投稿では、gPhoto2 自体の移植と統合について詳しく説明します。

最終的に、デジタル一眼レフカメラからのライブ フィードをプレビューし、USB 経由で設定を制御できる、動作するウェブ アプリケーションが完成しました。技術的な詳細を読む前に、ライブまたは事前録画のデモをご覧ください。

ソニーのカメラに接続されたノートパソコンで実行されているデモ。

カメラ固有の特性に関する注意事項

動画の中で、設定の変更に時間がかかっていることにお気づきかもしれません。他のほとんどの問題と同様に、これは WebAssembly や WebUSB のパフォーマンスが原因ではなく、gPhoto2 がデモ用に選択された特定のカメラとどのようにやり取りするかが原因です。

Sony a6600 には、ISO、絞り、シャッター速度などの値を直接設定する API がありません。代わりに、指定されたステップ数だけ値を増減するコマンドのみが用意されています。さらに複雑なことに、実際にサポートされている値のリストも返されません。返されるリストは、多くの Sony カメラモデルでハードコードされているようです。

これらの値のいずれかを設定する場合、gPhoto2 には次の選択肢しかありません。

  1. 選択した値の方向に 1 ステップ(または数ステップ)進みます。
  2. カメラの設定が更新されるまでしばらく待ちます。
  3. カメラが実際に設定した値を読み取ります。
  4. 最後のステップで、目的の値がスキップされていないこと、リストの末尾または先頭がラップされていないことを確認します。
  5. 繰り返し。

時間がかかることもありますが、値がカメラで実際にサポートされている場合は、その値に到達します。サポートされていない場合は、最も近いサポートされている値で停止します。

他のカメラでは、設定、基盤となる API、不具合が異なる可能性があります。gPhoto2 はオープンソース プロジェクトであり、すべてのカメラモデルの自動テストまたは手動テストは不可能です。詳細な問題レポートと PR はいつでも歓迎します(ただし、最初に公式の gPhoto2 クライアントで問題を再現してください)。

クロス プラットフォームの互換性に関する重要な注意事項

残念ながら、Windows では、デジタル一眼レフカメラを含む「既知の」デバイスにシステム ドライバが割り当てられます。このドライバは WebUSB と互換性がありません。Windows でデモを試す場合は、Zadig などのツールを使用して、接続された DSLR のドライバを WinUSB または libusb にオーバーライドする必要があります。この方法は私や他の多くのユーザーにとっては問題なく機能しますが、自己責任で使用してください。

Linux では、WebUSB 経由でデジタル一眼レフカメラにアクセスできるようにカスタム権限を設定する必要があります(ディストリビューションによって異なります)。

macOS と Android では、デモはすぐに動作します。Android スマートフォンで試す場合は、横表示に切り替えてください。レスポンシブ対応にはあまり力を入れていません(PR を歓迎します)。

USB-C ケーブルでキヤノンのカメラに接続された Android スマートフォン。
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++ でのみ動作するため、私は C++ を選択し、必要な構造とプライベート データ フィールド用の sizeof(val) を含む libusb/libusb/os/emscripten_webusb.cpp を追加しました。

#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 側で拒否をキャッチし、結果を { error, value } オブジェクトに変換することで回避できます。このオブジェクトは、C++ 側から安全に解析できるようになります。これは、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 オペレーションから返された Promise で promise_result::await() を使用し、error フィールドと value フィールドを個別に検査できるようになりました。

たとえば、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 デバイスを列挙する方法がないことです。代わりに、フローは 2 つの部分に分割されます。まず、ウェブ アプリケーションが navigator.usb.requestDevice() を介して特定のプロパティを持つデバイスをリクエストし、ユーザーが手動で公開するデバイスを選択するか、権限プロンプトを拒否します。その後、アプリは navigator.usb.getDevices() を介して、すでに承認され接続されているデバイスを一覧表示します。

最初は、get_device_list ハンドラの実装で requestDevice() を直接使用しようとしました。ただし、接続されているデバイスのリストを含む権限プロンプトを表示することは、機密性の高い操作と見なされ、ユーザー操作(ページ上のボタンクリックなど)によってトリガーされる必要があります。そうでない場合、常に拒否された Promise が返されます。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;
}

バックエンド コードのほとんどは、すでに上記で示したのと同様の方法で val と promise_result を使用しています。データ転送処理コードには、他にも興味深いハックがいくつかありますが、それらの実装の詳細は、この記事の目的にはあまり重要ではありません。興味のある方は、GitHub でコードとコメントを確認してください。

イベントループをウェブに移植する

libusb ポートについてもう 1 つ説明したいのは、イベント処理です。前の記事で説明したように、C などのシステム言語のほとんどの API は同期的であり、イベント処理も例外ではありません。通常は、一連の外部 I/O ソースから「ポーリング」(データの読み取りを試行するか、データが利用可能になるまで実行をブロック)する無限ループを介して実装されます。少なくとも 1 つのソースが応答すると、そのソースがイベントとして対応するハンドラに渡されます。ハンドラが終了すると、制御はループに戻り、次のポーリングまで一時停止します。

ウェブでこのアプローチを使用すると、いくつかの問題が発生します。

まず、WebUSB は基盤となるデバイスの未加工のハンドルを公開しないため、それらを直接ポーリングすることはできません。次に、libusb は、他のイベントや、未加工のデバイス ハンドルがないオペレーティング システムでの転送の処理に eventfd API と pipe API を使用しますが、eventfd は現在 Emscripten でサポートされていません。pipe はサポートされていますが、現在は仕様に準拠しておらず、イベントを待機できません。

最後に、最大の課題は、ウェブに独自のイベントループがあることです。このグローバル イベントループは、外部 I/O オペレーション(fetch()、タイマー、この場合は WebUSB など)に使用され、対応するオペレーションが完了するたびにイベント ハンドラまたは Promise ハンドラを呼び出します。別のネストされた無限イベントループを実行すると、ブラウザのイベントループの進行がブロックされます。つまり、UI が応答しなくなるだけでなく、コードが待機している I/O イベントの通知を受け取ることができなくなります。これは通常、デッドロックを引き起こします。デモで libusb を使用しようとしたときにも、この問題が発生しました。ページがフリーズした。

他のブロッキング I/O と同様に、このようなイベントループをウェブに移植するには、メインスレッドをブロックせずにループを実行する方法を見つける必要があります。1 つの方法は、アプリをリファクタリングして、I/O イベントを別のスレッドで処理し、結果をメインスレッドに返すことです。もう 1 つは、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() 自体の最小時間が 4 ミリ秒であるためです。それでも、概念実証ではデジタル一眼レフカメラから 13 ~ 14 FPS のライブ配信を生成するのに十分な性能を発揮しました。

その後、ブラウザのイベント システムを利用して改善することにしました。この実装をさらに改善する方法はいくつかありますが、今回は、特定の 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 ~ 14 FPS から一貫して 30 FPS 以上に増加しました。これは、スムーズなライブフィードに十分な値です。

ビルドシステムと最初のテスト

バックエンドが完了したら、Makefile.am と configure.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 に変更しています。これにより、パッケージ内の実行可能ファイル(テストとサンプル)は、Emscripten のデフォルト シェルを含む HTML になり、JavaScript と WebAssembly の読み込みとインスタンス化が処理されます。

次に、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(Sony のカメラ)が現在選択されています。

DevTools が開いたままの次のステップのスクリーンショット。デバイスを選択すると、コンソールで新しい式「Module.callMain(['-v'])」が評価され、testlibusb アプリが詳細モードで実行されました。出力には、以前に接続された 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 をお送りください。
  • 前述のクロスプラットフォームの制限事項。これらの制限はオペレーティング システムによって課されるため、ユーザーにドライバまたは権限をオーバーライドするよう依頼する以外に、Google でできることはあまりありません。ただし、HID デバイスやシリアル デバイスを移植する場合は、libusb の例に沿って、別のライブラリを別の Fugu API に移植できます。たとえば、C ライブラリ hidapi を WebHID に移植して、低レベルの USB アクセスに関連する問題をすべて回避できます。

まとめ

この投稿では、Emscripten、Asyncify、Fugu API を使用して、libusb のような低レベルのライブラリでも、いくつかの統合トリックを使用してウェブに移植する方法について説明しました。

このような、不可欠で広く使用されている低レベルのライブラリを移植することは、特に有益です。これにより、高レベルのライブラリやアプリケーション全体をウェブに移植できるようになるためです。これにより、以前は 1 つまたは 2 つのプラットフォームのユーザーに限定されていたエクスペリエンスが、あらゆる種類のデバイスやオペレーティング システムに開放され、リンクをクリックするだけで利用できるようになります。

次の投稿では、ウェブ gPhoto2 デモを構築する手順について説明します。このデモでは、デバイス情報を取得するだけでなく、libusb の転送機能も幅広く使用します。libusb の例が参考になれば幸いです。デモを試したり、ライブラリ自体を操作したり、広く使用されている別のライブラリを Fugu API のいずれかに移植したりしてみてください。