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

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

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

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

最初にすべきこと: デモ

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

私が選んだのは、デジタル一眼レフリモコンです。特に、オープンソース プロジェクト gPhoto2 は、リバース エンジニアリングを行い、さまざまなデジタルカメラのサポートを実装するのに十分な期間にわたってこの分野で活動しています。いくつかのプロトコルをサポートしていますが、私が最も興味を持っていたのは、libusb を介して実行される USB サポートでした。

このデモの作成手順を 2 部に分けて説明します。このブログ投稿では、libusb 自体を移植した方法と、よく利用されている他のライブラリを Fugu API に移植する際に必要なコツを紹介します。2 番目の投稿では、gPhoto2 自体を移植して統合する方法について詳しく説明します。

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

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph> デモ: ソニーのカメラに接続されたノートパソコンで実行されています。

カメラの特徴に関する注意事項

動画の中で、設定の変更に時間がかかることにお気づきかと思います。よく見られる他の問題と同様に、この問題は WebAssembly や WebUSB のパフォーマンスではなく、デモ用に選択した特定のカメラと gPhoto2 が連携する方法が原因です。

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

これらの値のいずれかを設定する場合、gPhoto2 は以下を行うしかありません。

  1. 選択した値の方向に 1 ~ 2 ステップ進みます。
  2. カメラの設定が更新されるまで少し待ちます。
  3. カメラが実際に着地した値を読み戻します。
  4. 最後のステップで目的の値を超えたり、リストの先頭や末尾で折り返されなかったりしていないことを確認します。
  5. この繰り返しです。

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

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

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

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

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

macOS と Android では、デモはすぐに利用できます。Android スマートフォンで試していらっしゃる場合は、必ず横表示に切り替えてください。ここでは、レスポンシブ デザインにこだわってはいません(PR を歓迎します)。

<ph type="x-smartling-placeholder">
</ph> USB-C ケーブルでキヤノンのカメラと接続された Android スマートフォン。 <ph type="x-smartling-placeholder">
</ph> 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() を使用し、その 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;
}

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

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

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

ウェブでのこのアプローチには問題がいくつかあります。

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

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

他のブロッキング I/O と同様に、このようなイベントループをウェブに移植するには、デベロッパーはメインスレッドをブロックせずにこれらのループを実行する方法を見つける必要があります。1 つの方法は、別のスレッドで 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() 自体の最小時間が 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.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=...)を公開する必要がありました。

これらすべてが完了したら、生成されたファイルを静的ウェブサーバーで配信し、WebUSB を初期化して、DevTools を使用してそれらの HTML 実行可能ファイルを手動で実行できます。

ローカルで提供される「testlibusb」ページで DevTools が開いている Chrome ウィンドウを示すスクリーンショットDevTools コンソールが `navigator.usb.requestDevice({Filters: [] })` を評価しています。これにより権限プロンプトがトリガーされ、ページと共有する USB デバイスを選択するようユーザーに求めます。現在、ILCE-6600(Sony カメラ)が選択されています。

DevTools を開いた状態での次のステップのスクリーンショット。デバイスが選択されると、コンソールは新しい式 `Module.callMain([&#39;-v&#39;])` を評価しました。これにより、`testlibusb` アプリが詳細モードで実行されます。出力には、以前に接続された USB カメラに関するさまざまな詳細情報(ソニーのメーカー、製品 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 のような低レベルのライブラリであっても、いくつかの統合のコツによってウェブに移植できる方法をご紹介しました。

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

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