將 USB 應用程式轉移到網路。第 1 部分:libusb

瞭解如何使用 WebAssembly 和 Fugu API,將與外部裝置互動的程式碼移植至網路。

Ingvar Stepanyan
Ingvar Stepanyan

上一篇文章中,我說明瞭如何使用 File System Access API、WebAssembly 和 Asyncify,將使用檔案系統 API 的應用程式移植到網路。接下來,我想繼續討論同一個主題,也就是如何將 Fugu API 與 WebAssembly 整合,並將應用程式移植到網頁,同時保留重要功能。

我將說明如何將 libusb (一種以 C 編寫的熱門 USB 程式庫) 移植至 WebAssembly (透過 Emscripten)、Asyncify 和 WebUSB,進而將與 USB 裝置通訊的應用程式移植至網路。

首先,先看示範

在移植程式庫時,最重要的是選擇合適的示範內容,以便展示移植程式庫的功能,讓您以各種方式測試,同時提供引人入勝的視覺效果。

我選擇的想法是使用 DSLR 遙控器。特別是,開放原始碼專案 gPhoto2 已在這個領域經營相當長的時間,因此可以為各種數位相機進行逆向工程,並實作支援功能。它支援多種通訊協定,但我最感興趣的是 USB 支援,這是透過 libusb 執行的。

我將分兩部分說明建構此示範內容的步驟。在本篇文章中,我將說明如何移植 libusb 本身,以及將其他熱門程式庫移植至 Fugu API 時可能需要哪些技巧。在第二篇文章中,我會詳細說明如何移植及整合 gPhoto2。

最後,我得到了可正常運作的網頁應用程式,可預覽數位單眼相機的即時影像串流,並透過 USB 控制相機設定。歡迎先觀看直播或預錄的示範影片,再閱讀技術細節:

在連接 Sony 相機的筆記型電腦上執行的示範

攝影機專屬異常情形的注意事項

你可能會發現,在影片中變更設定需要一段時間。與您可能遇到的大多數其他問題一樣,這並非由 WebAssembly 或 WebUSB 的效能造成,而是由 gPhoto2 與用於示範的特定相機互動方式造成。

Sony a6600 不會公開 API,無法直接設定 ISO、光圈或快門速度等值,而是只提供指令,以指定的步數增加或減少這些值。更複雜的是,它也不會傳回實際支援的值清單,傳回的清單似乎在許多 Sony 相機型號中硬式編碼。

設定其中一個值時,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 傳輸線將 Android 手機連接至 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 中表示及儲存這些值的自然方式,是透過 emscripten::val 類別,該類別是 Embind (Emscripten 的繫結系統) 的一部分。

資料夾中的大部分後端都是以 C 實作,但少數則是以 C++ 實作。Embind 只支援 C++,因此我選擇了這個資料夾,並為私人資料欄位新增 libusb/libusb/os/emscripten_webusb.cppsizeof(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

目前需要一種方法來處理非同步 WebUSB API,因為 libusb 會預期同步作業。為此,我可以使用 Asyncify,更具體來說,是透過 val::await() 整合 Embind。

我也想正確處理 WebUSB 錯誤,並將這些錯誤轉換為 libusb 錯誤代碼,但 Embind 目前沒有任何方法可處理 JavaScript 例外狀況或 C++ 端的 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(),並分別檢查其 errorvalue 欄位。

舉例來說,從 libusb_device_handle 擷取代表 USBDeviceval、呼叫其 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 處理其他事件,以及在沒有原始裝置句柄的作業系統上處理轉移作業,但 Emscripten 目前不支援 eventfd,而 pipe 雖然支援,但目前不符合規格,且無法等待事件。

最後,最大的問題是網頁有自己的事件迴圈。這個全域事件迴圈可用於任何外部 I/O 作業 (包括 fetch()、計時器或本例中的 WebUSB),且每當對應作業完成時,就會叫用事件或 Promise 處理常式。執行另一個巢狀的無限事件迴圈,會阻止瀏覽器的事件迴圈繼續執行,這意味著 UI 不僅會停止回應,程式碼也永遠不會收到等待的 I/O 事件通知。這通常會導致死結,而我嘗試在示範中使用 libusb 時也發生了這種情況。頁面停止運作。

如同其他阻斷式 I/O,如果要將這類事件迴圈移植到網頁,開發人員必須找到方法,在不阻斷主執行緒的情況下執行這些迴圈。其中一種方法是重構應用程式,以便在個別執行緒中處理 I/O 事件,並將結果傳回至主執行緒。另一種方法是使用 Asyncify 暫停迴圈,並以非阻斷方式等待事件。

我不想對 libusb 或 gPhoto2 進行重大變更,而且我已經使用 Asyncify 進行 Promise 整合,所以這是我選擇的路徑。為了模擬 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(),並在此處用於將控制權交還給主要瀏覽器事件迴圈。這樣一來,瀏覽器就能處理任何使用者互動和 I/O 事件,包括 WebUSB。
  3. 檢查指定的逾時期限是否已到期,如果未到期,則繼續執行迴圈。

如註解所述,這個方法並非最佳做法,因為即使沒有要處理的 USB 事件 (這在大多數情況下都會發生),它仍會持續使用 Asyncify 儲存/還原整個呼叫堆疊,且 setTimeout() 本身在新式瀏覽器中的最短時間為 4 毫秒。不過,在概念驗證階段,這項技術已能順利以每秒 13 到 14 張的影格數,從單眼數位相機進行直播。

後來,我決定利用瀏覽器事件系統來改善這個問題。這項實作方式可以進一步改善,但目前我選擇直接在全域物件上發出自訂事件,而不與特定 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);
 
}
});

每當 libusb 嘗試回報事件 (例如資料傳輸完成) 時,就會使用 em_libusb_notify() 函式:

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 事件或逾時期限已過時,em_libusb_wait() 部分會用於從 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,這樣套件中的任何可執行檔 (包括測試和範例) 都會變成 HTML,並使用 Emscripten 的預設殼層,負責載入及例項化 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` 頁面上,開啟開發人員工具的 Chrome 視窗。DevTools 主控台正在評估 `navigator.usb.requestDevice({ filters: [] })`,這會觸發權限提示,目前要求使用者選擇要與網頁共用的 USB 裝置。目前選取的是 ILCE-6600 (Sony 相機)。

下一步的螢幕截圖,開發人員工具仍處於開啟狀態。選取裝置後,管理中心會評估新的運算式 `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 程式庫 hidapi 移植至 WebHID,並完全避開與低階 USB 存取相關的問題。

結論

在這篇文章中,我將說明如何透過 Emscripten、Asyncify 和 Fugu API 的協助,利用幾個整合技巧將 libusb 等低階程式庫移植到網路。

將這類廣泛使用的低階程式庫移植到網路上,可帶來特別豐碩的成果,因為這麼做可以讓更高階的程式庫,甚至整個應用程式,也能移植到網路上。這項功能可讓先前僅限於一或兩個平台的使用者體驗,開放給所有裝置和作業系統,讓使用者只要點按連結就能體驗。

下一篇文章中,我將逐步說明建構網頁版 gPhoto2 示範的步驟,這不僅可擷取裝置資訊,還可廣泛使用 libusb 的傳輸功能。同時,我希望您能從 libusb 範例中獲得靈感,並試用該範例、操作程式庫本身,甚至將其他廣泛使用的程式庫移植到 Fugu API 中。