WebAssembly से एसिंक्रोनस वेब एपीआई इस्तेमाल करना

वेब पर मौजूद I/O एपीआई एसिंक्रोनस होते हैं. हालांकि, वे सिस्टम की ज़्यादातर भाषाओं में सिंक्रोनस होते हैं. कोड को 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 में "स्लीप" के एक ज़्यादा मुहावरे वाले वर्शन में setTimeout() को कॉल करना और किसी हैंडलर की सदस्यता लेना शामिल होगा:

console.log("A");
setTimeout(() => {
    console.log("B");
}, 1000);

इन सभी उदाहरणों और एपीआई में क्या समानता है? हर मामले में, ओरिजनल सिस्टम भाषा में मौजूद आइडियोमैटिक कोड, I/O के लिए ब्लॉकिंग एपीआई का इस्तेमाल करता है. वहीं, वेब के लिए इसी तरह के उदाहरण में, ब्लॉकिंग एपीआई के बजाय असाइनोक्रोनस एपीआई का इस्तेमाल किया जाता है. वेब पर कॉम्पाइल करते समय, आपको उन दोनों प्रोग्राम को किसी तरह से एक से दूसरे में बदलना होगा. फ़िलहाल, WebAssembly में ऐसा करने की कोई सुविधा नहीं है.

Asyncify के साथ अंतर को कम करें

ऐसी ही जगह पर Asyncify काम आता है. Asyncify, Emscripten की मदद से कॉम्पाइल करने के समय काम करने वाली सुविधा है. इसकी मदद से, पूरे प्रोग्राम को रोका जा सकता है और बाद में उसे असिंक्रोनस तरीके से फिर से शुरू किया जा सकता है.

JavaScript -> WebAssembly -> वेब एपीआई -> किसी दूसरे टास्क के साथ काम न करने वाले टास्क को शुरू करने के बारे में बताने वाला कॉल ग्राफ़, जहां Asyncify किसी दूसरे टास्क के साथ काम न करने वाले टास्क के नतीजे को वापस WebAssembly से कनेक्ट करता है

Emscripten के साथ C / C++ में इस्तेमाल करना

अगर आपको पिछले उदाहरण में एसिंक्रोनस स्लीप को लागू करने के लिए एसिंक्रोनस स्लीप का इस्तेमाल करना है, तो यह ऐसा किया जा सकता है:

#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 को एसिंक्रोनसी सुविधा चालू करने के लिए कहना होगा. ऐसा करने के लिए, -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");

एसिंक्रोनस कोड इस कोड को लेकर इसे करीब-करीब यहां दिए गए कोड की तरह बदल देता है (इसके मुकाबले, स्यूडो-कोड, असल ट्रांसफ़ॉर्मेशन की प्रोसेस ज़्यादा होती है):

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() तक ले जाने वाले हिस्से का आकलन किया जाएगा. एसिंक्रोनस कार्रवाई शेड्यूल होते ही, एसिंक्रोनस कार्रवाई सभी लोकल को सेव कर लेती है और हर फ़ंक्शन से ऊपर की ओर वापस आकर स्टैक को अनविल्ड करती है. इस तरह से यह कंट्रोल, ब्राउज़र इवेंट लूप पर वापस आ जाता है.

इसके बाद, async_sleep() ठीक होने पर, Asyncify सहायता कोड mode को REWINDING में बदल देगा और फ़ंक्शन को फिर से कॉल करेगा. इस बार, "सामान्य एक्ज़ीक्यूशन" ब्रांच को छोड़ दिया गया है - क्योंकि यह पिछली बार पहले ही काम कर चुका था और मैं "A" को दो बार प्रिंट करने से बचना चाहता/चाहती हूं - और इसके बजाय यह सीधे "रिवाइंडिंग" ब्रांच पर आता है. इस बिंदु पर पहुंचने के बाद, यह सेव किए गए सभी लोकल वैरिएबल को वापस लाता है. साथ ही, मोड को फिर से "सामान्य" पर सेट करता है और कोड को ऐसे चलाता है जैसे कि उसे कभी रोका ही न गया हो.

ट्रांसफ़ॉर्मेशन की लागत

माफ़ करें, Asyncify ट्रांसफ़ॉर्म पूरी तरह से बिना किसी शुल्क के उपलब्ध नहीं है. ऐसा इसलिए है, क्योंकि इसे उन सभी लोकल वैरिएबल को सेव और वापस लाने के लिए, सहायक कोड का इस्तेमाल करना पड़ता है. साथ ही, अलग-अलग मोड में कॉल स्टैक पर नेविगेट करना पड़ता है वगैरह. यह सिर्फ़ कमांड लाइन पर एसिंक्रोनस के तौर पर मार्क किए गए फ़ंक्शन और उनके संभावित कॉलर में बदलाव करने की कोशिश करता है. हालांकि, कंप्रेस करने से पहले कोड साइज़ ओवरहेड करीब 50% तक बढ़ सकता है.

अलग-अलग मानदंडों के लिए, कोड के साइज़ का ओवरहेड दिखाने वाला ग्राफ़. इसमें, बेहतर स्थिति में करीब-करीब 0% से लेकर सबसे खराब मामलों में

100% से ज़्यादा तक के आंकड़े दिखाए गए हैं

यह तरीका बिलकुल सही नहीं है. हालांकि, कई मामलों में इसे तब स्वीकार किया जा सकता है, जब विकल्प के तौर पर सभी सुविधाएं उपलब्ध न हों या ओरिजनल कोड में ज़रूरी बदलाव करने की ज़रूरत न हो.

यह पक्का करें कि फ़ाइनल बिल्ड के लिए ऑप्टिमाइज़ेशन हमेशा चालू हो, ताकि यह और ज़्यादा न बढ़े. 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, यूएसबी डिवाइसों के साथ काम करने के लिए सबसे लोकप्रिय नेटिव लाइब्रेरी है. WebUSB API, वेब पर ऐसे डिवाइसों का ऐक्सेस असाइनॉन्स के हिसाब से देता है. मैप करने और कंपाइल करने के बाद, मुझे चुने गए डिवाइसों के लिए वेब पेज के सैंडबॉक्स में स्टैंडर्ड लिबब टेस्ट और उदाहरण मिले.

वेब पेज पर libusb के डीबग आउटपुट का स्क्रीनशॉट, जिसमें कनेक्ट किए गए Canon कैमरे की जानकारी दिख रही है

हालांकि, यह शायद किसी दूसरी ब्लॉग पोस्ट के लिए एक कहानी हो.

इन उदाहरणों से पता चलता है कि एसिंक्रोनस तरीके से काम करने की सुविधा कितनी कारगर है. यह वेब पर सभी तरह के ऐप्लिकेशन की पोर्टिंग और अंतर को कम करने के साथ-साथ, क्रॉस-प्लैटफ़ॉर्म ऐक्सेस, सैंडबॉक्सिंग, और बेहतर सुरक्षा की सुविधा भी देती है. इन सभी सुविधाओं को इस्तेमाल करने से, वेब ब्राउज़र पर काम करना आसान हो जाता है.