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 は、リバース エンジニアリングを行い、さまざまなデジタルカメラのサポートを実装するのに十分な期間にわたって、この分野で行われてきました。複数のプロトコルをサポートしていますが、私が最も興味を持っていたのは USB サポートで、libusb 経由で実行されます。

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

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

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

カメラ固有の注意事項

動画を見ながら、設定を変更するのに時間がかかることにお気づきでしょうか。発生する可能性のある他のほとんどの問題と同様に、これは 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 経由で DSLR にアクセスするには、カスタム権限を設定する必要があります(ディストリビューションによって異なります)。

macOS と Android では、デモはすぐに機能するはずです。Android スマートフォンで試す場合は、レスポンシブにするために労力を注いでいないため、必ず横表示に切り替えてください(PR も歓迎します)。

USB-C ケーブルでキヤノンカメラに接続された Android スマートフォン。
Android スマートフォンで実行している同じデモ。写真提供: Surma

WebUSB のクロスプラットフォームでの使用について詳しくは、「WebUSB 用デバイスの作成」の「プラットフォーム固有の考慮事項」をご覧ください。

libusb に新しいバックエンドを追加する

次は技術的な詳細ですlibusb に似た shim API を提供し(これは以前に他のアプリケーションによって行われています)、この 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++ でのみ機能するため、私は必要な構造とプライベート データ フィールド用の 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 側で拒否をキャッチし、その結果を 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 つの外部 I/O ソースが応答すると、対応するハンドラにイベントとして渡されます。ハンドラが終了すると、コントロールはループに戻り、次のポーリングのために一時停止します。

ウェブの場合、この方法にはいくつかの問題があります。

まず、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 ms であるため、このアプローチは最適ではありませんでした。それでも、概念実証では、デジタル一眼レフで 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 カメラに関するさまざまな詳細情報(メーカー 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 API、Asyncify API、Fugu API を利用して、libusb のような低レベルのライブラリでさえも、インテグレーションのコツでウェブに移植する方法を紹介しました。

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

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