Melakukan porting aplikasi USB ke web. Bagian 1: libusb

Pelajari cara kode yang berinteraksi dengan perangkat eksternal dapat di-porting ke web dengan WebAssembly dan Fugu API.

Dalam postingan sebelumnya, saya menunjukkan cara melakukan port aplikasi menggunakan API sistem file ke web dengan File System Access API, WebAssembly, dan Asyncify. Sekarang saya ingin melanjutkan topik yang sama tentang integrasi Fugu API dengan WebAssembly dan melakukan porting aplikasi ke web tanpa kehilangan fitur penting.

Saya akan menunjukkan cara aplikasi yang berkomunikasi dengan perangkat USB dapat di-porting ke web dengan melakukan porting libusb—library USB populer yang ditulis dalam C—ke WebAssembly (melalui Emscripten), Asyncify, dan WebUSB.

Pertama-tama: demo

Hal terpenting yang harus dilakukan saat melakukan porting library adalah memilih demo yang tepat—sesuatu yang akan menunjukkan kemampuan library yang di-port, memungkinkan Anda mengujinya dengan berbagai cara, dan menarik secara visual pada saat yang sama.

Ide yang saya pilih adalah remote control DSLR. Secara khusus, project open source gPhoto2 telah ada di ruang ini cukup lama untuk melakukan rekayasa balik dan menerapkan dukungan untuk berbagai kamera digital. Library ini mendukung beberapa protokol, tetapi yang paling saya minati adalah dukungan USB, yang dilakukan melalui libusb.

Saya akan menjelaskan langkah-langkah untuk mem-build demo ini dalam dua bagian. Dalam postingan blog ini, saya akan menjelaskan cara mem-port libusb itu sendiri, dan trik yang mungkin diperlukan untuk mem-port library populer lainnya ke Fugu API. Dalam postingan kedua, saya akan membahas detail tentang porting dan integrasi gPhoto2 itu sendiri.

Pada akhirnya, saya mendapatkan aplikasi web yang berfungsi dan menampilkan pratinjau feed langsung dari DSLR serta dapat mengontrol setelannya melalui USB. Jangan ragu untuk menonton demo live atau rekaman sebelum membaca detail teknis:

Demo yang berjalan di laptop yang terhubung ke kamera Sony.

Catatan tentang keanehan khusus kamera

Anda mungkin melihat bahwa perubahan setelan memerlukan waktu beberapa saat dalam video. Seperti sebagian besar masalah lain yang mungkin Anda lihat, masalah ini tidak disebabkan oleh performa WebAssembly atau WebUSB, tetapi oleh cara gPhoto2 berinteraksi dengan kamera tertentu yang dipilih untuk demo.

Sony a6600 tidak mengekspos API untuk menetapkan nilai seperti ISO, aperture, atau kecepatan shutter secara langsung, dan hanya memberikan perintah untuk meningkatkan atau menurunkannya dengan jumlah langkah yang ditentukan. Untuk memperumit masalah, metode ini juga tidak menampilkan daftar nilai yang sebenarnya didukung—daftar yang ditampilkan tampaknya di-hardcode di banyak model kamera Sony.

Saat menetapkan salah satu nilai tersebut, gPhoto2 tidak memiliki pilihan lain selain:

  1. Lakukan langkah (atau beberapa langkah) ke arah nilai yang dipilih.
  2. Tunggu beberapa saat hingga kamera memperbarui setelan.
  3. Baca kembali nilai yang sebenarnya dituju kamera.
  4. Pastikan langkah terakhir tidak melewati nilai yang diinginkan atau digabungkan di akhir atau awal daftar.
  5. Ulangi.

Mungkin perlu waktu beberapa saat, tetapi jika nilai tersebut benar-benar didukung oleh kamera, nilai tersebut akan sampai ke sana, dan jika tidak, nilai akan berhenti pada nilai terdekat yang didukung.

