Melakukan porting aplikasi USB ke web. Bagian 1: libusb

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

Dalam postingan sebelumnya, saya telah 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 mengintegrasikan Fugu API dengan WebAssembly dan mentransfer aplikasi ke web tanpa kehilangan fitur penting.

Saya akan menunjukkan bagaimana aplikasi yang berkomunikasi dengan perangkat USB dapat di-port ke web dengan melakukan 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 melakukan porting library adalah memilih demo yang tepat—sesuatu yang akan menunjukkan kemampuan library yang di-port, sehingga Anda dapat mengujinya dalam berbagai cara, dan menjadi menarik secara visual pada saat yang sama.

Ide yang saya pilih adalah remote control DSLR. Secara khusus, proyek open source gPhoto2 telah cukup lama berada di bidang ini untuk merekayasa balik dan mengimplementasikan dukungan untuk berbagai kamera digital. Mendukung beberapa protokol, tetapi yang paling saya minati adalah dukungan USB, yang dijalankan melalui libusb.

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

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

Demo yang berjalan di laptop yang terhubung ke kamera Sony.

Catatan tentang kebiasaan khusus kamera

Anda mungkin telah mengetahui bahwa mengubah setelan memerlukan waktu beberapa saat dalam video. Seperti kebanyakan masalah lain yang mungkin Anda lihat, hal 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, apertur, atau kecepatan shutter secara langsung, dan sebagai gantinya hanya memberikan perintah untuk menambah atau menguranginya berdasarkan jumlah langkah yang ditentukan. Untuk membuatnya lebih rumit, sistem 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 agar kamera memperbarui setelan.
  3. Baca kembali nilai yang benar-benar dicapai kamera.
  4. Pastikan langkah terakhir tidak melompati nilai yang diinginkan atau melampaui akhir atau awal daftar.
  5. Ulangi.

Proses ini mungkin memerlukan waktu beberapa saat, tetapi jika benar-benar didukung oleh kamera, nilai akan sampai ke sana, dan jika tidak, nilainya akan berhenti pada nilai terdekat yang didukung.

Kamera lain mungkin akan memiliki set setelan, API dasar, dan quirk yang berbeda. Perlu diingat bahwa gPhoto2 adalah proyek {i>open-source<i}, dan pengujian otomatis maupun manual dari semua model kamera di luar sana sama sekali tidak mungkin dilakukan, sehingga laporan masalah dan PR yang terperinci selalu diterima (tetapi pastikan untuk mereproduksi masalah dengan klien gPhoto2 resmi terlebih dahulu).

Catatan penting terkait kompatibilitas lintas platform

Sayangnya, di Windows, setiap alat perangkat, termasuk kamera DSLR, akan 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 cocok bagi saya dan banyak pengguna lainnya, tetapi Anda harus menanggung sendiri risikonya.

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

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

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

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

Menambahkan backend baru ke libusb

Sekarang beralih ke detail teknis. Meskipun memungkinkan untuk menyediakan API shim yang mirip dengan libusb (ini telah dilakukan oleh orang lain sebelumnya) dan menautkan aplikasi lain ke API tersebut, 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 menjadi libusb di masa mendatang.

Untungnya, libusb README menyebutkan:

“libusb diabstraksi secara internal sedemikian rupa sehingga dapat diharapkan dapat ditransfer ke sistem operasi lain. Silakan lihat file PORTING untuk informasi selengkapnya.”

libusb disusun sedemikian rupa sehingga API publik terpisah dari "backend". Backend tersebut bertanggung jawab untuk membuat listingan, membuka, menutup, dan benar-benar berkomunikasi dengan perangkat melalui API tingkat rendah sistem operasi. Ini adalah cara libusb memisahkan 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 menampilkan 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),
};

Saat mempelajari 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 dialokasikan untuk menyimpan data tingkat perangkat-/context-/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 setiap operasi. Dalam implementasi web, handle OS akan menjadi objek JavaScript WebUSB yang mendasarinya. Cara alami untuk mewakili dan menyimpannya di Emscripten adalah melalui class emscripten::val, yang disediakan sebagai bagian dari Embind (sistem binding Emscripten).

