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

वेब पर 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 को सिंक्रोनस एपीआई के तौर पर पेश करते हैं. उदाहरण के लिए, अगर आप रस्ट के उदाहरण का अनुवाद करते हैं, तो एपीआई ज़्यादा आसान लग सकता है, लेकिन वे सिद्धांत लागू होते हैं. आप बस कॉल करते हैं और नतीजे के लिए सिंक होने का इंतज़ार करते हैं. इस दौरान, यह सभी महंगे काम करता है और नतीजा एक बार में एक ही कॉल में दिखाता है:

fn main() {
    let s = std::fs::read_to_string("name.txt");
    println!("Hello, {}!", s);
}

हालांकि, जब उनमें से किसी भी सैंपल को WebAssembly में कंपाइल करने और उनका अनुवाद वेब पर करने की कोशिश की जाती है, तो क्या होता है? इसके अलावा, कोई खास उदाहरण देने के लिए, "फ़ाइल पढ़ी जा सकती है" कार्रवाई का क्या अनुवाद हो सकता है? इसे कुछ स्टोरेज से डेटा पढ़ने की ज़रूरत होगी.

वेब का एसिंक्रोनस मॉडल

वेब में स्टोरेज के ऐसे कई विकल्प होते हैं जिन्हें मैप किया जा सकता है, जैसे कि इन-मेमोरी स्टोरेज (JS ऑब्जेक्ट), localStorage, IndexedDB, सर्वर-साइड स्टोरेज, और नया File System Access API.

हालांकि, इनमें से सिर्फ़ दो एपीआई—इन-मेमोरी स्टोरेज और localStorage—का इस्तेमाल सिंक्रोनस तरीके से किया जा सकता है. ये दोनों ही, आपके पास कितने समय तक और क्या स्टोर कर सकते हैं, इसे सीमित करने वाले विकल्प हैं. अन्य सभी विकल्प सिर्फ़ एसिंक्रोनस एपीआई उपलब्ध कराते हैं.

यह वेब पर कोड को एक्ज़ीक्यूट करने की मुख्य प्रॉपर्टी में से एक है: समय लेने वाली कोई भी कार्रवाई, जिसमें कोई भी I/O शामिल है, एसिंक्रोनस होना चाहिए.

इसकी वजह यह है कि वेब, अब तक सिंगल-थ्रेड है और यूज़र इंटरफ़ेस (यूआई) को छूने वाला कोई भी उपयोगकर्ता कोड, यूज़र इंटरफ़ेस (यूआई) की तरह ही थ्रेड पर चलाना है. इसे सीपीयू समय के लिए लेआउट, रेंडरिंग, और इवेंट मैनेज करने जैसे दूसरे ज़रूरी कामों से मुकाबला करना होता है. आप नहीं चाहेंगे कि JavaScript या WebAssembly का कोई हिस्सा "फ़ाइल पढ़ने" की कार्रवाई शुरू कर सके और बाकी सब कुछ—पूरा टैब या पहले पूरा ब्राउज़र—मिलीसेकंड से लेकर कुछ सेकंड तक की अवधि खत्म होने तक बाकी सभी चीज़ों को ब्लॉक कर सके.

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

इसका छोटा वर्शन है कि ब्राउज़र सभी कोड के हिस्सों को सूची से एक-एक करके इनफ़ाइनाइट लूप की तरह चलाता है. जब कोई इवेंट ट्रिगर होता है, तो ब्राउज़र उससे जुड़े हैंडलर को लाइन में जोड़ देता है. साथ ही, अगले लूप इटरेशन पर, उसे सूची से हटाकर एक्ज़ीक्यूट कर दिया जाता है. इस तरीके की मदद से, सिर्फ़ एक थ्रेड का इस्तेमाल करते हुए, एक साथ कई काम किए जा सकते हैं.

इस तरीके के बारे में आपको याद रखना ज़रूरी है कि आपका कस्टम JavaScript (या WebAssembly) कोड काम करते समय, इवेंट लूप ब्लॉक रहता है. हालांकि, किसी बाहरी हैंडलर, इवेंट, 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 के पूरा होने के दौरान, पूरे यूज़र इंटरफ़ेस (यूआई) को रिस्पॉन्सिव और अन्य कामों के लिए उपलब्ध छोड़ देता है. इनमें रेंडरिंग, स्क्रोलिंग वगैरह शामिल हैं.

आखिरी उदाहरण के तौर पर, "नींद" जैसे सामान्य एपीआई भी 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 "नींद" को लागू करने के डिफ़ॉल्ट तरीके में ऐसा ही करता है. हालांकि, यह बहुत कारगर नहीं है. इससे पूरा यूज़र इंटरफ़ेस (यूआई) ब्लॉक हो जाएगा और इस दौरान कोई दूसरा इवेंट हैंडल नहीं किया जा सकेगा. आम तौर पर, प्रोडक्शन कोड में ऐसा न करें.

