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

वेब पर मौजूद I/O एपीआई एसिंक्रोनस होते हैं, लेकिन सिस्टम की ज़्यादातर भाषाओं में वे सिंक्रोनस होते हैं. टास्क कब शुरू होगा कोड को WebAssembly में कंपाइल करते समय, आपको एक तरह के एपीआई को दूसरे से जोड़ना होगा—और यह ब्रिज सिंक न करें. इस पोस्ट में बताया गया है कि 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, सर्वर-साइड स्टोरेज, और एक नया File System Access API मिलेगा.

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

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

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

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

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

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

उदाहरण के लिए, अगर आपको ऊपर दिए गए सैंपल को मॉडर्न JavaScript में फिर से लिखना हो और का नाम रिमोट यूआरएल से मिलता है, तो आप फे़च एपीआई और एक साथ काम नहीं करने वाले सिंटैक्स का इस्तेमाल करेंगे:

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

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

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

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

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

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

कॉल ग्राफ़
JavaScript के बारे में जानकारी देना -> वेबअसेंबली -> वेब एपीआई -> एसिंक्रोनस टास्क शुरू करना, जहां 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() है, लेकिन इसे कॉलबैक स्वीकार करने वाले किसी भी दूसरे कॉन्टेक्स्ट में इस्तेमाल किया जा सकता है. आख़िर में, आपके पास जहां भी आप चाहें, ठीक उसी तरह सामान्य sleep() या किसी दूसरे सिंक्रोनस API (एपीआई) को async_sleep() पर कॉल करें.

ऐसे कोड को कंपाइल करते समय, आपको Emscripten को एसिंक्रोनसी सुविधा चालू करने के लिए कहना होगा. ऐसा तब तक करें -s ASYNCIFY और -s ASYNCIFY_IMPORTS=[func1, func2] की पास एसिंक्रोनस हो सकता है कि फ़ंक्शन की श्रेणी जैसी सूची.

emcc -O2 \
    -s ASYNCIFY \
    -s ASYNCIFY_IMPORTS=[async_sleep] \
    ...

इससे Emscripten को यह पता चलता है कि उन फ़ंक्शन को किए जाने वाले किसी भी कॉल के लिए, राज्य से जुड़ी होती है, इसलिए कंपाइलर ऐसे कॉल के आस-पास सपोर्ट करने वाला कोड इंजेक्ट करेगा.

अब, जब आप इस कोड को ब्राउज़र में एक्ज़ीक्यूट करेंगे, तो आपको अपनी उम्मीद के मुताबिक़ आसान आउटपुट लॉग दिखेगा, जिसमें B, A के बाद थोड़ी देर से आएगा.

A
B

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

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 कोड में मौजूद इसी तरह का सिंक्रोनस कॉल है, जिसे वेब पर async API. इस तरह, आप भी ऐसा कर सकते हैं!

सबसे पहले, आपको ऐसे फ़ंक्शन को 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 हमारे लिए ऐसा करेगा, लेकिन यहां इसका इस्तेमाल नहीं किया गया है, इसलिए यह प्रक्रिया थोड़ी और मैन्युअल है.

अच्छी बात यह है कि एसिंक्रोनसी ट्रांसफ़ॉर्मेशन पूरी तरह से टूलचेन-एग्नोस्टिक है. यह आर्बिट्रेरी प्रॉपर्टी में बदलाव कर सकता है 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 or npm asyncify-wasm नाम के नीचे.

यह स्टैंडर्ड WebAssembly इंस्टैंशिएशन को सिम्युलेट करता है API, लेकिन इसके अपने नेमस्पेस के तहत. सिर्फ़ अंतर यह है कि सामान्य WebAssembly API में, आप सिर्फ़ सिंक्रोनस फ़ंक्शन इंपोर्ट के साथ-साथ एसिंक्रोनस रैपर में एसिंक्रोनस इंपोर्ट भी किए जा सकते हैं:

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

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

यह सब किस तरह से काम करता है?

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

यह JavaScript की एसिंक्रोनस-अवेट सुविधा से काफ़ी मिलता-जुलता है, जिसे मैंने पहले दिखाया था. हालांकि, यह JavaScript में 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() के ठीक हो जाने पर, एसिंक्रोनस सहायता कोड mode को बदलकर REWINDING कर देगा और फ़ंक्शन को फिर से कॉल करें. इस बार, "सामान्य तरीके से एक्ज़ीक्यूट किया जाता है" ब्रांच को छोड़ दिया गया है - क्योंकि यह पहले ही हो चुका है मैंने काम को पिछली बार ठीक नहीं किया था और मुझे "A" प्रिंट नहीं करना है दो बार - और इसके बजाय यह सीधे "रिवाइंड कर रहा है" ब्रांच. पहुंच जाते ही, यह सेव की गई सभी लोकल लोकेशन को वापस ला देता है. इसके बाद, मोड वापस बदलकर "सामान्य" और एक्ज़ीक्यूशन को इस तरह से जारी रखता है मानो कोड कभी रुक ही न गया हो.

बदलाव की लागत

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

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

यह तरीका बिलकुल सही नहीं है. हालांकि, कई मामलों में यह तब स्वीकार किया जाता है, जब विकल्प के तौर पर किसी अन्य विकल्प का इस्तेमाल न किया जा रहा हो या आपको मूल कोड में ज़रूरी बदलाव करने होंगे.

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

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

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

जैसा कि लेख की शुरुआत में बताया गया है, वेब पर मौजूद स्टोरेज के विकल्पों में से एक है एसिंक्रोनस File System Access API. इससे आपको किसी वेब ऐप्लिकेशन से रीयल होस्ट फ़ाइल सिस्टम.

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

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

इस डेमो में, मैंने Rust coreutils क्रेट को WASI पर कुछ छोटे पैच, एसिंक्रोनस ट्रांसफ़ॉर्म के ज़रिए पास किए गए और एसिंक्रोनस लागू किए गए WASI की बाइंडिंग फ़ाइल सिस्टम ऐक्सेस एपीआई पर स्विच करें. एक बार इसके साथ जोड़े जाने पर Xterm.js टर्मिनल घटक, यह ब्राउज़र टैब और वास्तविक उपयोगकर्ता फ़ाइलों पर काम कर रहा होता है - बिलकुल किसी वास्तविक टर्मिनल की तरह.

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

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

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

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

हालांकि, यह किसी और ब्लॉग पोस्ट के लिए एक कहानी हो सकती है.

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