Pelajari cara kode yang berinteraksi dengan perangkat eksternal dapat di-porting ke web dengan WebAssembly dan Fugu API.
Dalam postingan sebelumnya, saya menunjukkan cara mem-porting aplikasi yang menggunakan API sistem file ke web dengan File System Access API, WebAssembly, dan Asyncify. Sekarang saya ingin melanjutkan topik yang sama, yaitu mengintegrasikan Fugu API dengan WebAssembly dan mem-porting aplikasi ke web tanpa kehilangan fitur penting.
Saya akan menunjukkan cara mem-porting aplikasi yang berkomunikasi dengan perangkat USB ke web dengan mem-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 mem-porting library adalah memilih demo yang tepat—sesuatu yang akan menampilkan kemampuan library yang di-porting, sehingga Anda dapat mengujinya dengan berbagai cara, dan sekaligus menarik secara visual.
Ide yang saya pilih adalah remote control DSLR. Secara khusus, project open source gPhoto2 telah lama berada di bidang ini untuk merekayasa balik dan menerapkan dukungan untuk berbagai kamera digital. Alat ini mendukung beberapa protokol, tetapi yang paling menarik bagi saya adalah dukungan USB, yang dijalankan melalui libusb.
Saya akan menjelaskan langkah-langkah untuk membangun demo ini dalam dua bagian. Dalam postingan blog ini, saya akan menjelaskan cara mem-porting libusb itu sendiri, dan trik apa yang mungkin diperlukan untuk mem-porting 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 untuk melihat pratinjau feed live dari DSLR dan dapat mengontrol setelannya melalui USB. Jangan ragu untuk melihat demo live atau demo yang telah direkam sebelumnya sebelum membaca detail teknis:
Catatan tentang keunikan khusus kamera
Anda mungkin memperhatikan 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 hanya menyediakan perintah untuk menambah atau menguranginya dengan jumlah langkah yang ditentukan. Untuk memperumit masalah, API ini juga tidak menampilkan daftar nilai yang benar-benar didukung—daftar yang ditampilkan tampaknya di-hardcode di banyak model kamera Sony.
Saat menetapkan salah satu nilai tersebut, gPhoto2 tidak memiliki pilihan lain selain:
- Ambil satu (atau beberapa) langkah ke arah nilai yang dipilih.
- Tunggu beberapa saat hingga kamera memperbarui setelan.
- Baca kembali nilai yang sebenarnya ditampilkan kamera.
- Pastikan langkah terakhir tidak melompati nilai yang diinginkan atau mengulang ke akhir atau awal daftar.
- Ulangi.
Proses ini mungkin memerlukan waktu, tetapi jika nilai tersebut benar-benar didukung oleh kamera, nilai tersebut akan tercapai, dan jika tidak, proses akan berhenti pada nilai yang didukung terdekat.
Kamera lain mungkin akan memiliki kumpulan setelan, API dasar, dan keunikan yang berbeda. Perlu diingat bahwa gPhoto2 adalah project open source, dan pengujian otomatis atau manual terhadap semua model kamera yang ada tidak mungkin 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", 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 langsung berfungsi. Jika Anda mencobanya di ponsel Android, pastikan untuk beralih ke mode lanskap karena saya tidak terlalu berusaha membuatnya responsif (PR dipersilakan!):
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, mari kita bahas detail teknisnya. Meskipun Anda dapat menyediakan shim API yang mirip dengan libusb (hal ini telah dilakukan oleh orang lain sebelumnya) dan menautkan aplikasi lain ke shim 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 diabstraksi 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. Dengan cara ini, libusb sudah 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),
};
Dengan melihat propertinya, 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 berguna setidaknya untuk menyimpan handle OS ke semua hal tersebut, karena tanpa handle, kita tidak tahu item mana yang diterapkan oleh operasi tertentu. Dalam implementasi web, pengendali 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 di C, tetapi beberapa diimplementasikan di 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 handle 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 menyusunnya 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))) {}
};
Web API 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 spesifiknya, 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 diuraikan dengan aman dari sisi C++. Saya melakukannya dengan kombinasi makro EM_JS dan API Emval.to{Handle, Value}:
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 apa pun yang ditampilkan dari operasi WebUSB dan memeriksa kolom error dan value-nya 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 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 secara manual perangkat yang ingin mereka ekspos atau menolak dialog izin. Setelah itu, aplikasi akan mencantumkan perangkat yang sudah disetujui dan terhubung melalui navigator.usb.getDevices().
Awalnya, saya mencoba menggunakan requestDevice() secara langsung dalam penerapan pengendali get_device_list. Namun, menampilkan dialog izin dengan daftar perangkat terhubung dianggap sebagai operasi sensitif, dan harus dipicu oleh interaksi pengguna (seperti klik tombol di halaman), jika tidak, dialog tersebut akan selalu menampilkan promise yang ditolak. Aplikasi libusb mungkin sering ingin mencantumkan perangkat terhubung saat aplikasi dimulai, sehingga penggunaan requestDevice() bukanlah opsi.
Sebagai gantinya, saya harus menyerahkan pemanggilan navigator.usb.requestDevice() kepada 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 mirip seperti yang telah ditunjukkan di atas. Ada beberapa peretasan yang lebih menarik dalam kode penanganan transfer data, tetapi detail implementasi tersebut kurang penting untuk tujuan artikel ini. Pastikan untuk memeriksa kode dan komentar di GitHub jika Anda tertarik.
Mem-porting loop peristiwa ke web
Satu lagi bagian 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. Loop peristiwa 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, saat setidaknya salah satu sumber tersebut merespons, meneruskannya sebagai peristiwa ke pengendali yang sesuai. Setelah pengendali selesai, kontrol akan kembali ke loop, dan akan dijeda untuk polling lainnya.
Ada beberapa masalah dengan pendekatan ini di web.
Pertama, WebUSB tidak dan tidak dapat mengekspos handle mentah dari perangkat yang mendasarinya, sehingga melakukan polling langsung bukanlah opsi. Kedua, libusb menggunakan API eventfd dan pipe untuk peristiwa 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 peristiwa.
Terakhir, masalah terbesarnya adalah web memiliki loop peristiwanya sendiri. Loop peristiwa global ini digunakan untuk operasi I/O eksternal apa pun (termasuk fetch(), timer, atau, dalam hal ini, WebUSB), dan memanggil pengendali peristiwa atau Promise setiap kali operasi yang sesuai selesai. Menjalankan loop peristiwa bertingkat dan tanpa batas lainnya akan memblokir loop peristiwa browser agar tidak pernah berjalan, yang berarti tidak hanya UI yang akan menjadi tidak responsif, tetapi juga kode tidak akan pernah mendapatkan notifikasi untuk peristiwa I/O yang sama yang ditunggunya. Hal ini biasanya menyebabkan deadlock, dan itulah yang terjadi saat saya mencoba menggunakan libusb dalam demo juga. Halaman berhenti merespons.
Seperti halnya I/O pemblokiran lainnya, untuk mem-porting loop peristiwa tersebut ke web, developer perlu menemukan cara untuk menjalankan loop tersebut tanpa memblokir thread utama. Salah satu caranya adalah dengan merefaktorisasi 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:
#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:
- Memanggil
poll()untuk memeriksa apakah ada peristiwa yang telah dilaporkan oleh backend. Jika ada, loop akan berhenti. Jika tidak, implementasipoll()Emscripten akan segera ditampilkan dengan0. - Panggilan
emscripten_sleep(0). Fungsi ini menggunakan Asyncify dansetTimeout()di balik layar dan digunakan di sini untuk mengembalikan kontrol ke loop peristiwa browser utama. Hal ini memungkinkan browser menangani interaksi pengguna dan peristiwa I/O, termasuk WebUSB. - Periksa apakah waktu tunggu yang ditentukan telah berakhir, dan, jika belum, lanjutkan loop.
Seperti yang disebutkan dalam komentar, pendekatan ini tidak optimal, karena terus menyimpan-memulihkan seluruh stack panggilan dengan Asyncify bahkan saat belum ada peristiwa USB yang harus ditangani (yang sering terjadi), dan karena setTimeout() sendiri memiliki durasi minimal 4 md di browser modern. Namun, hal ini cukup berhasil untuk menghasilkan livestream 13-14 FPS dari DSLR dalam bukti konsep.
Kemudian, saya memutuskan untuk meningkatkannya dengan memanfaatkan sistem peristiwa browser. Ada beberapa cara untuk meningkatkan implementasi ini lebih lanjut, tetapi untuk saat ini saya memilih untuk memunculkan peristiwa kustom langsung pada objek global, tanpa mengaitkannya dengan struktur data libusb tertentu. Saya 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 "membangunkan" dari mode 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 sleep dan wake-up, mekanisme ini memperbaiki masalah efisiensi 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 memerlukan enumerasi perangkat yang dilakukan melalui gestur pengguna. Contoh dan pengujian libusb mengasumsikan bahwa mereka dapat mengenumerasi 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 menyajikan file yang dihasilkan dengan server web statis, melakukan inisialisasi WebUSB, dan menjalankan file HTML yang dapat dieksekusi tersebut secara manual dengan bantuan DevTools.
![Screenshot yang menampilkan jendela Chrome dengan DevTools terbuka di halaman `testlibusb` yang disajikan secara lokal. Konsol DevTools mengevaluasi `navigator.usb.requestDevice({ filters: [] })`, yang memicu dialog izin dan saat ini meminta pengguna untuk memilih perangkat USB yang harus dibagikan ke halaman. ILCE-6600 (kamera Sony) saat ini dipilih.](https://web.developers.google.cn/static/articles/porting-libusb-to-webusb/image/screenshot-showing-chrom-b19a9e4e1b916.png?hl=id)
![Screenshot langkah berikutnya, dengan DevTools masih terbuka. Setelah perangkat dipilih, Konsol telah mengevaluasi ekspresi baru `Module.callMain(['-v'])`, yang menjalankan aplikasi `testlibusb` dalam mode panjang respons. Output menampilkan berbagai informasi mendetail tentang kamera USB yang sebelumnya terhubung: produsen Sony, produk ILCE-6600, nomor seri, konfigurasi, dll.](https://web.developers.google.cn/static/articles/porting-libusb-to-webusb/image/screenshot-the-next-step-bd4e4c7f85be7.png?hl=id)
Mungkin terlihat sederhana, tetapi saat mem-porting library ke platform baru, mencapai tahap di mana library menghasilkan output yang valid untuk pertama kalinya adalah hal yang 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 hal yang perlu Anda lakukan:
- Download libusb terbaru sebagai arsip sebagai bagian dari build Anda atau tambahkan sebagai submodul git di project Anda.
- Jalankan
autoreconf -fivdi folderlibusb. - Jalankan
emconfigure ./configure –host=wasm32 –prefix=/some/installation/pathuntuk melakukan inisialisasi project untuk kompilasi silang dan untuk menetapkan jalur tempat Anda ingin meletakkan artefak yang di-build. - Jalankan
emmake make install. - Arahkan aplikasi atau library tingkat tinggi Anda untuk menelusuri libusb di jalur yang dipilih sebelumnya.
- 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 gilirannya, berasal dari kurangnya pembatalan transfer lintas platform di libusb itu sendiri.
- Tidak ada dukungan transfer isochronous. Seharusnya tidak sulit untuk menambahkannya dengan mengikuti implementasi 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 ke library, kami menerima permintaan pull.
- Batasan lintas platform yang disebutkan sebelumnya. 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 Anda mem-porting perangkat HID atau serial, Anda dapat mengikuti contoh libusb dan mem-porting beberapa library lain ke Fugu API lain. Misalnya, Anda dapat mem-porting library C hidapi ke WebHID dan menghindari masalah tersebut, yang terkait dengan akses USB tingkat rendah, sekaligus.
Kesimpulan
Dalam postingan ini, saya telah menunjukkan cara, dengan bantuan API Emscripten, Asyncify, dan Fugu, bahkan library tingkat rendah seperti libusb dapat di-porting ke web dengan beberapa trik integrasi.
Mem-porting library tingkat rendah yang penting dan banyak digunakan seperti ini sangat bermanfaat, karena pada gilirannya, hal ini juga memungkinkan library tingkat tinggi atau bahkan seluruh aplikasi dibawa ke web. Hal ini membuka pengalaman yang sebelumnya terbatas bagi pengguna satu atau dua platform, ke semua jenis perangkat dan sistem operasi, sehingga pengalaman tersebut tersedia hanya dengan mengklik link.
Dalam postingan berikutnya, saya akan membahas 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, semoga contoh libusb ini menginspirasi Anda dan Anda akan mencoba demonya, bereksperimen dengan library itu sendiri, atau bahkan melanjutkan dan mem-porting library lain yang banyak digunakan ke salah satu API Fugu juga.