Kamera lain kemungkinan akan memiliki kumpulan setelan, API yang mendasari, dan keanehan yang berbeda. Perlu diingat bahwa gPhoto2 adalah project open source, dan pengujian otomatis atau manual untuk semua model kamera di luar sana tidak dapat dilakukan, sehingga laporan masalah dan PR yang mendetail selalu diterima (tetapi pastikan untuk mereproduksi masalah dengan klien gPhoto2 resmi terlebih dahulu).

Catatan penting tentang kompatibilitas lintas platform

Sayangnya, di Windows, perangkat "terkenal" apa pun, termasuk kamera DSLR, diberi driver sistem, yang tidak kompatibel dengan WebUSB. Jika ingin mencoba demo di Windows, Anda harus menggunakan alat seperti Zadig untuk mengganti driver DSLR yang terhubung ke WinUSB atau libusb. Pendekatan ini berfungsi dengan baik untuk saya dan banyak pengguna lainnya, tetapi Anda harus menggunakannya dengan risiko Anda sendiri.

Di Linux, Anda mungkin perlu menetapkan izin kustom untuk mengizinkan akses ke DSLR melalui WebUSB, meskipun hal ini bergantung pada distribusi Anda.

Di macOS dan Android, demo akan berfungsi secara otomatis. Jika Anda mencobanya di ponsel Android, pastikan untuk beralih ke mode lanskap karena saya tidak berusaha keras untuk membuatnya responsif (PRs welcome!):

Ponsel Android terhubung ke kamera Canon melalui kabel USB-C.
Demo yang sama berjalan di ponsel Android. Gambar oleh Surma.

Untuk panduan yang lebih mendalam tentang penggunaan WebUSB lintas platform, lihat bagian"Pertimbangan khusus platform" di "Mem-build perangkat untuk WebUSB".

Menambahkan backend baru ke libusb

Sekarang, mari kita lihat detail teknisnya. Meskipun Anda dapat menyediakan API shim yang mirip dengan libusb (hal ini telah dilakukan oleh orang lain sebelumnya) dan menautkan aplikasi lain ke API tersebut, pendekatan ini rentan terhadap error dan mempersulit ekstensi atau pemeliharaan lebih lanjut. Saya ingin melakukan hal yang benar, dengan cara yang berpotensi berkontribusi kembali ke upstream dan digabungkan ke libusb pada masa mendatang.

Untungnya, README libusb menyatakan:

“libusb di-abstrak secara internal sedemikian rupa sehingga diharapkan dapat di-porting ke sistem operasi lain. Lihat file PORTING untuk mengetahui informasi selengkapnya.”

libusb disusun sedemikian rupa sehingga API publik terpisah dari "backend". Backend tersebut bertanggung jawab untuk mencantumkan, membuka, menutup, dan benar-benar berkomunikasi dengan perangkat melalui API tingkat rendah sistem operasi. Inilah cara libusb mengabstraksi perbedaan antara Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku, dan Solaris serta berfungsi di semua platform ini.

Yang harus saya lakukan adalah menambahkan backend lain untuk "sistem operasi" Emscripten+WebUSB. Implementasi untuk backend tersebut berada di folder 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

Setiap backend menyertakan header libusbi.h dengan jenis dan helper umum, serta perlu mengekspos variabel usbi_backend dari jenis usbi_os_backend. Misalnya, seperti inilah tampilan 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),
};

Melihat properti, kita dapat melihat bahwa struct menyertakan nama backend, serangkaian kemampuannya, pengendali untuk berbagai operasi USB tingkat rendah dalam bentuk pointer fungsi, dan, terakhir, ukuran yang akan dialokasikan untuk menyimpan data tingkat perangkat/konteks/transfer pribadi.

Kolom data pribadi setidaknya berguna untuk menyimpan handle OS ke semua hal tersebut, karena tanpa handle, kita tidak tahu item mana yang berlaku untuk operasi tertentu. Dalam implementasi web, handle OS akan menjadi objek JavaScript WebUSB yang mendasarinya. Cara alami untuk merepresentasikan dan menyimpannya di Emscripten adalah melalui class emscripten::val, yang disediakan sebagai bagian dari Embind (sistem binding Emscripten).

