Melakukan porting aplikasi USB ke web. Bagian 1: libusb

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

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

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

Hal pertama yang paling penting: demo

Hal terpenting yang harus dilakukan saat mem-porting library adalah memilih demo yang tepat—sesuatu yang akan menunjukkan kemampuan library yang telah di-porting, sehingga Anda dapat mengujinya dalam berbagai cara, dan sekaligus menarik secara visual.

Ide yang saya pilih adalah {i>remote control<i} DSLR. Secara khusus, project open source gPhoto2 telah berada di ruang ini cukup lama untuk merekayasa balik dan menerapkan dukungan untuk berbagai kamera digital. Ada beberapa protokol yang didukung, tetapi yang paling saya minati adalah dukungan USB, yang dijalankan melalui libusb.

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

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

Demo yang berjalan di laptop yang terhubung ke kamera Sony.

Catatan tentang quirk khusus kamera

Anda mungkin memperhatikan bahwa perlu waktu agak lama untuk mengubah setelan dalam video. Seperti kebanyakan masalah lain yang mungkin Anda temui, 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 menyetel nilai seperti ISO, aperture, atau kecepatan shutter secara langsung, dan hanya memberikan perintah untuk menambah atau menguranginya dengan jumlah langkah yang ditentukan. Untuk membuat masalahnya lebih rumit, kode ini juga tidak menampilkan daftar nilai yang sebenarnya didukung—daftar yang ditampilkan tampak di-hardcode di banyak model kamera Sony.

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

  1. Buat satu langkah (atau beberapa langkah) ke arah nilai yang dipilih.
  2. Tunggu sebentar sampai kamera memperbarui setelan.
  3. Membaca kembali nilai yang sebenarnya diterima kamera.
  4. Pastikan langkah terakhir tidak melampaui nilai yang diinginkan atau mencapai akhir atau awal daftar.
  5. Ulangi.

Proses ini dapat memerlukan waktu beberapa saat, tetapi jika nilai benar-benar didukung oleh kamera, nilai tersebut akan muncul di sana, dan jika tidak, nilai tersebut akan berhenti pada nilai terdekat yang didukung.

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

Catatan penting tentang kompatibilitas lintas platform

Sayangnya, di Windows setiap perangkat yang "terkenal", termasuk kamera DSLR, ditetapkan 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 bekerja 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 seharusnya dapat langsung berfungsi. Jika Anda mencobanya di ponsel Android, pastikan untuk beralih ke mode lanskap karena saya tidak terlalu berupaya untuk membuatnya responsif (Silakan menerima PR!):

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

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

Menambahkan backend baru ke libusb

Sekarang, kita masuk ke bagian detail teknis. Meskipun memungkinkan untuk menyediakan API shim yang mirip dengan libusb (ini telah dilakukan oleh orang lain sebelumnya) dan menautkan aplikasi lain ke sana, pendekatan ini rentan terhadap error dan membuat ekstensi atau pemeliharaan lebih lanjut menjadi lebih sulit. Saya ingin melakukan semuanya dengan benar, dengan cara yang berpotensi dikontribusikan kembali ke hulu dan digabungkan ke libusb pada masa mendatang.

Untungnya, README libusb menyatakan:

“libusb diabstraksi secara internal sedemikian rupa sehingga diharapkan dapat ditransfer 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 membuat listingan, 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 helper dan jenis umum, serta perlu mengekspos variabel usbi_backend 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 melalui properti, kita dapat melihat bahwa struct mencakup nama backend, sekumpulan kemampuannya, pengendali untuk berbagai operasi USB tingkat rendah dalam bentuk pointer fungsi, dan, terakhir, ukuran yang akan dialokasikan untuk menyimpan data level perangkat/konteks//transfer pribadi.

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

Sebagian besar backend dalam folder diimplementasikan di C, tetapi beberapa diimplementasikan di C++. Embind hanya berfungsi dengan C++, jadi pilihan ini 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 tuas perangkat

libusb menyediakan pointer yang siap digunakan ke area yang dialokasikan untuk data pribadi. Agar dapat menggunakan pointer tersebut sebagai instance val, saya telah menambahkan helper kecil yang membentuknya 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 WebUSB API asinkron saat libusb mengharapkan operasi sinkron. Untuk itu, saya dapat menggunakan Asyncify, atau, lebih khusus lagi, 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 kini dapat diurai 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 serta memeriksa kolom error dan value secara terpisah.

Misalnya, mengambil val yang mewakili USBDevice dari libusb_device_handle, memanggil metode open(), menunggu hasilnya, dan menampilkan kode error sebagai kode status libusb akan 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 bisa membuka perangkat apa pun, libusb perlu mengambil daftar perangkat yang tersedia. Backend harus menerapkan operasi ini melalui pengendali get_device_list.

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

Pada awalnya, saya mencoba menggunakan requestDevice() secara langsung dalam implementasi pengendali get_device_list. Namun, menampilkan dialog izin dengan daftar perangkat yang terhubung dianggap sebagai operasi yang sensitif, dan harus dipicu oleh interaksi pengguna (seperti klik tombol pada halaman), jika tidak, permintaan tersebut selalu menampilkan promise yang ditolak. Aplikasi libusb mungkin sering kali ingin membuat daftar perangkat yang terhubung saat aplikasi dimulai, sehingga menggunakan requestDevice() bukanlah sebuah opsi.

Sebagai gantinya, saya harus menyerahkan panggilan navigator.usb.requestDevice() ke developer akhir, dan hanya mengekspos perangkat yang sudah 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 sudah ditunjukkan di atas. Ada beberapa peretasan 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 tertarik.

Melakukan porting loop peristiwa ke web

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

Ada beberapa masalah dengan pendekatan ini di web.