इसके बजाय, JavaScript में "sleep" के एक इडियोमैटिक वर्शन में, setTimeout() को कॉल करना और हैंडलर की मदद से सदस्यता लेना शामिल होगा:

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

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

Asyncify के साथ इस अंतर को दूर करना

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

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

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() को पास किया गया है. हालांकि, इसे ऐसे किसी भी दूसरे कॉन्टेक्स्ट में इस्तेमाल किया जा सकता है जो कॉलबैक स्वीकार करता हो. आखिर में, सामान्य sleep() या किसी अन्य सिंक्रोनस एपीआई की तरह, async_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() कॉलबैक को भेजना होगा. मिसाल

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() जैसे प्रॉमिस-आधारित एपीआई के लिए, कॉलबैक-आधारित एपीआई का इस्तेमाल करने के बजाय, Asyncify को JavaScript की async-await सुविधा के साथ मिलाया जा सकता है. इसके लिए, 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() को कॉल कर सकते हैं. यह 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 फ़ाइलों को बदल सकता है. इससे कोई फ़र्क़ नहीं पड़ता कि उसे किस कंपाइलर ने बनाया है. ट्रांसफ़ॉर्म को अलग से दिया जाता है, जो बाइनरियन टूलचेन से wasm-opt ऑप्टिमाइज़ेशनर के हिस्से के तौर पर दिया जाता है. इसे इस तरह से शुरू किया जा सकता है:

wasm-opt -O2 --asyncify \
      --pass-arg=asyncify-imports@env.get_answer \
      [...]

ट्रांसफ़ॉर्मेशन की सुविधा को चालू करने के लिए, --asyncify को पास करें. इसके बाद, एसिंक्रोनस फ़ंक्शन की कॉमा-सेपरेटेड लिस्ट देने के लिए, --pass-arg=… का इस्तेमाल करें. ऐसा करने पर, प्रोग्राम की स्थिति को निलंबित करके बाद में फिर से शुरू किया जा सकता है.

अब सिर्फ़ काम करने वाला रनटाइम कोड देना है, जो असल में काम करेगा. WebAssembly कोड को निलंबित करके फिर से शुरू करें. फिर से, C / C++ केस में इसे Emscripten में शामिल किया जाएगा, लेकिन अब आपको कस्टम JavaScript ग्लू कोड की ज़रूरत होगी जो आर्बिट्रेरी WebAssembly फ़ाइलों को संभाल सके. इसके लिए हमने एक लाइब्रेरी बनाई है.

आपको GitHub पर यह https://github.com/GoogleChromeLabs/asyncify या npm asyncify-wasm नाम के नीचे मिलेगा.

यह अपने नेमस्पेस के अंदर, स्टैंडर्ड WebAssembly Instantiation API को सिम्युलेट करता है. फ़र्क़ सिर्फ़ यह है कि सामान्य 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();

ऊपर दिए गए उदाहरण में get_answer() जैसे किसी एसिंक्रोनस फ़ंक्शन को कॉल करने की कोशिश करने पर लाइब्रेरी, वापस मिले Promise का पता लगाएगी, WebAssembly ऐप्लिकेशन को निलंबित और सेव करेगी, प्रॉमिस ऐक्शन को सब्सक्राइब करेगी, और बाद में इसका समाधान हो जाने पर, कॉल स्टैक और स्थिति को आसानी से पहले जैसा करेगी और काम करने की प्रोसेस ऐसे रहेगी जैसे कुछ भी न हुआ हो.

मॉड्यूल में कोई भी फ़ंक्शन एसिंक्रोनस कॉल कर सकता है, इसलिए सभी एक्सपोर्ट संभावित रूप से एसिंक्रोनस भी हो जाते हैं, इसलिए वे भी रैप हो जाते हैं. आपने ऊपर दिए गए उदाहरण में देखा होगा कि निष्पादन असल में पूरा होने पर, आपको instance.exports.main() के नतीजे को await की ज़रूरत होती है.

यह सब हुड में कैसे काम करता है?

जब Asyncify, ASYNCIFY_IMPORTS फ़ंक्शन में से किसी एक फ़ंक्शन को कॉल का पता लगाता है, तो यह एसिंक्रोनस ऑपरेशन शुरू करता है और ऐप्लिकेशन की पूरी स्थिति को सेव करता है. इसमें कॉल स्टैक और कुछ अस्थायी डिवाइस शामिल हैं. बाद में, जब यह कार्रवाई पूरी हो जाती है, तो सभी मेमोरी और कॉल स्टैक को पहले जैसा करता है. साथ ही, उसी जगह से फिर से शुरू होता है जहां प्रोग्राम कभी बंद नहीं हुआ.