Sebagian besar backend dalam folder diimplementasikan dalam C, tetapi beberapa diimplementasikan dalam C++. Embind hanya berfungsi dengan C++, jadi pilihan dibuat untuk saya dan saya telah menambahkan libusb/libusb/os/emscripten_webusb.cpp dengan struktur yang diperlukan dan dengan sizeof(val) untuk kolom data pribadi:

#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),
};

Menyimpan objek WebUSB sebagai nama sebutan perangkat

libusb menyediakan pointer siap pakai ke area yang dialokasikan untuk data pribadi. Untuk menggunakan pointer tersebut sebagai instance val, saya telah menambahkan helper kecil yang membuatnya di tempat, mengambilnya sebagai referensi, dan memindahkan nilai:

// 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))) {}
};

API web asinkron dalam konteks C sinkron

Sekarang diperlukan cara untuk menangani API WebUSB asinkron saat libusb mengharapkan operasi sinkron. Untuk ini, saya dapat menggunakan Asyncify, atau, lebih khusus, integrasi Embind-nya melalui val::await().

Saya juga ingin menangani error WebUSB dengan benar dan mengonversinya menjadi kode error libusb, tetapi Embind saat ini tidak memiliki cara untuk menangani pengecualian JavaScript atau penolakan Promise dari sisi C++. Masalah ini dapat diatasi dengan menangkap penolakan di sisi JavaScript dan mengonversi hasilnya menjadi objek { error, value } yang sekarang dapat diuraikan dengan aman dari sisi C++. Saya melakukannya dengan kombinasi makro EM_JS dan 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()};
 
}
};

Sekarang saya dapat menggunakan promise_result::await() pada Promise yang ditampilkan dari operasi WebUSB dan memeriksa kolom error dan value secara terpisah.

Misalnya, mengambil val yang mewakili USBDevice dari libusb_device_handle, memanggil metode open()-nya, menunggu hasilnya, dan menampilkan kode error sebagai kode status libusb terlihat seperti ini:

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;
}

Enumerasi perangkat

Tentu saja, sebelum saya dapat membuka perangkat apa pun, libusb perlu mengambil daftar perangkat yang tersedia. Backend harus menerapkan operasi ini melalui pengendali get_device_list.

Kesulitannya adalah, tidak seperti di platform lain, tidak ada cara untuk menghitung semua perangkat USB yang terhubung di web karena alasan keamanan. Sebagai gantinya, alur dibagi menjadi dua bagian. Pertama, aplikasi web meminta perangkat dengan properti tertentu melalui navigator.usb.requestDevice() dan pengguna memilih perangkat yang ingin diekspos secara manual atau menolak permintaan izin. Setelah itu, aplikasi mencantumkan perangkat yang telah disetujui dan terhubung melalui navigator.usb.getDevices().

Awalnya, saya mencoba menggunakan requestDevice() secara langsung dalam implementasi pengendali get_device_list. Namun, menampilkan perintah izin dengan daftar perangkat yang terhubung dianggap sebagai operasi sensitif, dan harus dipicu oleh interaksi pengguna (seperti klik tombol di halaman). Jika tidak, perintah tersebut akan selalu menampilkan promise yang ditolak. Aplikasi libusb mungkin sering ingin mencantumkan perangkat yang terhubung saat aplikasi dimulai, sehingga menggunakan requestDevice() bukanlah suatu opsi.

Sebagai gantinya, saya harus menyerahkan pemanggilan navigator.usb.requestDevice() kepada developer akhir, dan hanya mengekspos perangkat yang telah disetujui dari 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;
}

Sebagian besar kode backend menggunakan val dan promise_result dengan cara yang sama seperti yang telah ditunjukkan di atas. Ada beberapa hack yang lebih menarik dalam kode penanganan transfer data, tetapi detail penerapan tersebut kurang penting untuk tujuan artikel ini. Pastikan untuk memeriksa kode dan komentar di GitHub jika Anda berminat.

Mentransfer loop peristiwa ke web

