जानें कि बाहरी डिवाइसों के साथ इंटरैक्ट करने वाले कोड को, WebAssembly और Fugu API की मदद से वेब पर कैसे पोर्ट किया जा सकता है.
पिछली पोस्ट में, मैंने फ़ाइल सिस्टम एपीआई का इस्तेमाल करके, File System Access API, WebAssembly, और Asyncify की मदद से, ऐप्लिकेशन को वेब पर पोर्ट करने का तरीका बताया था. अब मुझे ज़रूरी सुविधाओं को खोए बिना, WebAssembly के साथ Fugu API को इंटिग्रेट करने और ऐप्लिकेशन को वेब पर पोर्ट करने के विषय को जारी रखना है.
हम आपको बताएंगे कि यूएसबी डिवाइसों के साथ काम करने वाले ऐप्लिकेशन को वेब पर कैसे पोर्ट किया जा सकता है. इसके लिए, libusb को WebAssembly (Emscripten के ज़रिए), Asyncify, और WebUSB पर पोर्ट किया जाएगा. libusb, C में लिखी गई एक लोकप्रिय यूएसबी लाइब्रेरी है.
सबसे पहले, एक डेमो
किसी लाइब्रेरी को पोर्ट करते समय, सबसे अहम बात यह है कि सही डेमो चुना जाए. ऐसा डेमो चुनें जो पोर्ट की गई लाइब्रेरी की क्षमताओं को दिखाए. साथ ही, आपको कई तरीकों से उसकी जांच करने की सुविधा दे और एक ही समय पर विज़ुअल रूप से आकर्षक हो.
मैंने DSLR रिमोट कंट्रोल का आइडिया चुना. खास तौर पर, ओपन सोर्स प्रोजेक्ट gPhoto2, इस क्षेत्र में लंबे समय से काम कर रहा है. इसकी मदद से, कई तरह के डिजिटल कैमरों के लिए रिवर्स-इंजीनियरिंग की जा सकती है और उन्हें इस्तेमाल करने की सुविधा लागू की जा सकती है. यह कई प्रोटोकॉल के साथ काम करता है, लेकिन इसमें मुझे सबसे ज़्यादा दिलचस्पी थी यूएसबी सपोर्ट, जो यह लिब्सब के ज़रिए करती है.
हम इस डेमो को बनाने के तरीके के बारे में दो हिस्सों में बताएंगे. इस ब्लॉग पोस्ट में, मैं बताऊंगा कि मैंने liBusb दूसरी पोस्ट में, मुझे gPhoto2 को पोर्ट करने और इंटिग्रेट करने के बारे में जानकारी मिलेगी.
आखिर में, मुझे एक ऐसा वेब ऐप्लिकेशन मिला जो डीएसएलआर से लाइव फ़ीड की झलक दिखाता है और यूएसबी के ज़रिए उसकी सेटिंग को कंट्रोल कर सकता है. तकनीकी जानकारी पढ़ने से पहले, लाइव या पहले से रिकॉर्ड किया गया डेमो देखें:
कैमरे से जुड़ी समस्याओं के बारे में जानकारी
आपको पता होगा कि वीडियो में सेटिंग बदलने में कुछ समय लगता है. आपको दिखने वाली ज़्यादातर अन्य समस्याओं की तरह, यह समस्या WebAssembly या WebUSB की परफ़ॉर्मेंस की वजह से नहीं होती. यह समस्या, gPhoto2 के डेमो के लिए चुने गए कैमरे के साथ इंटरैक्ट करने के तरीके की वजह से होती है.
Sony a6600 में, आईएसओ, अपर्चर या शटर स्पीड जैसी वैल्यू को सीधे सेट करने के लिए एपीआई उपलब्ध नहीं है. इसके बजाय, इन वैल्यू को तय की गई संख्या में बढ़ाने या घटाने के लिए सिर्फ़ निर्देश दिए जाते हैं. समस्या को और मुश्किल बनाने के लिए, यह काम करने वाली वैल्यू की सूची भी नहीं दिखाता. ऐसा लगता है कि दिखाई गई सूची, Sony के कई कैमरा मॉडल में पहले से मौजूद है.
इनमें से किसी एक वैल्यू को सेट करते समय, gPhoto2 के पास इन विकल्पों के अलावा कोई दूसरा विकल्प नहीं होता:
- चुनी गई वैल्यू की दिशा में एक या उससे ज़्यादा कदम आगे बढ़ें.
- कैमरे की सेटिंग अपडेट होने तक इंतज़ार करें.
- पढ़ें कि कैमरा असल में किस वैल्यू पर पहुंचा.
- देखें कि आखिरी चरण में, सही वैल्यू पर पहुंचा गया हो या सूची के आखिर या शुरुआत में न पहुंचा हो.
- दोहराएं.
इसमें कुछ समय लग सकता है. हालांकि, अगर यह वैल्यू असल में कैमरे के साथ काम करती है, तो वह वहां पहुंच जाएगी. अगर ऐसा नहीं होता है, तो वह वैल्यू, उसके लिए तय की गई सबसे करीबी वैल्यू पर रोक दी जाएगी.
अन्य कैमरों में सेटिंग के अलग-अलग सेट, बुनियादी एपीआई, और क्वर्क अलग हो सकते हैं. ध्यान रखें कि gPhoto2 एक ओपन-सोर्स प्रोजेक्ट है. इसलिए, सभी कैमरा मॉडल की अपने-आप या मैन्युअल तरीके से जांच करना मुमकिन नहीं है. इसलिए, समस्या की पूरी जानकारी वाली रिपोर्ट और पीआर का हमेशा स्वागत है. हालांकि, पहले यह पक्का कर लें कि समस्याएं, आधिकारिक gPhoto2 क्लाइंट पर भी आ रही हों.
क्रॉस-प्लैटफ़ॉर्म के साथ काम करने से जुड़ी ज़रूरी जानकारी
माफ़ करें, Windows पर "अच्छी तरह से जाने-पहचाने" डिवाइसों को एक सिस्टम ड्राइवर असाइन किया जाता है. इसमें DSLR कैमरे भी शामिल हैं. यह ड्राइवर, WebUSB के साथ काम नहीं करता. अगर आपको Windows पर डेमो आज़माना है, तो Zadig जैसे टूल का इस्तेमाल करके, कनेक्ट किए गए डीएसएलआर के ड्राइवर को WinUSB या libusb पर बदलना होगा. यह तरीका मेरे और कई अन्य उपयोगकर्ताओं के लिए ठीक काम करता है. हालांकि, आपको इसे अपने जोखिम पर इस्तेमाल करना चाहिए.
Linux पर, WebUSB की मदद से अपने डीएसएलआर को ऐक्सेस करने के लिए, आपको कस्टम अनुमतियां सेट करनी होंगी. हालांकि, यह आपके डिस्ट्रिब्यूशन पर निर्भर करता है.
macOS और Android पर, डेमो एकदम अलग तरीके से काम करना चाहिए. अगर इसे Android फ़ोन पर आज़माया जा रहा है, तो लैंडस्केप मोड पर स्विच करना न भूलें. मैंने इसे रिस्पॉन्सिव बनाने के लिए ज़्यादा मेहनत नहीं की है. अगर आपको इसे रिस्पॉन्सिव बनाना है, तो हमें PR भेजें!:
अलग-अलग प्लैटफ़ॉर्म पर WebUSB के इस्तेमाल के बारे में ज़्यादा जानकारी पाने के लिए, "Webयूएसबी के लिए डिवाइस बनाना" में "प्लैटफ़ॉर्म के हिसाब से ध्यान देने वाली बातें" सेक्शन देखें.
libusb में नया बैकएंड जोड़ना
अब तकनीकी जानकारी पर जाएं. libusb जैसा ही एक शिम एपीआई उपलब्ध कराया जा सकता है (पहले भी ऐसा किया जा चुका है) और दूसरे ऐप्लिकेशन को इसके साथ लिंक किया जा सकता है. हालांकि, इस तरीके से गड़बड़ियां हो सकती हैं. साथ ही, आगे कोई भी एक्सटेंशन या रखरखाव करना मुश्किल हो जाता है. मुझे सही तरीके से काम करना था, ताकि आने वाले समय में इसे अपस्ट्रीम में वापस लाया जा सके और libusb में मर्ज किया जा सके.
सौभाग्य से, libusb README में बताया गया है:
“libusb को अंदरूनी तौर पर इस तरह से बनाया गया है कि इसे अन्य ऑपरेटिंग सिस्टम पर पोर्ट किया जा सकता है. ज़्यादा जानकारी के लिए, कृपया पोर्टिंग फ़ाइल देखें.”
li बसb को इस तरह से तैयार किया गया है कि सार्वजनिक एपीआई, "बैकएंड" से अलग होता है. ये बैकएंड, ऑपरेटिंग सिस्टम के लो-लेवल एपीआई की मदद से डिवाइसों की सूची बनाने, उन्हें खोलने, बंद करने, और उनसे असल में कनेक्ट करने के लिए ज़िम्मेदार होते हैं. libusb, Linux, macOS, Windows, Android, OpenBSD/NetBSD, Haiku, और Solaris के बीच के अंतर को पहले से ही हटा देता है. साथ ही, यह इन सभी प्लैटफ़ॉर्म पर काम करता है.
मुझे Emscripten+WebUSB "ऑपरेटिंग सिस्टम" के लिए, एक और बैकएंड जोड़ना था. इन बैकएंड को लागू करने की प्रोसेस, libusb/os
फ़ोल्डर में मौजूद है:
~/w/d/libusb $ ls libusb/os
darwin_usb.c haiku_usb_raw.h threads_posix.lo
darwin_usb.h linux_netlink.c threads_posix.o
events_posix.c linux_udev.c threads_windows.c
events_posix.h linux_usbfs.c threads_windows.h
events_posix.lo linux_usbfs.h windows_common.c
events_posix.o netbsd_usb.c windows_common.h
events_windows.c null_usb.c windows_usbdk.c
events_windows.h openbsd_usb.c windows_usbdk.h
haiku_pollfs.cpp sunos_usb.c windows_winusb.c
haiku_usb_backend.cpp sunos_usb.h windows_winusb.h
haiku_usb.h threads_posix.c
haiku_usb_raw.cpp threads_posix.h
हर बैकएंड में सामान्य टाइप और हेल्पर के साथ libusbi.h
हेडर शामिल होता है. साथ ही, इसमें usbi_os_backend
टाइप का usbi_backend
वैरिएबल एक्सपोज़र करना ज़रूरी होता है. उदाहरण के लिए, Windows बैकएंड ऐसा दिखता है:
const struct usbi_os_backend usbi_backend = {
"Windows",
USBI_CAP_HAS_HID_ACCESS,
windows_init,
windows_exit,
windows_set_option,
windows_get_device_list,
NULL, /* hotplug_poll */
NULL, /* wrap_sys_device */
windows_open,
windows_close,
windows_get_active_config_descriptor,
windows_get_config_descriptor,
windows_get_config_descriptor_by_value,
windows_get_configuration,
windows_set_configuration,
windows_claim_interface,
windows_release_interface,
windows_set_interface_altsetting,
windows_clear_halt,
windows_reset_device,
NULL, /* alloc_streams */
NULL, /* free_streams */
NULL, /* dev_mem_alloc */
NULL, /* dev_mem_free */
NULL, /* kernel_driver_active */
NULL, /* detach_kernel_driver */
NULL, /* attach_kernel_driver */
windows_destroy_device,
windows_submit_transfer,
windows_cancel_transfer,
NULL, /* clear_transfer_priv */
NULL, /* handle_events */
windows_handle_transfer_completion,
sizeof(struct windows_context_priv),
sizeof(union windows_device_priv),
sizeof(struct windows_device_handle_priv),
sizeof(struct windows_transfer_priv),
};
प्रॉपर्टी को देखते हुए, हमने देखा कि स्ट्रक्चर में बैकएंड नाम, इसकी क्षमताओं का एक सेट, फ़ंक्शन पॉइंटर के रूप में, कई लो-लेवल यूएसबी कार्रवाइयों के लिए हैंडलर, और आखिर में निजी डिवाइस-/कॉन्टेक्स्ट-/ट्रांसफ़र-लेवल डेटा को स्टोर करने के लिए तय किए गए साइज़ शामिल हैं.
निजी डेटा फ़ील्ड, कम से कम उन सभी चीज़ों के लिए ओएस हैंडल को सेव करने के लिए काम के होते हैं. हैंडल के बिना, हमें यह पता नहीं चलता कि कोई भी ऑपरेशन किस आइटम पर लागू होता है. वेब पर लागू करने के लिए, ओएस हैंडल, WebUSB JavaScript ऑब्जेक्ट होंगे. Emscripten में उन्हें दिखाने और सेव करने का सामान्य तरीका emscripten::val
क्लास है. यह क्लास Embind (एम्स्क्रिप्टन का बाइंडिंग सिस्टम) के हिस्से के तौर पर दी जाती है.
फ़ोल्डर में मौजूद ज़्यादातर बैकएंड, C में लागू किए गए हैं. हालांकि, कुछ बैकएंड C++ में लागू किए गए हैं. Embind सिर्फ़ C++ के साथ काम करता है. इसलिए, यह विकल्प मेरे लिए चुना गया था. मैंने ज़रूरी स्ट्रक्चर के साथ libusb/libusb/os/emscripten_webusb.cpp
और निजी डेटा फ़ील्ड के लिए sizeof(val)
जोड़ा है:
#include <emscripten.h>
#include <emscripten/val.h>
#include "libusbi.h"
using namespace emscripten;
// …function implementations
const usbi_os_backend usbi_backend = {
.name = "Emscripten + WebUSB backend",
.caps = LIBUSB_CAP_HAS_CAPABILITY,
// …handlers—function pointers to implementations above
.device_priv_size = sizeof(val),
.transfer_priv_size = sizeof(val),
};
WebUSB ऑब्जेक्ट को डिवाइस हैंडल के तौर पर सेव करना
libusb, निजी डेटा के लिए तय किए गए एरिया में, इस्तेमाल के लिए तैयार पॉइंटर उपलब्ध कराता है. उन पॉइंटर को val
इंस्टेंस के तौर पर इस्तेमाल करने के लिए, मैंने कुछ छोटे हेल्पर जोड़े हैं. ये हेल्पर, पॉइंटर को इन-प्लेस में बनाते हैं, उन्हें रेफ़रंस के तौर पर वापस लाते हैं, और वैल्यू को बाहर ले जाते हैं:
// We store an Embind handle to WebUSB USBDevice in "priv" metadata of
// libusb device, this helper returns a pointer to it.
struct ValPtr {
public:
void init_to(val &&value) { new (ptr) val(std::move(value)); }
val &get() { return *ptr; }
val take() { return std::move(get()); }
protected:
ValPtr(val *ptr) : ptr(ptr) {}
private:
val *ptr;
};
struct WebUsbDevicePtr : ValPtr {
public:
WebUsbDevicePtr(libusb_device *dev)
: ValPtr(static_cast<val *>(usbi_get_device_priv(dev))) {}
};
val &get_web_usb_device(libusb_device *dev) {
return WebUsbDevicePtr(dev).get();
}
struct WebUsbTransferPtr : ValPtr {
public:
WebUsbTransferPtr(usbi_transfer *itransfer)
: ValPtr(static_cast<val *>(usbi_get_transfer_priv(itransfer))) {}
};
सिंक्रोनस C कॉन्टेक्स्ट में एसिंक्रोनस वेब एपीआई
अब ऐसे तरीके की ज़रूरत है जिससे असाइनक WebUSB API को मैनेज किया जा सके, जहां libusb को सिंक्रोनस ऑपरेशन की उम्मीद होती है. इसके लिए, मैं Asyncify या खास तौर पर, val::await()
के ज़रिए Embind इंटिग्रेशन का इस्तेमाल कर सकता हूं.
मुझे WebUSB से जुड़ी गड़बड़ियों को सही तरीके से मैनेज करना था और उन्हें libusb गड़बड़ी कोड में बदलना था. हालांकि, फ़िलहाल Embind में, C++ साइड से JavaScript अपवाद या Promise
अस्वीकार करने की समस्या को मैनेज करने का कोई तरीका नहीं है. इस समस्या को हल करने के लिए, JavaScript साइड पर अस्वीकार किए जाने की जानकारी को कैप्चर करें और नतीजे को { error, value }
ऑब्जेक्ट में बदलें. अब C++ साइड से इस ऑब्जेक्ट को सुरक्षित तरीके से पार्स किया जा सकता है. मैंने EM_JS
मैक्रो और 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()};
}
};
अब मैं WebUSB ऑपरेशन से मिले किसी भी Promise
पर promise_result::await()
का इस्तेमाल कर सकता हूं. साथ ही, इसके error
और value
फ़ील्ड की अलग-अलग जांच कर सकता हूं.
उदाहरण के लिए, libusb_device_handle
से USBDevice
दिखाने वाले val
को वापस पाना, उसके open()
तरीके को कॉल करना, उसके नतीजे का इंतज़ार करना, और लिब्सब स्टेटस कोड के रूप में गड़बड़ी का कोड दिखाना इस तरह दिखता है:
int em_open(libusb_device_handle *handle) {
auto web_usb_device = get_web_usb_device(handle->dev);
return promise_result::await(web_usb_device.call<val>("open")).error;
}
डिवाइस की गिनती
किसी भी डिवाइस को खोलने से पहले, libusb को उपलब्ध डिवाइसों की सूची वापस लानी होगी. बैकएंड को get_device_list
हैंडलर के ज़रिए यह कार्रवाई लागू करनी होगी.
समस्या यह है कि सुरक्षा से जुड़ी वजहों से, दूसरे प्लैटफ़ॉर्म के उलट वेब पर कनेक्ट किए गए सभी यूएसबी डिवाइसों की गिनती नहीं की जा सकती. इसके बजाय, फ़्लो को दो हिस्सों में बांटा जाता है. सबसे पहले, वेब ऐप्लिकेशन navigator.usb.requestDevice()
के ज़रिए, खास प्रॉपर्टी वाले डिवाइसों का अनुरोध करता है. इसके बाद, उपयोगकर्ता मैन्युअल तरीके से चुनता है कि उसे किस डिवाइस को एक्सपोज़ करना है या अनुमति के अनुरोध को अस्वीकार करना है. इसके बाद, ऐप्लिकेशन navigator.usb.getDevices()
के ज़रिए, पहले से मंज़ूरी पा चुके और कनेक्ट किए गए डिवाइसों की सूची दिखाता है.
सबसे पहले, मैंने get_device_list
हैंडलर को लागू करने के लिए, सीधे requestDevice()
का इस्तेमाल करने की कोशिश की. हालांकि, कनेक्ट किए गए डिवाइसों की सूची के साथ अनुमति का अनुरोध दिखाना एक संवेदनशील कार्रवाई माना जाता है. इसे उपयोगकर्ता के इंटरैक्शन (जैसे, पेज पर बटन पर क्लिक करना) से ट्रिगर किया जाना चाहिए. ऐसा न करने पर, यह हमेशा अस्वीकार किए गए प्रॉमिस को दिखाता है. libusb ऐप्लिकेशन, अक्सर ऐप्लिकेशन के शुरू होने पर कनेक्ट किए गए डिवाइसों की सूची दिखाना चाहते हैं. इसलिए, requestDevice()
का इस्तेमाल नहीं किया जा सकता.
इसके बजाय, मुझे navigator.usb.requestDevice()
को कॉल करने की सुविधा, असली डेवलपर को देनी पड़ी. साथ ही, navigator.usb.getDevices()
से सिर्फ़ उन डिवाइसों को दिखाया गया जिन्हें पहले से मंज़ूरी मिल चुकी थी:
// Store the global `navigator.usb` once upon initialisation.
thread_local const val web_usb = val::global("navigator")["usb"];
int em_get_device_list(libusb_context *ctx, discovered_devs **devs) {
// C++ equivalent of `await navigator.usb.getDevices()`.
// Note: at this point we must already have some devices exposed -
// caller must have called `await navigator.usb.requestDevice(...)`
// in response to user interaction before going to LibUSB.
// Otherwise this list will be empty.
auto result = promise_result::await(web_usb.call<val>("getDevices"));
if (result.error) {
return result.error;
}
auto &web_usb_devices = result.value;
// Iterate over the exposed devices.
uint8_t devices_num = web_usb_devices["length"].as<uint8_t>();
for (uint8_t i = 0; i < devices_num; i++) {
auto web_usb_device = web_usb_devices[i];
// …
*devs = discovered_devs_append(*devs, dev);
}
return LIBUSB_SUCCESS;
}
ज़्यादातर बैकएंड कोड, val
और promise_result
का इस्तेमाल उसी तरह करता है जिस तरह ऊपर दिखाया गया है. डेटा ट्रांसफ़र हैंडलिंग कोड में कुछ और दिलचस्प हैक हैं, लेकिन इस लेख के लिए, लागू करने से जुड़ी वह जानकारी कम अहम है. अगर आपकी दिलचस्पी है, तो GitHub पर कोड और टिप्पणियों को देखना न भूलें.
इवेंट लूप को वेब पर पोर्ट करना
इवेंट मैनेज करने के बारे में हम एक और लिबब पोर्ट के बारे में बात करना चाहते हैं. पिछले लेख में बताया गया था कि C जैसी सिस्टम भाषाओं में ज़्यादातर एपीआई सिंक्रोनस होते हैं. इवेंट मैनेजमेंट भी सिंक्रोनस होता है. इसे आम तौर पर, इनफ़ाइनाइट लूप से लागू किया जाता है. बाहरी I/O सोर्स के किसी सेट से "पोल" (डेटा को पढ़ने या कुछ डेटा उपलब्ध होने तक एक्ज़ीक्यूशन को ब्लॉक करता है). साथ ही, जब उनमें से कम से कम कोई एक जवाब देता है, तो वह उससे जुड़े हैंडलर को इवेंट के तौर पर पास करता है. हैंडलर पूरा होने के बाद, कंट्रोल लूप में वापस आ जाता है और अगले पोल के लिए रुक जाता है.
वेब पर इस तरीके में कई समस्याएं हैं.
पहला, WebUSB, डिवाइसों के रॉ हैंडल को एक्सपोज़ नहीं करता और न ही कर सकता. इसलिए, उनसे सीधे तौर पर पोलिंग नहीं की जा सकती. दूसरा, libusb, अन्य इवेंट के साथ-साथ रॉ डिवाइस हैंडल के बिना ऑपरेटिंग सिस्टम पर ट्रांसफ़र मैनेज करने के लिए, eventfd
और pipe
एपीआई का इस्तेमाल करता है. हालांकि, फ़िलहाल Emscripten में eventfd
काम नहीं करता. साथ ही, pipe
काम करता है, लेकिन फ़िलहाल यह खास जानकारी के मुताबिक नहीं है और यह इवेंट के लिए इंतज़ार नहीं कर सकता.
आखिर में, सबसे बड़ी समस्या यह है कि वेब का अपना इवेंट लूप होता है. इस ग्लोबल इवेंट लूप का इस्तेमाल, किसी भी बाहरी I/O ऑपरेशन के लिए किया जाता है. इनमें fetch()
, टाइमर या इस मामले में, WebUSB शामिल है. साथ ही, जब इन ऑपरेशन को पूरा कर लिया जाता है, तब यह इवेंट या Promise
हैंडलर को ट्रिगर करता है. नेस्ट किए गए किसी अन्य अनलिमिटेड इवेंट लूप को चलाने से, ब्राउज़र के इवेंट लूप को आगे बढ़ने से रोक दिया जाएगा. इसका मतलब है कि यूज़र इंटरफ़ेस (यूआई) न सिर्फ़ काम नहीं करेगा, बल्कि कोड को उन I/O इवेंट के लिए कभी सूचनाएं नहीं मिलेंगी जिनकी वह इंतज़ार कर रहा है. आम तौर पर, इससे डेडलॉक की स्थिति पैदा होती है. ऐसा ही तब हुआ, जब मैंने किसी डेमो में libusb का इस्तेमाल करने की कोशिश की. पेज फ़्रीज़ हो गया.
ब्लॉक करने वाले अन्य I/O की तरह, ऐसे इवेंट लूप को वेब पर पोर्ट करने के लिए, डेवलपर को मुख्य थ्रेड को ब्लॉक किए बिना उन लूप को चलाने का तरीका ढूंढना होगा. इसका एक तरीका यह है कि I/O इवेंट को एक अलग थ्रेड में मैनेज करने के लिए, ऐप्लिकेशन को रीफ़ैक्टर किया जाए और नतीजों को मुख्य थ्रेड में वापस पास किया जाए. दूसरा तरीका, लूप को रोकने और इवेंट के लिए बिना ब्लॉक किए इंतज़ार करने के लिए, Asyncify का इस्तेमाल करना है.
मुझे libusb या gPhoto2 में कोई अहम बदलाव नहीं करना था. साथ ही, मैंने Promise
इंटिग्रेशन के लिए पहले ही Asyncify का इस्तेमाल कर लिया है. इसलिए, मैंने यही तरीका चुना है. poll()
के ब्लॉकिंग वैरिएंट को सिम्युलेट करने के लिए, मैंने कॉन्सेप्ट के शुरुआती सबूत के तौर पर, नीचे दिखाए गए लूप का इस्तेमाल किया है:
#ifdef __EMSCRIPTEN__
// TODO: optimize this. Right now it will keep unwinding-rewinding the stack
// on each short sleep until an event comes or the timeout expires.
// We should probably create an actual separate thread that does signaling
// or come up with a custom event mechanism to report events from
// `usbi_signal_event` and process them here.
double until_time = emscripten_get_now() + timeout_ms;
do {
// Emscripten `poll` ignores timeout param, but pass 0 explicitly just
// in case.
num_ready = poll(fds, nfds, 0);
if (num_ready != 0) break;
// Yield to the browser event loop to handle events.
emscripten_sleep(0);
} while (emscripten_get_now() < until_time);
#else
num_ready = poll(fds, nfds, timeout_ms);
#endif
यह सुविधा:
poll()
को कॉल करके यह पता लगाता है कि बैकएंड ने अब तक कोई इवेंट रिपोर्ट किया है या नहीं. अगर कोई है, तो लूप रुक जाता है. ऐसा न होने पर, Emscripten मेंpoll()
को लागू करने पर, तुरंत0
दिखेगा.emscripten_sleep(0)
को कॉल करता है. यह फ़ंक्शन, Asyncify औरsetTimeout()
का इस्तेमाल करता है. इसका इस्तेमाल, मुख्य ब्राउज़र इवेंट लूप को फिर से कंट्रोल करने के लिए किया जाता है. इससे ब्राउज़र, WebUSB के साथ-साथ उपयोगकर्ता के किसी भी इंटरैक्शन और I/O इवेंट को मैनेज कर सकता है.- देखें कि तय किए गए टाइम आउट की समयसीमा खत्म तो नहीं हो गई है. अगर समय खत्म नहीं हुआ है, तो लूप को जारी रखें.
जैसा कि टिप्पणी में बताया गया है, यह तरीका सही नहीं था, क्योंकि इसने पूरे कॉल स्टैक को एसिंक्रोनस की मदद से सेव करके रखा. भले ही, अब तक कोई यूएसबी इवेंट हैंडल न किया गया हो (ज़्यादातर समय ऐसा होता है). साथ ही, आधुनिक ब्राउज़र में setTimeout()
की अवधि कम से कम 4 मि॰से॰ होती है. इसके बावजूद, यह सुविधा इतनी अच्छी थी कि इसकी मदद से, डीएसएलआर से 13 से 14 एफ़पीएस की लाइव स्ट्रीम की जा सकी.
बाद में, मैंने ब्राउज़र इवेंट सिस्टम का फ़ायदा उठाकर, इसे बेहतर बनाने का फ़ैसला किया. इस तरीके को और बेहतर बनाने के कई तरीके हैं. हालांकि, फ़िलहाल मैंने कस्टम इवेंट को सीधे ग्लोबल ऑब्जेक्ट पर उत्सर्जित करने का विकल्प चुना है. ऐसा करने के लिए, इवेंट को किसी खास libusb डेटा स्ट्रक्चर से नहीं जोड़ा गया है. मैंने EM_ASYNC_JS
मैक्रो के आधार पर, इंतज़ार और सूचना देने वाले इस तरीके का इस्तेमाल किया है:
EM_JS(void, em_libusb_notify, (void), {
dispatchEvent(new Event("em-libusb"));
});
EM_ASYNC_JS(int, em_libusb_wait, (int timeout), {
let onEvent, timeoutId;
try {
return await new Promise(resolve => {
onEvent = () => resolve(0);
addEventListener('em-libusb', onEvent);
timeoutId = setTimeout(resolve, timeout, -1);
});
} finally {
removeEventListener('em-libusb', onEvent);
clearTimeout(timeoutId);
}
});
जब भी libb किसी इवेंट को रिपोर्ट करने की कोशिश करता है, तब em_libusb_notify()
फ़ंक्शन का इस्तेमाल किया जाता है, जैसे कि डेटा ट्रांसफ़र पूरा होना:
void usbi_signal_event(usbi_event_t *event)
{
uint64_t dummy = 1;
ssize_t r;
r = write(EVENT_WRITE_FD(event), &dummy, sizeof(dummy));
if (r != sizeof(dummy))
usbi_warn(NULL, "event write failed");
#ifdef __EMSCRIPTEN__
em_libusb_notify();
#endif
}
इस बीच, em_libusb_wait()
हिस्से का इस्तेमाल, Asyncify के स्लीप मोड से "जागने" के लिए किया जाता है. ऐसा तब होता है, जब कोई em-libusb
इवेंट मिलता है या टाइम आउट की समयसीमा खत्म हो जाती है:
double until_time = emscripten_get_now() + timeout_ms;
for (;;) {
// Emscripten `poll` ignores timeout param, but pass 0 explicitly just
// in case.
num_ready = poll(fds, nfds, 0);
if (num_ready != 0) break;
int timeout = until_time - emscripten_get_now();
if (timeout <= 0) break;
int result = em_libusb_wait(timeout);
if (result != 0) break;
}
सोने और स्क्रीन चालू करने की संख्या में काफ़ी कमी आने की वजह से, इस तरीके से emscripten_sleep()
की मदद से काम करने की क्षमता से जुड़ी समस्याओं को ठीक किया गया. साथ ही, डीएसएलआर डेमो के थ्रूपुट को 13 से 14 FPS (फ़्रेम प्रति सेकंड) से बढ़ाकर, 30 से ज़्यादा FPS (फ़्रेम प्रति सेकंड) किया गया. यह एक शानदार लाइव फ़ीड के लिए काफ़ी है.
सिस्टम बनाएं और पहला टेस्ट करें
बैकएंड बन जाने के बाद, मुझे इसे Makefile.am
और configure.ac
में जोड़ना पड़ा. यहां सिर्फ़ Emscripten के फ़्लैग में बदलाव करना दिलचस्प है:
emscripten)
AC_SUBST(EXEEXT, [.html])
# Note: LT_LDFLAGS is not enough here because we need link flags for executable.
AM_LDFLAGS="${AM_LDFLAGS} --bind -s ASYNCIFY -s ASSERTIONS -s ALLOW_MEMORY_GROWTH -s INVOKE_RUN=0 -s EXPORTED_RUNTIME_METHODS=['callMain']"
;;
पहली बात, Unix प्लैटफ़ॉर्म पर एक्ज़ीक्यूटेबल में आम तौर पर फ़ाइल एक्सटेंशन नहीं होते. हालांकि, Emscripten अलग-अलग तरह का आउटपुट देता है. यह इस बात पर निर्भर करता है कि आपने किस एक्सटेंशन का अनुरोध किया है. मैं AC_SUBST(EXEEXT, …)
का इस्तेमाल करके, एक्सीक्यूटेबल एक्सटेंशन को .html
में बदल रहा हूं, ताकि पैकेज में मौजूद कोई भी एक्सीक्यूटेबल—टेस्ट और उदाहरण—Emscripten के डिफ़ॉल्ट शेल के साथ एचटीएमएल बन जाए. यह शेल, JavaScript और WebAssembly को लोड और इंस्टैंशिएट करता है.
दूसरा, Embind और Asyncify का इस्तेमाल करने की वजह से, मुझे उन सुविधाओं (--bind -s ASYNCIFY
) को चालू करना होगा. साथ ही, लिंकर पैरामीटर की मदद से, डाइनैमिक मेमोरी में बढ़ोतरी (-s ALLOW_MEMORY_GROWTH
) की अनुमति देनी होगी. माफ़ करें, लाइब्रेरी के पास उन फ़्लैग को लिंकर को रिपोर्ट करने का कोई तरीका नहीं है. इसलिए, इस libusb पोर्ट का इस्तेमाल करने वाले हर ऐप्लिकेशन को अपने बिल्ड कॉन्फ़िगरेशन में भी वही लिंकर फ़्लैग जोड़ने होंगे.
आखिर में, जैसा कि पहले बताया गया है, WebUSB के लिए डिवाइस की गिनती, उपयोगकर्ता के जेस्चर के ज़रिए की जानी चाहिए. libusb के उदाहरणों और टेस्ट में यह माना जाता है कि वे डिवाइसों की गिनती स्टार्ट-अप के समय कर सकते हैं. साथ ही, बिना किसी बदलाव के गड़बड़ी के साथ काम करना बंद कर सकते हैं. इसके बजाय, मुझे अपने-आप लागू होने की सुविधा (-s INVOKE_RUN=0
) बंद करनी पड़ी और मैन्युअल callMain()
तरीके (-s EXPORTED_RUNTIME_METHODS=...
) को दिखाना पड़ा.
यह सब करने के बाद, मैंने जनरेट की गई फ़ाइलों को स्टैटिक वेब सर्वर से दिखाया, WebUSB को शुरू किया, और DevTools की मदद से उन एचटीएमएल एक्सीक्यूटेबल को मैन्युअल तरीके से चलाया.
ऐसा लगता है कि यह कोई बड़ी बात नहीं है, लेकिन लाइब्रेरी को किसी नए प्लैटफ़ॉर्म पर पोर्ट करते समय, पहली बार मान्य आउटपुट मिलने पर बहुत खुशी होती है!
पोर्ट का इस्तेमाल करना
ऊपर बताए गए तरीके के मुताबिक, पोर्ट Emscripten की कुछ सुविधाओं पर निर्भर करता है. फ़िलहाल, ऐप्लिकेशन को लिंक करने के दौरान इन सुविधाओं को चालू करना ज़रूरी है. अगर आपको अपने ऐप्लिकेशन में इस libusb पोर्ट का इस्तेमाल करना है, तो आपको यह करना होगा:
- नए libusb को अपने बिल्ड के हिस्से के तौर पर संग्रह के तौर पर डाउनलोड करें या उसे अपने प्रोजेक्ट में git सबमॉड्यूल के तौर पर जोड़ें.
libusb
फ़ोल्डर मेंautoreconf -fiv
चलाएं.- क्रॉस-कंपाइलेशन के लिए प्रोजेक्ट को शुरू करने और वह पाथ सेट करने के लिए
emconfigure ./configure –host=wasm32 –prefix=/some/installation/path
चलाएं जहां आपको बिल्ट किए गए आर्टफ़ैक्ट डालने हैं. emmake make install
चलाएं.- अपने ऐप्लिकेशन या उच्च-लेवल लाइब्रेरी को, पहले चुने गए पाथ में libusb खोजने के लिए निर्देश दें.
- अपने ऐप्लिकेशन के लिंक आर्ग्युमेंट में ये फ़्लैग जोड़ें:
--bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH
.
फ़िलहाल, लाइब्रेरी में कुछ सीमाएं हैं:
- ट्रांसफ़र रद्द करने की सुविधा उपलब्ध नहीं है. यह WebUSB की एक सीमा है. यह सीमा, libusb में क्रॉस-प्लैटफ़ॉर्म ट्रांसफ़र रद्द करने की सुविधा न होने की वजह से है.
- ट्रांसफ़र करने की सुविधा एक ही समय पर उपलब्ध नहीं है. उदाहरण के तौर पर, मौजूदा ट्रांसफ़र मोड को लागू करके, इसे जोड़ना मुश्किल नहीं होना चाहिए. हालांकि, यह एक ऐसा मोड है जो आम तौर पर इस्तेमाल नहीं किया जाता. साथ ही, मेरे पास इसकी जांच करने के लिए कोई डिवाइस नहीं है. इसलिए, फ़िलहाल मैंने इसे 'काम नहीं करता' के तौर पर सेट कर दिया है. अगर आपके पास ऐसे डिवाइस हैं और आपको लाइब्रेरी में योगदान देना है, तो हमें PR भेजें!
- पहले बताई गई, अलग-अलग प्लैटफ़ॉर्म पर काम करने से जुड़ी सीमाएं. ये पाबंदियां ऑपरेटिंग सिस्टम लगाते हैं. इसलिए, हम यहां कुछ नहीं कर सकते. हालांकि, हम उपयोगकर्ताओं से ड्राइवर या अनुमतियों को बदलने के लिए कह सकते हैं. हालांकि, अगर एचआईडी या सीरियल डिवाइसों को पोर्ट किया जा रहा है, तो libusb के उदाहरण का पालन करें और किसी अन्य लाइब्रेरी को किसी दूसरे Fugu API पर पोर्ट करें. उदाहरण के लिए, hidapi को WebHID में पोर्ट किया जा सकता है. इससे, यूएसबी के लो-लेवल ऐक्सेस से जुड़ी समस्याओं से पूरी तरह से बचा जा सकता है.
नतीजा
इस पोस्ट में, मैंने बताया है कि Emscripten, Asyncify, और Fugu API की मदद से, libusb जैसी लो-लेवल लाइब्रेरी को भी वेब पर पोर्ट किया जा सकता है. इसके लिए, इंटिग्रेशन से जुड़ी कुछ तरकीबों का इस्तेमाल किया जाता है.
इस तरह की ज़रूरी और बड़े स्तर पर इस्तेमाल की जाने वाली लाइब्रेरी को पोर्ट करना काफ़ी फ़ायदेमंद होता है. इसकी वजह यह है कि इसकी मदद से, ऊपर के लेवल की लाइब्रेरी या पूरे ऐप्लिकेशन को भी वेब पर लाया जा सकता है. इससे, पहले सिर्फ़ एक या दो प्लैटफ़ॉर्म के उपयोगकर्ताओं के लिए उपलब्ध सुविधाएं, सभी तरह के डिवाइसों और ऑपरेटिंग सिस्टम के लिए उपलब्ध हो जाती हैं. साथ ही, उन सुविधाओं को सिर्फ़ एक लिंक पर क्लिक करके ऐक्सेस किया जा सकता है.
अगली पोस्ट में, हम आपको वेब gPhoto2 डेमो बनाने का तरीका बताऊँगा. इससे न सिर्फ़ डिवाइस की जानकारी मिलेगी, बल्कि काफ़ी हद तक libb की ट्रांसफ़र सुविधा का भी इस्तेमाल किया जाएगा. इस बीच, हमें उम्मीद है कि आपको libusb का उदाहरण पसंद आया होगा. साथ ही, आपने डेमो आज़माया होगा और लाइब्रेरी को खुद आज़माया होगा. इसके अलावा, हो सकता है कि आपने किसी दूसरी लाइब्रेरी को भी Fugu के किसी एपीआई पर पोर्ट किया हो.