यह JavaScript की पहले दिखाई गई 'एक साथ काम नहीं करने वाली सुविधा' सुविधा से काफ़ी मिलता-जुलता है. हालांकि, JavaScript की तरह, इसके लिए किसी खास सिंटैक्स या रनटाइम के लिए सहायता की ज़रूरत नहीं होती. इसके बजाय, यह कंपाइलेशन समय पर सादे सिंक्रोनस फ़ंक्शन को पूरी तरह से बदलकर काम करती है.

पहले दिखाए गए एसिंक्रोनस नींद के उदाहरण को कंपाइल करते समय:

puts("A");
async_sleep(1);
puts("B");

Asyncify इस कोड को लेता है और उसे करीब-करीब नीचे दिए गए कोड की तरह बदल देता है (pseudo-code, असली बदलाव इसमें ज़्यादा शामिल है):

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 पर आधारित ऑप्टिमाइज़ेशन के विकल्प भी देखे जा सकते हैं. इसके लिए, कन्वर्ज़न को सिर्फ़ तय फ़ंक्शन और/या सिर्फ़ डायरेक्ट फ़ंक्शन कॉल के लिए सीमित करें. रनटाइम की परफ़ॉर्मेंस पर थोड़ा-बहुत खर्च होता है, लेकिन यह एसिंक्रोनस कॉल तक ही सीमित होता है. हालांकि, असली काम की लागत की तुलना में, आम तौर पर यह न के बराबर होता है.

असल दुनिया के डेमो

अब आपने आसान उदाहरण देख लिए हैं, अब मैं ज़्यादा जटिल परिस्थितियों के बारे में जानेंगे.

जैसा कि लेख की शुरुआत में बताया गया था, वेब पर स्टोरेज का एक विकल्प एक एसिंक्रोनस फ़ाइल सिस्टम ऐक्सेस एपीआई है. यह किसी वेब ऐप्लिकेशन से, रीयल होस्ट फ़ाइल सिस्टम को ऐक्सेस करने की सुविधा देता है.

दूसरी ओर, कंसोल और सर्वर साइड में WebAssembly I/O के लिए, WASI नाम का एक असली स्टैंडर्ड मौजूद है. इसे सिस्टम की भाषाओं के लिए एक कंपाइलेशन टारगेट के तौर पर डिज़ाइन किया गया था. यह सभी तरह के फ़ाइल सिस्टम और दूसरे ऑपरेशन को पारंपरिक सिंक्रोनस के रूप में दिखाता है.

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

इस डेमो में, मैंने कुछ छोटे पैच के साथ Rust coreutils क्रेट को WASI के साथ कंपाइल किया है, जो Asyncify बदलाव के ज़रिए पास किया गया है और JavaScript साइड पर WASI से File System Access API में एसिंक्रोनस बाइंडिंग लागू किया गया है. Xterm.js टर्मिनल कॉम्पोनेंट के साथ जोड़े जाने के बाद, यह ब्राउज़र टैब में चलने वाला एक असली शेल मिलता है. साथ ही, यह असल उपयोगकर्ता की फ़ाइलों पर काम करता है - बिलकुल किसी असल टर्मिनल की तरह.

इसे https://wasi.rreverser.com/ पर लाइव देखें.

एक साथ डेटा को सिंक करने की सुविधा के इस्तेमाल के उदाहरण, सिर्फ़ टाइमर और फ़ाइल सिस्टम तक ही सीमित नहीं हैं. इसके अलावा, वेब पर अन्य खास एपीआई का इस्तेमाल भी किया जा सकता है.

उदाहरण के लिए, Asyncify की मदद से भी, libubb को WebUSB API से मैप किया जा सकता है, जो शायद यूएसबी डिवाइसों पर काम करने के लिए सबसे लोकप्रिय नेटिव लाइब्रेरी है. इससे वेब पर इस तरह के डिवाइसों को एसिंक्रोनस ऐक्सेस मिलता है. मैप और कंपाइल करने के बाद, मुझे चुने हुए डिवाइसों पर चलाने के लिए स्टैंडर्ड लिब्सब टेस्ट और उदाहरण मिले. ये टेस्ट, वेब पेज के सैंडबॉक्स में ही मौजूद हैं.

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

हालांकि, यह मुमकिन है कि यह किसी अन्य ब्लॉग पोस्ट के लिए कहानी हो.

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