वेब पर I/O API असिंक्रोनस होते हैं, लेकिन ज़्यादातर सिस्टम भाषाओं में सिंक्रोनस होते हैं. कोड को WebAssembly में संकलित करते समय, आपको एक तरह के एपीआई को दूसरे से जोड़ना होगा. यह ब्रिज, Asyncify है. इस पोस्ट में, आपको यह जानकारी मिलेगी कि Asyncify का इस्तेमाल कब और कैसे किया जा सकता है. साथ ही, यह भी बताया गया है कि यह सुविधा कैसे काम करती है.
सिस्टम की भाषाओं में I/O
मैं C में एक आसान उदाहरण से शुरू करूंगा. मान लें कि आपको किसी फ़ाइल से उपयोगकर्ता का नाम पढ़ना है और उसे "नमस्ते, (उपयोगकर्ता नाम)!" मैसेज भेजकर नमस्ते कहना है, तो:
#include <stdio.h>
int main() {
FILE *stream = fopen("name.txt", "r");
char name[20+1];
size_t len = fread(&name, 1, 20, stream);
name[len] = '\0';
fclose(stream);
printf("Hello, %s!\n", name);
return 0;
}
इस उदाहरण में बहुत कुछ नहीं किया गया है, लेकिन इसमें पहले से ही कुछ ऐसा दिखाया गया है जो आपको किसी भी साइज़ के ऐप्लिकेशन में मिलेगा: यह बाहरी दुनिया से कुछ इनपुट पढ़ता है, उन्हें अंदरूनी तौर पर प्रोसेस करता है, और आउटपुट को फिर से बाहरी दुनिया में लिखता है. बाहरी दुनिया के साथ ऐसा इंटरैक्शन, कुछ फ़ंक्शन के ज़रिए होता है. इन्हें आम तौर पर इनपुट-आउटपुट फ़ंक्शन कहा जाता है. इन्हें I/O भी कहा जाता है.
C से नाम पढ़ने के लिए, आपको कम से कम दो ज़रूरी I/O कॉल की ज़रूरत होगी: fopen
, फ़ाइल खोलने के लिए, और
fread
उससे डेटा पढ़ने के लिए. डेटा वापस पाने के बाद, कंसोल पर नतीजा प्रिंट करने के लिए, किसी दूसरे I/O फ़ंक्शन printf
का इस्तेमाल किया जा सकता है.
ये फ़ंक्शन पहली नज़र में काफ़ी आसान लगते हैं. साथ ही, आपको डेटा को पढ़ने या लिखने के लिए, इस्तेमाल की जाने वाली मशीनरी के बारे में दोबारा सोचने की ज़रूरत नहीं पड़ती. हालांकि, एनवायरमेंट के हिसाब से, अंदर बहुत कुछ हो सकता है:
- अगर इनपुट फ़ाइल किसी लोकल ड्राइव में मौजूद है, तो ऐप्लिकेशन को फ़ाइल को ढूंढने, अनुमतियों की जांच करने, उसे पढ़ने के लिए खोलने, और फिर अनुरोध किए गए बाइट तक ब्लॉक के हिसाब से पढ़ने के लिए, मेमोरी और डिस्क को ऐक्सेस करने की एक सीरीज़ पूरी करनी होगी. आपकी डिस्क की स्पीड और अनुरोध किए गए साइज़ के हिसाब से, इसमें काफ़ी समय लग सकता है.
- इसके अलावा, हो सकता है कि इनपुट फ़ाइल, माउंट की गई नेटवर्क लोकेशन पर हो. ऐसे में, नेटवर्क स्टैक भी शामिल होगा. इससे हर ऑपरेशन के लिए, जटिलता, इंतज़ार का समय, और फिर से कोशिश करने की संख्या बढ़ जाएगी.
- आखिर में, यह भी ज़रूरी नहीं है कि
printf
, कॉन्सोले पर चीज़ें प्रिंट करे. हो सकता है कि इसे किसी फ़ाइल या नेटवर्क लोकेशन पर रीडायरेक्ट किया जाए. ऐसे में, आपको ऊपर बताए गए तरीके का ही इस्तेमाल करना होगा.
कम शब्दों में कहें, तो I/O प्रोसेस धीमी हो सकती है. साथ ही, कोड को एक नज़र में देखकर यह अनुमान नहीं लगाया जा सकता कि किसी कॉल को पूरा होने में कितना समय लगेगा. यह कार्रवाई चलने के दौरान, आपका पूरा ऐप्लिकेशन उपयोगकर्ता को फ़्रीज़ किया हुआ और काम न करने वाला दिखेगा.
यह C या C++ तक ही सीमित नहीं है. ज़्यादातर सिस्टम भाषाएं, सभी I/O को सिंक्रोनस एपीआई के तौर पर दिखाती हैं. उदाहरण के लिए, अगर उदाहरण को Rust में अनुवाद किया जाता है, तो एपीआई आसान दिख सकता है, लेकिन उस पर वही सिद्धांत लागू होते हैं. आपको सिर्फ़ एक कॉल करना होता है और नतीजा मिलने का इंतज़ार करना होता है. इस दौरान, यह सभी ज़रूरी काम करता है और एक ही बार में नतीजा दिखाता है:
fn main() {
let s = std::fs::read_to_string("name.txt");
println!("Hello, {}!", s);
}
हालांकि, इनमें से किसी भी सैंपल को WebAssembly में कंपाइल करने और उसे वेब पर ट्रांसलेट करने पर क्या होता है? उदाहरण के लिए, "फ़ाइल पढ़ना" ऑपरेशन का अनुवाद किसमें किया जा सकता है? इसके लिए, किसी स्टोरेज से डेटा पढ़ना होगा.
वेब का एसिंक्रोनस मॉडल
वेब पर स्टोरेज के कई विकल्प उपलब्ध हैं. इनमें इन-मेमोरी स्टोरेज (JS ऑब्जेक्ट), localStorage
, IndexedDB, सर्वर-साइड स्टोरेज, और नया फ़ाइल सिस्टम ऐक्सेस एपीआई शामिल हैं.
हालांकि, इनमें से सिर्फ़ दो एपीआई, एक साथ इस्तेमाल किए जा सकते हैं. इनमें से एक, इन-मेमोरी स्टोरेज और दूसरा localStorage
है. इन दोनों में, स्टोरेज को कितने समय तक और कितना सेव किया जा सकता है, इसकी सीमाएं तय होती हैं. बाकी सभी विकल्पों में सिर्फ़ असाइनोक्रोनस एपीआई उपलब्ध होते हैं.
यह वेब पर कोड को लागू करने की मुख्य प्रॉपर्टी है: इसमें कोई भी ऐसा ऑपरेशन शामिल है जिसमें समय लगता है और जो I/O के साथ काम करता है.
इसकी वजह यह है कि वेब हमेशा से सिंगल-थ्रेड वाला रहा है. साथ ही, यूज़र इंटरफ़ेस (यूआई) से जुड़े किसी भी उपयोगकर्ता कोड को यूआई के उसी थ्रेड पर चलाना होता है. सीपीयू टाइम के लिए, इसे लेआउट, रेंडरिंग, और इवेंट हैंडलिंग जैसे अन्य ज़रूरी टास्क के साथ मुकाबला करना पड़ता है. आपके पास यह तय करने का विकल्प नहीं होता कि JavaScript या WebAssembly का कोई हिस्सा, "फ़ाइल पढ़ने" की प्रोसेस शुरू करे और तब तक बाकी सभी चीज़ों को ब्लॉक कर दे. जैसे, पूरा टैब या पहले के वर्शन में, पूरा ब्राउज़र. यह प्रोसेस कुछ मिलीसेकंड से लेकर कुछ सेकंड तक चल सकती है.
इसके बजाय, कोड को सिर्फ़ I/O ऑपरेशन को शेड्यूल करने की अनुमति है. साथ ही, कोड के पूरा होने के बाद, कॉलबैक को लागू किया जाएगा. ऐसे कॉलबैक, ब्राउज़र के इवेंट लूप के हिस्से के तौर पर लागू होते हैं. हम यहां इस बारे में ज़्यादा जानकारी नहीं देंगे. हालांकि, अगर आपको यह जानना है कि इवेंट लूप कैसे काम करता है, तो टास्क, माइक्रोटास्क, सूचियां, और शेड्यूल लेख पढ़ें. इसमें इस विषय के बारे में पूरी जानकारी दी गई है.
कम शब्दों में कहें, तो ब्राउज़र कोड के सभी हिस्सों को एक अनलिमिटेड लूप में चलाता है. इसके लिए, वह उन्हें एक-एक करके सूची से हटाता है. जब कोई इवेंट ट्रिगर होता है, तो ब्राउज़र उससे जुड़े हैंडलर को सूची में जोड़ देता है. इसके बाद, अगले लूप में उसे सूची से हटाकर चलाया जाता है. इस तरीके से, एक ही थ्रेड का इस्तेमाल करके, एक साथ कई काम किए जा सकते हैं.
इस तरीके के बारे में यह बात याद रखना ज़रूरी है कि जब आपका कस्टम JavaScript (या WebAssembly) कोड चलता है, तब इवेंट लूप ब्लॉक हो जाता है. इस दौरान, किसी भी बाहरी हैंडलर, इवेंट, I/O वगैरह पर प्रतिक्रिया देने का कोई तरीका नहीं होता. I/O के नतीजे वापस पाने का एक ही तरीका है कि आप कॉलबैक रजिस्टर करें, अपना कोड चलाना खत्म करें, और ब्राउज़र को कंट्रोल वापस दें, ताकि वह किसी भी लंबित टास्क को प्रोसेस कर सके. I/O पूरा होने के बाद, आपका हैंडलर उन टास्क में से एक बन जाएगा और उसे लागू कर दिया जाएगा.
उदाहरण के लिए, अगर आपको ऊपर दिए गए सैंपल को आधुनिक JavaScript में फिर से लिखना है और किसी रिमोट यूआरएल से नाम पढ़ना है, तो आपको Fetch API और async-await सिंटैक्स का इस्तेमाल करना होगा:
async function main() {
let response = await fetch("name.txt");
let name = await response.text();
console.log("Hello, %s!", name);
}
भले ही, यह सिंक्रोनस दिखता है, लेकिन हर await
, कॉलबैक के लिए सिंटैक्स शुगर है:
function main() {
return fetch("name.txt")
.then(response => response.text())
.then(name => console.log("Hello, %s!", name));
}
इस उदाहरण में, अनुरोध शुरू करने और पहले कॉलबैक के साथ जवाबों की सदस्यता लेने की प्रोसेस को थोड़ा सा आसान तरीके से समझाया गया है. जब ब्राउज़र को शुरुआती रिस्पॉन्स मिलता है—सिर्फ़ एचटीटीपी हेडर—तो यह इस कॉलबैक को असिंक्रोनस तरीके से शुरू करता है. कॉलबैक, response.text()
का इस्तेमाल करके, बॉडी को टेक्स्ट के तौर पर पढ़ना शुरू करता है और नतीजे की सदस्यता लेने के लिए, किसी दूसरे कॉलबैक का इस्तेमाल करता है. आखिर में, fetch
के सभी कॉन्टेंट को हासिल करने के बाद, वह आखिरी कॉलबैक को शुरू करता है. इससे कंसोल पर "नमस्ते, (उपयोगकर्ता नाम)!" प्रिंट होता है.
इन चरणों के असाइन होने के बाद, मूल फ़ंक्शन, I/O शेड्यूल होने के तुरंत बाद ब्राउज़र को कंट्रोल वापस कर सकता है. साथ ही, I/O बैकग्राउंड में चलने के दौरान, पूरे यूज़र इंटरफ़ेस (यूआई) को अन्य टास्क के लिए उपलब्ध और रिस्पॉन्सिव रख सकता है. जैसे, रेंडरिंग, स्क्रोल करना वगैरह.
आखिरी उदाहरण के तौर पर, "sleep" जैसे आसान एपीआई भी I/O ऑपरेशन के तौर पर काम करते हैं. ये एपीआई, ऐप्लिकेशन को तय समय के लिए इंतज़ार कराते हैं:
#include <stdio.h>
#include <unistd.h>
// ...
printf("A\n");
sleep(1);
printf("B\n");
हां, इसे आसानी से अनुवाद किया जा सकता है. इससे, समय खत्म होने तक मौजूदा थ्रेड को ब्लॉक किया जा सकता है:
console.log("A");
for (let start = Date.now(); Date.now() - start < 1000;);
console.log("B");
असल में, Emscripten "sleep" को डिफ़ॉल्ट रूप से लागू करने के दौरान यही करता है. हालांकि, यह तरीका बहुत ही खराब है. इससे पूरा यूज़र इंटरफ़ेस (यूआई) ब्लॉक हो जाएगा और इस दौरान किसी भी दूसरे इवेंट को मैनेज नहीं किया जा सकेगा. आम तौर पर, प्रोडक्शन कोड में ऐसा न करें.
इसके बजाय, JavaScript में "sleep" के ज़्यादा सामान्य वर्शन में, setTimeout()
को कॉल करना और किसी हैंडलर के साथ सदस्यता लेना शामिल होगा:
console.log("A");
setTimeout(() => {
console.log("B");
}, 1000);
इन सभी उदाहरणों और एपीआई में क्या समानता है? हर मामले में, ओरिजनल सिस्टम लैंग्वेज में मौजूद आइडियोमैटिक कोड, I/O के लिए ब्लॉकिंग एपीआई का इस्तेमाल करता है. वहीं, वेब के लिए इसी तरह के उदाहरण में, ब्लॉकिंग एपीआई के बजाय असाइनोक्रोनस एपीआई का इस्तेमाल किया जाता है. वेब पर कॉम्पाइल करते समय, आपको उन दोनों प्रोग्राम को किसी तरह से ट्रांसफ़ॉर्म करना होगा. फ़िलहाल, WebAssembly में ऐसा करने की कोई सुविधा नहीं है.
Asyncify की मदद से, गैप को कम करना
ऐसे में, Asyncify की मदद ली जा सकती है. Asyncify, Emscripten की मदद से कॉम्पाइल करने के समय काम करने वाली सुविधा है. इसकी मदद से, पूरे प्रोग्राम को रोका जा सकता है और बाद में उसे असिंक्रोनस तरीके से फिर से शुरू किया जा सकता है.
Emscripten के साथ C / C++ में इस्तेमाल करना
अगर आपको आखिरी उदाहरण में, असिंक्रोनस स्लीप लागू करने के लिए Asyncify का इस्तेमाल करना है, तो इसे इस तरह से किया जा सकता है:
#include <stdio.h>
#include <emscripten.h>
EM_JS(void, async_sleep, (int seconds), {
Asyncify.handleSleep(wakeUp => {
setTimeout(wakeUp, seconds * 1000);
});
});
…
puts("A");
async_sleep(1);
puts("B");
EM_JS
एक मैक्रो है, जिसकी मदद से JavaScript स्निपेट को C फ़ंक्शन की तरह तय किया जा सकता है. इसके अंदर, Asyncify.handleSleep()
फ़ंक्शन का इस्तेमाल करें. यह फ़ंक्शन, Emscripten को प्रोग्राम को निलंबित करने के लिए कहता है और एक wakeUp()
हैंडलर उपलब्ध कराता है. इस हैंडलर को असिंक्रोनस ऑपरेशन पूरा होने के बाद कॉल किया जाना चाहिए. ऊपर दिए गए उदाहरण में, हैंडलर को setTimeout()
पर पास किया गया है. हालांकि, इसका इस्तेमाल किसी भी ऐसे कॉन्टेक्स्ट में किया जा सकता है जो कॉलबैक स्वीकार करता है. आखिर में, async_sleep()
को किसी भी जगह से कॉल किया जा सकता है, जैसे कि सामान्य sleep()
या किसी दूसरे सिंक्रोनस एपीआई को.
इस तरह के कोड को कंपाइल करते समय, आपको Emscripten को Asyncify सुविधा चालू करने के लिए कहना होगा. ऐसा करने के लिए, -s ASYNCIFY
के साथ-साथ -s ASYNCIFY_IMPORTS=[func1,
func2]
को फ़ंक्शन की ऐसी सूची के साथ पास करें जो ऐसिंक्रोनस हो सकती है.
emcc -O2 \
-s ASYNCIFY \
-s ASYNCIFY_IMPORTS=[async_sleep] \
...
इससे Emscripten को पता चलता है कि उन फ़ंक्शन को कॉल करने के लिए, स्थिति को सेव और वापस लाने की ज़रूरत पड़ सकती है. इसलिए, कंपाइलर ऐसे कॉल के आस-पास सहायक कोड इंजेक्ट करेगा.
अब, जब इस कोड को ब्राउज़र में चलाया जाएगा, तो आपको उम्मीद के मुताबिक आसानी से आउटपुट लॉग दिखेगा. इसमें A के बाद, B कुछ देर बाद दिखेगा.
A
B
Asyncify फ़ंक्शन से भी वैल्यू रिटर्न की जा सकती हैं. आपको handleSleep()
का नतीजा दिखाना होगा और नतीजे को wakeUp()
कॉलबैक में भेजना होगा. उदाहरण के लिए, अगर आपको किसी फ़ाइल से पढ़ने के बजाय, किसी रिमोट रिसॉर्स से कोई नंबर फ़ेच करना है, तो अनुरोध करने के लिए नीचे दिए गए स्निपेट का इस्तेमाल किया जा सकता है. इसके बाद, C कोड को निलंबित करें और रिस्पॉन्स बॉडी मिलने के बाद उसे फिर से शुरू करें. यह सब आसानी से किया जा सकता है, जैसे कि कॉल सिंक्रोनस हो.
EM_JS(int, get_answer, (), {
return Asyncify.handleSleep(wakeUp => {
fetch("answer.txt")
.then(response => response.text())
.then(text => wakeUp(Number(text)));
});
});
puts("Getting answer...");
int answer = get_answer();
printf("Answer is %d\n", answer);
असल में, fetch()
जैसे प्रॉमिस-आधारित एपीआई के लिए, कॉलबैक-आधारित एपीआई का इस्तेमाल करने के बजाय, JavaScript की async-await सुविधा के साथ Asyncify को भी जोड़ा जा सकता है. इसके लिए, Asyncify.handleSleep()
के बजाय Asyncify.handleAsync()
को कॉल करें. इसके बाद, wakeUp()
कॉलबैक को शेड्यूल करने के बजाय, async
JavaScript फ़ंक्शन को पास किया जा सकता है. साथ ही, await
और return
का इस्तेमाल किया जा सकता है. इससे कोड और भी आसान और सिंक्रोनस दिखता है. साथ ही, एसिंक्रोनस I/O के किसी भी फ़ायदे को खोने की ज़रूरत नहीं पड़ती.
EM_JS(int, get_answer, (), {
return Asyncify.handleAsync(async () => {
let response = await fetch("answer.txt");
let text = await response.text();
return Number(text);
});
});
int answer = get_answer();
कॉम्प्लेक्स वैल्यू का इंतज़ार किया जा रहा है
हालांकि, इस उदाहरण में भी सिर्फ़ संख्याओं का इस्तेमाल किया गया है. अगर आपको मूल उदाहरण लागू करना है, तो क्या होगा, जिसमें मैंने किसी फ़ाइल से उपयोगकर्ता का नाम स्ट्रिंग के तौर पर पाने की कोशिश की थी? आपके पास ऐसा करने का विकल्प भी है!
Emscripten में Embind नाम की एक सुविधा होती है. इसकी मदद से, JavaScript और C++ वैल्यू के बीच कन्वर्ज़न मैनेज किए जा सकते हैं. इसमें Asyncify की सुविधा भी है, ताकि आप बाहरी Promise
पर await()
को कॉल कर सकें. यह async-await JavaScript कोड में await
की तरह ही काम करेगा:
val fetch = val::global("fetch");
val response = fetch(std::string("answer.txt")).await();
val text = response.call<val>("text").await();
auto answer = text.as<std::string>();
इस तरीके का इस्तेमाल करते समय, आपको ASYNCIFY_IMPORTS
को कंपाइल फ़्लैग के तौर पर पास करने की ज़रूरत नहीं है, क्योंकि यह डिफ़ॉल्ट रूप से पहले से शामिल होता है.
ठीक है, तो यह सब Emscripten में बेहतर तरीके से काम करता है. अन्य टूलचेन और भाषाओं के बारे में क्या?
अन्य भाषाओं में इस्तेमाल
मान लें कि आपके Rust कोड में कहीं एक ऐसा सिंक्रोनस कॉल है जिसे आपको वेब पर मौजूद किसी एसिंक्रोनस एपीआई से मैप करना है. ऐसा किया जा सकता है!
सबसे पहले, आपको extern
ब्लॉक (या बाहरी फ़ंक्शन के लिए अपनी चुनी गई भाषा के सिंटैक्स) की मदद से, ऐसे फ़ंक्शन को रेगुलर इंपोर्ट के तौर पर तय करना होगा.
extern {
fn get_answer() -> i32;
}
println!("Getting answer...");
let answer = get_answer();
println!("Answer is {}", answer);
और अपने कोड को WebAssembly में कंपाइल करें:
cargo build --target wasm32-unknown-unknown
अब आपको स्टैक को सेव/बहाल करने के लिए, कोड के साथ WebAssembly फ़ाइल को इंस्ट्रूमेंट करना होगा. C / C++ के लिए, Emscripten यह काम हमारे लिए करता है. हालांकि, यहां इसका इस्तेमाल नहीं किया जाता है. इसलिए, प्रोसेस थोड़ी ज़्यादा मैन्युअल होती है.
अच्छी बात यह है कि Asyncify ट्रांसफ़ॉर्म, टूलचेन पर निर्भर नहीं करता. यह किसी भी तरह की WebAssembly फ़ाइलों को बदल सकता है. भले ही, उन्हें किसी भी कंपाइलर से बनाया गया हो. ट्रांसफ़ॉर्म को Binaryen टूलचैन के wasm-opt
ऑप्टिमाइज़र के हिस्से के तौर पर अलग से उपलब्ध कराया जाता है. इसे इस तरह से शुरू किया जा सकता है:
wasm-opt -O2 --asyncify \
--pass-arg=asyncify-imports@env.get_answer \
[...]
ट्रांसफ़ॉर्म को चालू करने के लिए --asyncify
पास करें. इसके बाद, कॉमा से अलग किए गए ऐसे फ़ंक्शन की सूची देने के लिए --pass-arg=…
का इस्तेमाल करें जिनके लिए प्रोग्राम की स्थिति को निलंबित और फिर से शुरू किया जाना चाहिए.
अब बस रनटाइम कोड देना बाकी है, जो असल में ऐसा करेगा—WebAssembly कोड को निलंबित और फिर से शुरू करेगा. फिर से, C / C++ के मामले में, Emscripten इसे शामिल करेगा, लेकिन अब आपको पसंद के मुताबिक JavaScript glue कोड की ज़रूरत होगी, जो किसी भी WebAssembly फ़ाइल को मैनेज करेगा. हमने इसके लिए एक लाइब्रेरी बनाई है.
इसे GitHub पर https://github.com/GoogleChromeLabs/asyncify पर या npm पर asyncify-wasm
नाम से ढूंढा जा सकता है.
यह स्टैंडर्ड WebAssembly इंस्टैंशिएशन एपीआई को सिम्युलेट करता है, लेकिन अपने नेमस्पेस में. इन दोनों के बीच का एकमात्र अंतर यह है कि सामान्य WebAssembly API में, सिर्फ़ सिंक्रोनस फ़ंक्शन को इंपोर्ट के तौर पर दिया जा सकता है. वहीं, Asyncify रैपर में, असिंक्रोनस इंपोर्ट भी दिए जा सकते हैं:
const { instance } = await Asyncify.instantiateStreaming(fetch('app.wasm'), {
env: {
async get_answer() {
let response = await fetch("answer.txt");
let text = await response.text();
return Number(text);
}
}
});
…
await instance.exports.main();
जब WebAssembly साइड से, ऊपर दिए गए उदाहरण में get_answer()
जैसे किसी ऐसे फ़ंक्शन को कॉल करने की कोशिश की जाती है जो सिंक नहीं होता, तो लाइब्रेरी, रिटर्न किए गए Promise
का पता लगाएगी. साथ ही, WebAssembly ऐप्लिकेशन की स्थिति को निलंबित करके सेव करेगी और प्रॉमिस के पूरा होने की सदस्यता लेगी. इसके बाद, जब प्रॉमिस पूरा हो जाएगा, तो कॉल स्टैक और स्थिति को आसानी से वापस लाया जाएगा और प्रोसेस को ऐसे जारी रखा जाएगा जैसे कुछ हुआ ही न हो.
मॉड्यूल में मौजूद कोई भी फ़ंक्शन, असाइनॉन्स कॉल कर सकता है. इसलिए, सभी एक्सपोर्ट भी असाइनॉन्स हो सकते हैं. इसलिए, उन्हें भी रैप कर दिया जाता है. ऊपर दिए गए उदाहरण में, आपने देखा होगा कि instance.exports.main()
के नतीजे को await
करने पर, आपको पता चलता है कि फ़ंक्शन कब पूरा हुआ.
यह सुविधा कैसे काम करती है?
जब Asyncify को ASYNCIFY_IMPORTS
फ़ंक्शन में से किसी एक को कॉल करने का पता चलता है, तो यह एक असाइनिश्नल ऑपरेशन शुरू करता है. साथ ही, कॉल स्टैक और किसी भी अस्थायी लोकल के साथ-साथ ऐप्लिकेशन की पूरी स्थिति को सेव करता है. इसके बाद, जब वह ऑपरेशन पूरा हो जाता है, तो सभी मेमोरी और कॉल स्टैक को वापस लाता है और उसी जगह से फिर से शुरू करता है और उसी स्थिति में, जैसे कि प्रोग्राम कभी रुका ही नहीं था.
यह JavaScript की async-await सुविधा से काफ़ी मिलती-जुलती है, जिसे मैंने पहले दिखाया था. हालांकि, JavaScript की सुविधा के मुकाबले, इसके लिए भाषा के किसी खास सिंटैक्स या रनटाइम की ज़रूरत नहीं होती. इसके बजाय, यह कॉम्पाइल के समय, सामान्य सिंक्रोनस फ़ंक्शन को बदलकर काम करती है.
पहले दिखाए गए असाइनमेंट के साथ, असाइनमेंट के पूरा होने का इंतज़ार किए बिना, असाइनमेंट को कंपाइल करने का उदाहरण:
puts("A");
async_sleep(1);
puts("B");
Asyncify इस कोड को लेता है और इसे कुछ इस तरह बदल देता है (यह एक आभासी कोड है, असल में बदलाव करने की प्रोसेस इससे ज़्यादा जटिल होती है):
if (mode == NORMAL_EXECUTION) {
puts("A");
async_sleep(1);
saveLocals();
mode = UNWINDING;
return;
}
if (mode == REWINDING) {
restoreLocals();
mode = NORMAL_EXECUTION;
}
puts("B");
शुरुआत में mode
, NORMAL_EXECUTION
पर सेट होता है. इसी तरह, पहली बार जब इस तरह के बदले गए कोड को चलाया जाएगा, तो सिर्फ़ async_sleep()
तक के हिस्से का आकलन किया जाएगा. असाइनिश्नल के साथ काम करने वाली प्रोसेस शेड्यूल होने के बाद, Asyncify सभी लोकल वैरिएबल सेव कर लेता है. साथ ही, हर फ़ंक्शन से सबसे ऊपर तक वापस आकर स्टैक को अनवाइंड कर देता है. इस तरह, ब्राउज़र इवेंट लूप को फिर से कंट्रोल मिल जाता है.
इसके बाद, async_sleep()
ठीक होने पर, Asyncify सहायता कोड mode
को REWINDING
में बदल देगा और फ़ंक्शन को फिर से कॉल करेगा. इस बार, "सामान्य तरीके से लागू करना" शाखा को छोड़ दिया गया है - क्योंकि यह पिछली बार पहले ही काम कर चुकी है और मुझे "A" को दो बार प्रिंट करने से बचना है - और इसके बजाय, यह सीधे "रीवाइंड करना" शाखा पर आती है. इस बिंदु पर पहुंचने के बाद, यह सेव किए गए सभी लोकल वैरिएबल को वापस लाता है. साथ ही, मोड को फिर से "सामान्य" पर सेट करता है और कोड को ऐसे चलाता है जैसे कि उसे कभी रोका ही न गया हो.
ट्रांसफ़ॉर्मेशन की लागत
माफ़ करें, Asyncify ट्रांसफ़ॉर्म पूरी तरह से बिना किसी शुल्क के उपलब्ध नहीं है. ऐसा इसलिए है, क्योंकि इसे उन सभी लोकल वैरिएबल को सेव और वापस लाने के लिए, सहायक कोड का इस्तेमाल करना पड़ता है. साथ ही, अलग-अलग मोड में कॉल स्टैक पर नेविगेट करना पड़ता है वगैरह. यह सिर्फ़ उन फ़ंक्शन में बदलाव करने की कोशिश करता है जिन्हें कमांडलाइन पर असाइनॉन्स के तौर पर मार्क किया गया है. साथ ही, इन फ़ंक्शन को कॉल करने वाले किसी भी फ़ंक्शन में बदलाव करने की कोशिश करता है. हालांकि, कंप्रेस करने से पहले, कोड साइज़ में करीब 50% की बढ़ोतरी हो सकती है.
यह सही नहीं है, लेकिन कई मामलों में इसे स्वीकार किया जा सकता है. ऐसा तब होता है, जब विकल्प में पूरी तरह से फ़ंक्शन काम न कर रहा हो या ओरिजनल कोड में काफ़ी बदलाव करने पड़ रहे हों.
यह पक्का करें कि फ़ाइनल बिल्ड के लिए ऑप्टिमाइज़ेशन हमेशा चालू हो, ताकि यह और ज़्यादा न बढ़े. Asyncify के लिए ऑप्टिमाइज़ेशन के खास विकल्प भी देखे जा सकते हैं. इससे, सिर्फ़ चुनिंदा फ़ंक्शन और/या सिर्फ़ डायरेक्ट फ़ंक्शन कॉल पर ट्रांसफ़ॉर्मेशन को सीमित करके, ओवरहेड को कम किया जा सकता है. रनटाइम परफ़ॉर्मेंस पर भी थोड़ा असर पड़ता है. हालांकि, यह असर सिर्फ़ असाइन किए गए कॉल पर पड़ता है. हालांकि, आम तौर पर यह शुल्क, असल काम की लागत के मुकाबले बहुत कम होता है.
असल दुनिया के डेमो
अब आपने आसान उदाहरण देख लिए हैं. अब हम ज़्यादा मुश्किल स्थितियों के बारे में जानेंगे.
जैसा कि लेख की शुरुआत में बताया गया है, वेब पर स्टोरेज का एक विकल्प, एसिंक्रोनस File System Access API है. यह वेब ऐप्लिकेशन से, असली होस्ट फ़ाइल सिस्टम का ऐक्सेस देता है.
दूसरी ओर, कंसोल और सर्वर-साइड में WebAssembly I/O के लिए, WASI नाम का एक डिफ़ैक्ट स्टैंडर्ड है. इसे सिस्टम भाषाओं के लिए कंपाइलेशन टारगेट के तौर पर डिज़ाइन किया गया था. साथ ही, यह सभी तरह के फ़ाइल सिस्टम और अन्य ऑपरेशन को पारंपरिक सिंक्रोनस फ़ॉर्म में दिखाता है.
अगर एक को दूसरे से मैप किया जा सके, तो क्या होगा? इसके बाद, WASI टारगेट के साथ काम करने वाली किसी भी टूलचैन की मदद से, किसी भी सोर्स भाषा में किसी भी ऐप्लिकेशन को कॉम्पाइल किया जा सकता है. साथ ही, उसे वेब पर सैंडबॉक्स में चलाया जा सकता है. इस दौरान, उसे असल उपयोगकर्ता फ़ाइलों पर काम करने की अनुमति भी दी जा सकती है! Asyncify की मदद से, ऐसा किया जा सकता है.
इस डेमो में, मैंने WASI में कुछ छोटे पैच के साथ Rust coreutils क्रेट को Asyncify ट्रांसफ़ॉर्म के ज़रिए पास किया है. साथ ही, JavaScript साइड पर WASI से File System Access API में एसिंक्रोनस बाइंडिंग लागू की हैं. Xterm.js टर्मिनल कॉम्पोनेंट के साथ जोड़ने पर, यह ब्राउज़र टैब में एक असली शेल उपलब्ध कराता है. यह असल टर्मिनल की तरह ही, उपयोगकर्ता की असल फ़ाइलों पर काम करता है.
इसे https://wasi.rreverser.com/ पर लाइव देखें.
असाइनमेंट को अलग-अलग थ्रेड पर चलाने के उदाहरण सिर्फ़ टाइमर और फ़ाइल सिस्टम तक ही सीमित नहीं हैं. इसके अलावा, वेब पर ज़्यादा खास एपीआई का इस्तेमाल किया जा सकता है.
उदाहरण के लिए, Asyncify की मदद से, libusb को WebUSB API पर मैप किया जा सकता है. ऐसा करने पर, वेब पर ऐसे डिवाइसों को असाइनोक्रोनस ऐक्सेस मिलता है. यूएसबी डिवाइसों के साथ काम करने के लिए, libusb शायद सबसे लोकप्रिय नेटिव लाइब्रेरी है. मैप और कंपाइल करने के बाद, मुझे वेब पेज के सैंडबॉक्स में चुने गए डिवाइसों के लिए, स्टैंडर्ड libusb टेस्ट और उदाहरण मिले.
हालांकि, यह शायद किसी दूसरी ब्लॉग पोस्ट के लिए बनाई गई स्टोरी हो.
इन उदाहरणों से पता चलता है कि Asyncify, सभी तरह के ऐप्लिकेशन को वेब पर पोर्ट करने और गैप को कम करने के लिए कितना असरदार हो सकता है. इससे आपको क्रॉस-प्लैटफ़ॉर्म ऐक्सेस, सैंडबॉक्सिंग, और बेहतर सुरक्षा मिलती है. साथ ही, ऐप्लिकेशन की सुविधाओं में कोई बदलाव नहीं होता.