Sebagian besar backend di folder diimplementasikan di C, tetapi beberapa diimplementasikan dalam C++. Embind hanya berfungsi dengan C++, jadi pilihan ini dibuat untuk saya. Saya telah menambahkan libusb/libusb/os/emscripten_webusb.cpp dengan struktur yang diperlukan dan 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. Untuk menangani pointer tersebut sebagai instance val, saya telah menambahkan helper kecil yang membangunnya 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 memerlukan cara untuk menangani WebUSB API asinkron jika 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 saat ini Embind tidak memiliki cara untuk menangani pengecualian JavaScript atau penolakan Promise dari sistem C++. Masalah ini dapat diatasi dengan menangkap penolakan di sisi JavaScript dan mengonversi hasilnya menjadi objek { error, value } yang sekarang 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-nya 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 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.

Kesulitannya adalah, tidak seperti platform lain, tidak ada cara untuk menghitung semua perangkat USB yang terhubung di web karena 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 permintaan 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 permintaan izin dengan daftar perangkat terhubung dianggap sebagai operasi yang sensitif, dan harus dipicu oleh interaksi pengguna (seperti klik tombol di halaman), jika tidak, pertanyaan ini akan selalu menampilkan promise yang ditolak. Aplikasi libusb mungkin sering kali ingin mencantumkan perangkat yang terhubung saat aplikasi dimulai, sehingga penggunaan requestDevice() bukanlah pilihan.

Sebagai gantinya, saya harus menyerahkan panggilan navigator.usb.requestDevice() kepada developer akhir, dan hanya menampilkan 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 implementasi tersebut tidak begitu penting untuk tujuan artikel ini. Pastikan untuk memeriksa kode dan komentar di GitHub jika Anda tertarik.

Melakukan porting loop peristiwa ke web

Satu lagi porta {i>libusb<i} 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. Hal ini biasanya diimplementasikan melalui loop tak terbatas yang melakukan "polling" (mencoba membaca data atau memblokir eksekusi hingga beberapa data tersedia) dari sekumpulan sumber I/O eksternal, dan jika setidaknya salah satu sumber tersebut merespons, meneruskannya sebagai peristiwa ke pengendali yang sesuai. Setelah pengendali selesai, kontrol akan kembali ke loop, dan berhenti sejenak untuk polling lain.

Ada beberapa masalah dengan pendekatan ini di web.

Pertama, WebUSB tidak dan tidak dapat mengekspos tuas mentah perangkat yang mendasarinya, sehingga melakukan polling secara langsung bukanlah opsi. Kedua, libusb menggunakan API eventfd dan pipe untuk peristiwa lain serta untuk menangani transfer pada sistem operasi tanpa tuas 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 peristiwa 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. Mengeksekusi loop peristiwa lain, bertingkat, dan tak terbatas akan memblokir loop peristiwa browser agar tidak pernah progres, yang berarti UI tidak hanya akan menjadi tidak responsif, tetapi juga bahwa kode tidak akan pernah mendapatkan notifikasi untuk peristiwa I/O yang sama dengan yang ditunggunya. Hal ini biasanya mengakibatkan deadlock, dan itulah yang terjadi ketika saya juga mencoba menggunakan libusb dalam demo. Halaman berhenti berfungsi.

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 yang utama. Cara lainnya adalah menggunakan Asyncify untuk menjeda loop dan menunggu event 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 jalur yang saya pilih. Untuk menyimulasikan varian pemblokiran poll(), untuk bukti konsep awal, 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 beberapa, loop akan berhenti. Jika tidak, implementasi poll() oleh Emscripten akan langsung ditampilkan dengan 0.
  2. Memanggil emscripten_sleep(0). Fungsi ini menggunakan Asyncify dan setTimeout() di balik layar dan digunakan di sini untuk menghasilkan kontrol kembali ke loop peristiwa browser utama. Hal ini memungkinkan browser menangani interaksi pengguna dan peristiwa I/O apa pun, termasuk WebUSB.
  3. Periksa 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 menghemat pemulihan seluruh stack panggilan dengan Asyncify meskipun belum ada peristiwa USB yang perlu ditangani (yang paling sering terjadi), dan karena setTimeout() sendiri memiliki durasi minimal 4 md di browser modern. Namun, format ini sudah cukup baik untuk menghasilkan livestream 13-14 FPS dari DSLR dalam bukti konsepnya.