Satu lagi bagian dari port libusb yang ingin saya bahas adalah penanganan peristiwa. Seperti yang dijelaskan dalam artikel sebelumnya, sebagian besar API dalam bahasa sistem seperti C bersifat sinkron, dan penanganan peristiwa tidak terkecuali. Ini biasanya diterapkan melalui loop tanpa batas yang "mengambil sampel" (mencoba membaca data atau memblokir eksekusi hingga beberapa data tersedia) dari sekumpulan sumber I/O eksternal, dan, jika setidaknya salah satu dari sumber tersebut merespons, meneruskannya sebagai peristiwa ke pengendali yang sesuai. Setelah pengendali selesai, kontrol akan kembali ke loop, dan dijeda untuk polling lain.

Ada beberapa masalah dengan pendekatan ini di web.

Pertama, WebUSB tidak dan tidak dapat mengekspos handle mentah perangkat yang mendasarinya, sehingga polling langsung tidak dapat dilakukan. Kedua, libusb menggunakan API eventfd dan pipe untuk peristiwa lain serta untuk menangani transfer di sistem operasi tanpa handle perangkat mentah, tetapi eventfd saat ini tidak didukung di Emscripten, dan pipe, meskipun didukung, saat ini tidak sesuai dengan spesifikasi dan tidak dapat menunggu peristiwa.

Terakhir, masalah terbesarnya adalah web memiliki loop peristiwanya sendiri. Loop peristiwa global ini digunakan untuk operasi I/O eksternal (termasuk fetch(), timer, atau, dalam hal ini, WebUSB), dan memanggil pengendali peristiwa atau Promise setiap kali operasi yang sesuai selesai. Menjalankan loop peristiwa lain yang bertingkat dan tak terbatas akan memblokir loop peristiwa browser agar tidak pernah berkembang, yang berarti bahwa UI tidak hanya menjadi tidak responsif, tetapi juga kode tidak akan pernah mendapatkan notifikasi untuk peristiwa I/O yang sama yang ditunggu. Hal ini biasanya menyebabkan deadlock, dan hal itu juga terjadi saat saya mencoba menggunakan libusb dalam demo. Halaman berhenti berfungsi.

Seperti I/O pemblokiran lainnya, untuk mem-port loop peristiwa tersebut ke web, developer perlu menemukan cara untuk menjalankan loop tersebut tanpa memblokir thread utama. Salah satu caranya adalah dengan memfaktorkan ulang aplikasi untuk menangani peristiwa I/O di thread terpisah dan meneruskan hasilnya kembali ke thread utama. Cara lainnya adalah menggunakan Asyncify untuk menjeda loop dan menunggu peristiwa dengan cara yang tidak memblokir.

Saya tidak ingin melakukan perubahan signifikan pada libusb atau gPhoto2, dan saya telah menggunakan Asyncify untuk integrasi Promise, jadi itulah jalur yang saya pilih. Untuk menyimulasikan varian pemblokiran poll(), untuk bukti konsep awal, saya telah menggunakan loop seperti yang ditunjukkan di bawah ini:

#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

Fungsinya adalah:

  1. Memanggil poll() untuk memeriksa apakah ada peristiwa yang dilaporkan oleh backend. Jika ada, loop akan berhenti. Jika tidak, implementasi poll() Emscripten akan segera ditampilkan dengan 0.
  2. Memanggil emscripten_sleep(0). Fungsi ini menggunakan Asyncify dan setTimeout() di balik layar dan digunakan di sini untuk memberikan kontrol kembali ke loop peristiwa browser utama. Hal ini memungkinkan browser menangani interaksi pengguna dan peristiwa I/O, termasuk WebUSB.
  3. Periksa apakah waktu tunggu yang ditentukan sudah habis masa berlakunya, dan jika belum, lanjutkan loop.

Seperti yang disebutkan dalam komentar, pendekatan ini tidak optimal, karena terus menyimpan-memulihkan seluruh stack panggilan dengan Asyncify meskipun belum ada peristiwa USB yang harus ditangani (yang sering terjadi), dan karena setTimeout() itu sendiri memiliki durasi minimal 4 md di browser modern. Namun, alat ini cukup berfungsi untuk menghasilkan live stream 13-14 FPS dari DSLR dalam bukti konsep.