Pertama, WebUSB tidak dan tidak dapat mengekspos handle mentah dari perangkat dasar, sehingga melakukan polling tersebut secara langsung bukanlah pilihan. Kedua, libusb menggunakan API eventfd dan pipe untuk event lain serta untuk menangani transfer pada 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 event.

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 peristiwa atau pengendali Promise setiap kali operasi yang sesuai selesai. Mengeksekusi loop peristiwa bersarang dan tak terbatas yang lain akan memblokir loop peristiwa browser agar tidak terus berjalan, yang berarti bahwa UI tidak hanya akan menjadi tidak responsif, tetapi juga bahwa kode tidak akan pernah mendapatkan notifikasi untuk peristiwa I/O yang sama dengan yang ditunggu. Hal ini biasanya menyebabkan deadlock, dan itulah yang terjadi saat saya mencoba menggunakan libusb dalam demo juga. Halaman berhenti.

Seperti halnya I/O pemblokir 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. Kedua, dengan 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 sudah menggunakan Asyncify untuk integrasi Promise, jadi ini adalah jalur yang saya pilih. Untuk menyimulasikan varian pemblokiran poll(), sebagai bukti awal konsep, saya telah menggunakan loop seperti yang ditunjukkan di bawah:

#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 Emscripten terhadap poll() akan segera ditampilkan dengan 0.
  2. Memanggil emscripten_sleep(0). Fungsi ini menggunakan Asyncify dan setTimeout() di balik layar serta digunakan di sini untuk menghasilkan kontrol kembali ke loop peristiwa browser utama. Hal ini memungkinkan browser menangani interaksi pengguna dan peristiwa I/O, termasuk WebUSB.
  3. Memeriksa apakah waktu tunggu yang ditentukan telah habis masa berlakunya, dan jika belum, lanjutkan loop.

Seperti yang disebutkan dalam komentar, pendekatan ini tidak optimal karena terus menyimpan pemulihan seluruh stack panggilan dengan Asinkron meskipun belum ada peristiwa USB yang dapat ditangani (yang hampir selalu terjadi), dan karena setTimeout() sendiri memiliki durasi minimal 4 md di browser modern. Namun, terbukti berhasil untuk memproduksi livestream 13-14 FPS dari DSLR dalam bukti konsepnya.

Kemudian, saya memutuskan untuk memperbaikinya dengan memanfaatkan sistem peristiwa browser. Ada beberapa cara untuk meningkatkan penerapan ini, tetapi untuk saat ini saya telah memilih untuk memunculkan peristiwa kustom langsung pada objek global, tanpa mengaitkannya dengan struktur data libusb tertentu. Kami telah melakukannya melalui mekanisme tunggu dan beri tahu 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 mode tidur Asinkron 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 waktu tidur dan bangun yang signifikan, mekanisme ini memperbaiki masalah efisiensi dari implementasi berbasis emscripten_sleep() sebelumnya, dan meningkatkan throughput demo DSLR dari 13-14 FPS menjadi 30+ FPS secara 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 bagian 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, {i>executable<i} di platform Unix biasanya tidak memiliki ekstensi file. Namun, Emscripten menghasilkan output yang berbeda bergantung pada ekstensi mana 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 memungkinkan pertumbuhan memori dinamis (-s ALLOW_MEMORY_GROWTH) melalui parameter penaut. Sayangnya, tidak ada cara bagi library untuk melaporkan tanda tersebut ke penaut, sehingga setiap aplikasi yang menggunakan port libusb ini juga harus menambahkan tanda penaut yang sama ke dalam konfigurasi build.

Terakhir, seperti yang disebutkan sebelumnya, WebUSB mengharuskan enumerasi perangkat dilakukan melalui gestur pengguna. Contoh dan pengujian library 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 bisa menyajikan file yang dihasilkan dengan server web statis, melakukan inisialisasi WebUSB, dan menjalankan HTML yang bisa dieksekusi secara manual dengan bantuan DevTools.

Screenshot yang menunjukkan 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 harus dibagikan ke halaman. ILCE-6600 (kamera Sony) saat ini dipilih.

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

Memang tidak terlihat bagus, tetapi saat mem-porting library ke platform baru, mencapai tahap di mana ia menghasilkan output yang valid untuk pertama kalinya akan sangat menarik.

Menggunakan port

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

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

Saat ini library memiliki beberapa batasan:

  • Tidak ada dukungan pembatalan transfer. Hal ini adalah batasan WebUSB, yang pada akhirnya berasal dari kurangnya pembatalan transfer lintas platform di libusb itu sendiri.
  • Tidak ada dukungan transfer isochronous. Seharusnya 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 seperti itu, dan ingin berkontribusi untuk perpustakaan, kirimkan PR.
  • Artikel sebelumnya menyebutkan batasan lintas platform. Batasan tersebut diberlakukan oleh sistem operasi, jadi tidak banyak yang dapat kita lakukan di sini, kecuali meminta pengguna untuk mengganti driver atau izin. Namun, jika melakukan porting HID atau perangkat serial, Anda dapat mengikuti contoh libusb dan melakukan port beberapa library lain ke Fugu API lain. Misalnya, Anda dapat mem-port hidapi library C ke WebHID dan menghapus semua masalah tersebut, yang terkait dengan akses USB tingkat rendah.

Kesimpulan

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

Mentransfer library tingkat rendah yang penting dan banyak digunakan seperti ini sangat bermanfaat, karena, nantinya, hal ini juga memungkinkan menghadirkan library tingkat tinggi atau bahkan seluruh aplikasi ke web. Hal ini akan membuka pengalaman yang sebelumnya terbatas untuk pengguna satu atau dua platform, lalu ke semua jenis perangkat dan sistem operasi, sehingga pengalaman tersebut dapat diakses dengan sekali klik.

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