Kemudian, saya memutuskan untuk meningkatkannya dengan memanfaatkan sistem peristiwa browser. Ada beberapa cara untuk meningkatkan implementasi ini, tetapi untuk saat ini saya telah memilih untuk menampilkan peristiwa kustom langsung pada objek global, tanpa mengaitkannya dengan struktur data libusb tertentu. Saya 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 "membangunkan" dari tidur Asyncify saat peristiwa em-libusb diterima atau waktu tunggu 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 pada penerapan berbasis emscripten_sleep() sebelumnya, dan meningkatkan throughput demo DSLR dari 13-14 FPS menjadi 30+ FPS yang konsisten, yang cukup untuk kelancaran feed live.

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} pada 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 serta 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 linker, sehingga setiap aplikasi yang menggunakan port libusb ini juga harus menambahkan tanda penaut yang sama ke dalam konfigurasi build-nya.

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

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

Screenshot yang menunjukkan jendela Chrome dengan DevTools terbuka di halaman `testlibusb` yang disalurkan secara lokal. Konsol DevTools sedang mengevaluasi `navigator.usb.requestDevice({ filters: [] })`, yang memicu dialog izin dan saat ini meminta pengguna 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 mengeksekusi 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.

Ini tidak terlihat banyak, tetapi, saat melakukan porting library ke platform baru, mencapai tahap di mana ia 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 ingin menggunakan port libusb ini di aplikasi Anda sendiri, berikut yang harus 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 melakukan inisialisasi project untuk kompilasi silang dan menetapkan jalur tempat Anda ingin meletakkan artefak yang dibangun.
  4. Jalankan emmake make install.
  5. Arahkan aplikasi Anda atau library tingkat yang 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.

Library ini saat ini memiliki beberapa batasan:

  • Tidak ada dukungan pembatalan transfer. Hal ini merupakan keterbatasan WebUSB, yang berasal dari kurangnya pembatalan transfer lintas platform dalam libusb itu sendiri.
  • Tidak ada dukungan transfer isocron. Seharusnya tidak sulit untuk menambahkannya dengan mengikuti implementasi mode transfer yang ada sebagai contoh. Namun, mode ini juga agak jarang terjadi 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 ke perpustakaan, PR dipersilakan!
  • 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 melakukan porting HID atau perangkat serial, Anda dapat mengikuti contoh libusb dan mem-port beberapa library lain ke Fugu API lain. Misalnya, Anda dapat mem-port hidapi library C ke WebHID dan mengesampingkan masalah tersebut, yang terkait dengan akses USB tingkat rendah.

Kesimpulan

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

Melakukan porting library tingkat rendah yang sangat penting dan banyak digunakan ini sangat bermanfaat, karena, sebagai hasilnya, memungkinkan membawa library dengan tingkat yang lebih tinggi atau bahkan seluruh aplikasi ke web. Hal ini membuka pengalaman yang sebelumnya terbatas pada pengguna satu atau dua platform, ke semua jenis perangkat dan sistem operasi, sehingga pengalaman tersebut tersedia hanya dengan sekali klik.

Pada postingan berikutnya saya akan menjelaskan langkah-langkah yang diperlukan dalam membangun 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-main dengan library itu sendiri, atau bahkan mungkin melanjutkan dan mentransfer library lain yang banyak digunakan ke salah satu API Fugu.