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

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

前回の記事では、File System Access API、WebAssembly、Asyncify を使用して、ファイルシステム API を使用するアプリをウェブに移植する方法について説明しました。引き続き、Fugu API を WebAssembly と統合し、重要な機能を失うことなくアプリをウェブに移植する方法について説明します。

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

まずデモをご覧ください

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

私が選んだアイデアは、DSLR リモコンでした。特に、オープンソース プロジェクトの gPhoto2 は、長い間この分野に携わっており、さまざまなデジタルカメラのリバース エンジニアリングとサポートの実装を行っています。複数のプロトコルをサポートしていますが、私が最も興味を持ったのは、libusb を介して実行される USB サポートです。

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

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

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

カメラ固有の不具合に関する注意事項

動画の設定を変更する際に時間がかかることがあります。他のほとんどの問題と同様に、これは WebAssembly や WebUSB のパフォーマンスが原因ではなく、デモ用に選択された特定のカメラと gPhoto2 のやり取りが原因です。

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

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

  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 ケーブルで Canon カメラに接続された 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++ でのみ機能するため、選択は私に任せられ、必要な構造を持つ 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 側で拒否をキャッチし、結果を { 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 オペレーションから返された任意の 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() を直接使用しようとしました。ただし、接続されているデバイスのリストを含む権限プロンプトを表示することは機密性の高いオペレーションと見なされ、ユーザー操作(ページ上のボタンのクリックなど)によってトリガーする必要があります。そうしないと、常に拒否されたプロミスが返されます。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 ポートでもう 1 つ説明したいのは、イベント処理です。前回の記事で説明したように、C などのシステム言語のほとんどの API は同期型であり、イベント処理も例外ではありません。通常、一連の外部 I/O ソースから「ポーリング」(データを読み取ろうとする、またはデータが利用可能になるまで実行をブロックする)を行う無限ループで実装され、少なくとも 1 つのソースが応答すると、そのソースをイベントとして対応するハンドラに渡します。ハンドラが完了すると、制御はループに戻り、別のポーリングのために一時停止します。

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

まず、WebUSB は基盤となるデバイスの未加工ハンドルを公開しません。また、公開することもできません。そのため、それらを直接ポーリングすることはできません。2 つ目は、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 ミリ秒以上です。それでも、概念実証では DSLR から 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 に変換しています。

2 つ目は、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 が開いたままの次のステップのスクリーンショット。デバイスが選択されると、コンソールは新しい式「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、Asyncify、Fugu API の助けを借りて、libusb などの低レベル ライブラリでも、いくつかの統合方法でウェブに移植できることを示しました。

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

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