Kemudian, saya memutuskan untuk meningkatkannya dengan memanfaatkan sistem peristiwa browser. Ada beberapa cara untuk meningkatkan penerapan ini lebih lanjut, tetapi untuk saat ini saya telah memilih untuk memunculkan peristiwa kustom langsung pada objek global, tanpa mengaitkannya dengan struktur data libusb tertentu. Saya telah melakukannya melalui mekanisme tunggu dan pemberitahuan berikut berdasarkan makro 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);
 
}
});

Fungsi em_libusb_notify() digunakan setiap kali libusb mencoba melaporkan peristiwa, seperti penyelesaian transfer data:

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
}

Sementara itu, bagian em_libusb_wait() digunakan untuk "mengaktifkan" dari tidur Asyncify saat peristiwa em-libusb diterima, atau waktu tunggu telah habis:

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;
}

Karena pengurangan yang signifikan dalam mode tidur dan bangun, mekanisme ini memperbaiki masalah efisiensi dari implementasi berbasis emscripten_sleep() sebelumnya, dan meningkatkan throughput demo DSLR dari 13-14 FPS menjadi 30+ FPS yang konsisten, yang cukup untuk feed live yang lancar.

Sistem build dan pengujian pertama

Setelah backend selesai, saya harus menambahkannya ke Makefile.am dan configure.ac. Satu-satunya hal yang menarik di sini adalah modifikasi flag khusus 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']"
 
;;

Pertama, file yang dapat dieksekusi di platform Unix biasanya tidak memiliki ekstensi file. Namun, Emscripten menghasilkan output yang berbeda bergantung pada ekstensi yang Anda minta. Saya menggunakan AC_SUBST(EXEEXT, …) untuk mengubah ekstensi yang dapat dieksekusi menjadi .html sehingga setiap file yang dapat dieksekusi dalam paket—pengujian dan contoh—menjadi HTML dengan shell default Emscripten yang menangani pemuatan dan pembuatan instance JavaScript dan WebAssembly.

Kedua, karena saya menggunakan Embind dan Asyncify, saya perlu mengaktifkan fitur tersebut (--bind -s ASYNCIFY) serta mengizinkan pertumbuhan memori dinamis (-s ALLOW_MEMORY_GROWTH) melalui parameter penaut. Sayangnya, tidak ada cara bagi library untuk melaporkan flag tersebut ke penaut, sehingga setiap aplikasi yang menggunakan port libusb ini juga harus menambahkan flag penaut yang sama ke konfigurasi build-nya.

Terakhir, seperti yang disebutkan sebelumnya, WebUSB mengharuskan enumerasi perangkat dilakukan melalui gestur pengguna. Contoh dan pengujian libusb mengasumsikan bahwa keduanya dapat menghitung perangkat saat memulai, dan gagal dengan error tanpa perubahan. Sebagai gantinya, saya harus menonaktifkan eksekusi otomatis (-s INVOKE_RUN=0) dan mengekspos metode callMain() manual (-s EXPORTED_RUNTIME_METHODS=...).

Setelah semua ini selesai, saya dapat menayangkan file yang dihasilkan dengan server web statis, menginisialisasi WebUSB, dan menjalankan file yang dapat dieksekusi HTML tersebut secara manual dengan bantuan DevTools.

Screenshot yang menampilkan jendela Chrome dengan DevTools terbuka di halaman `testlibusb` yang ditayangkan secara lokal. Konsol DevTools mengevaluasi `navigator.usb.requestDevice({ filters: [] })`, yang memicu permintaan izin dan saat ini meminta pengguna untuk memilih perangkat USB yang akan dibagikan ke halaman. ILCE-6600 (kamera Sony) saat ini dipilih.

Screenshot langkah berikutnya, dengan DevTools masih terbuka. Setelah perangkat dipilih, Konsol telah mengevaluasi ekspresi baru `Module.callMain([&#39;-v&#39;])`, yang menjalankan aplikasi `testlibusb` dalam mode panjang. Output menampilkan berbagai informasi mendetail tentang kamera USB yang terhubung sebelumnya: produsen Sony, produk ILCE-6600, nomor seri, konfigurasi, dll.

Tampaknya tidak banyak, tetapi, saat mem-port library ke platform baru, mencapai tahap yang menghasilkan output yang valid untuk pertama kalinya cukup menarik.

Menggunakan port

Seperti yang disebutkan di atas, port bergantung pada beberapa fitur Emscripten yang saat ini perlu diaktifkan pada tahap penautan aplikasi. Jika Anda ingin menggunakan port libusb ini di aplikasi Anda sendiri, berikut adalah hal yang perlu Anda lakukan:

  1. Download libusb terbaru sebagai arsip sebagai bagian dari build Anda atau tambahkan sebagai submodul git dalam project Anda.
  2. Jalankan autoreconf -fiv di folder libusb.
  3. Jalankan emconfigure ./configure –host=wasm32 –prefix=/some/installation/path untuk melakukan inisialisasi project untuk kompilasi silang dan menetapkan jalur tempat Anda ingin menempatkan artefak yang di-build.
  4. Jalankan emmake make install.
  5. Arahkan aplikasi atau library tingkat tinggi Anda untuk menelusuri libusb di jalur yang dipilih sebelumnya.
  6. Tambahkan flag berikut ke argumen link aplikasi Anda: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

Library ini saat ini memiliki beberapa batasan:

  • Tidak ada dukungan pembatalan transfer. Ini adalah batasan WebUSB, yang pada akhirnya berasal dari kurangnya pembatalan transfer lintas platform di libusb itu sendiri.
  • Tidak ada dukungan transfer isochronous. Tidak sulit untuk menambahkannya dengan mengikuti penerapan mode transfer yang ada sebagai contoh, tetapi mode ini juga agak jarang dan saya tidak memiliki perangkat untuk mengujinya, jadi untuk saat ini saya membiarkannya sebagai tidak didukung. Jika Anda memiliki perangkat tersebut, dan ingin berkontribusi pada library, PR sangatlah disambut.
  • Batasan lintas platform yang disebutkan sebelumnya. Batasan tersebut diberlakukan oleh sistem operasi, sehingga tidak banyak yang dapat kita lakukan di sini, kecuali meminta pengguna untuk mengganti driver atau izin. Namun, jika Anda mem-porting perangkat HID atau serial, Anda dapat mengikuti contoh libusb dan mem-porting beberapa library lain ke API Fugu lain. Misalnya, Anda dapat melakukan port library C hidapi ke WebHID dan mengabaikan masalah tersebut, yang terkait dengan akses USB tingkat rendah, secara keseluruhan.

Kesimpulan

Dalam postingan ini, saya telah menunjukkan cara, dengan bantuan Emscripten, Asyncify, dan Fugu API, bahkan library tingkat rendah seperti libusb dapat di-porting ke web dengan beberapa trik integrasi.

Mentransfer library tingkat rendah yang penting dan banyak digunakan tersebut sangat bermanfaat, karena pada akhirnya, hal ini memungkinkan library tingkat tinggi atau bahkan seluruh aplikasi ditransfer ke web. Hal ini membuka pengalaman yang sebelumnya terbatas untuk pengguna satu atau dua platform, ke semua jenis perangkat dan sistem operasi, sehingga pengalaman tersebut tersedia hanya dengan mengklik link.

Di postingan berikutnya, saya akan menjelaskan langkah-langkah yang terlibat dalam mem-build demo gPhoto2 web yang tidak hanya mengambil informasi perangkat, tetapi juga menggunakan fitur transfer libusb secara ekstensif. Sementara itu, semoga Anda menemukan contoh libusb yang menginspirasi dan akan mencoba demo, bermain dengan library itu sendiri, atau bahkan melanjutkan dan mem-port library lain yang banyak digunakan ke salah satu Fugu API juga.