WebAssembly থেকে অ্যাসিঙ্ক্রোনাস ওয়েব API ব্যবহার করা

ওয়েবে I/O APIগুলি অ্যাসিঙ্ক্রোনাস, কিন্তু বেশিরভাগ সিস্টেম ভাষায় তারা সিঙ্ক্রোনাস। WebAssembly-এ কোড কম্পাইল করার সময়, আপনাকে এক ধরনের API-এর সাথে আরেকটা ব্রিজ করতে হবে—এবং এই ব্রিজটি হল Asyncify। এই পোস্টে, আপনি শিখবেন কখন এবং কীভাবে Asyncify ব্যবহার করবেন এবং কীভাবে এটি হুডের অধীনে কাজ করে।

সিস্টেমের ভাষায় I/O

আমি সি-তে একটি সাধারণ উদাহরণ দিয়ে শুরু করব। বলুন, আপনি একটি ফাইল থেকে ব্যবহারকারীর নাম পড়তে চান এবং "হ্যালো, (ব্যবহারকারীর নাম)!" দিয়ে তাদের শুভেচ্ছা জানান। বার্তা:

#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 সিঙ্ক্রোনাস API-এর আকারে উপস্থাপন করে। উদাহরণ স্বরূপ, আপনি যদি উদাহরণটিকে Rust-এ অনুবাদ করেন, তাহলে APIটি সহজ দেখাতে পারে, কিন্তু একই নীতি প্রযোজ্য। আপনি শুধু একটি কল করুন এবং সিঙ্ক্রোনাসভাবে ফলাফলটি ফেরত দেওয়ার জন্য অপেক্ষা করুন, যখন এটি সমস্ত ব্যয়বহুল ক্রিয়াকলাপ সম্পাদন করে এবং অবশেষে একটি একক আহ্বানে ফলাফলটি ফেরত দেয়:

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

কিন্তু যখন আপনি WebAssembly-তে সেই নমুনাগুলির মধ্যে যেকোনও কম্পাইল করার চেষ্টা করেন এবং ওয়েবে অনুবাদ করেন তখন কী হয়? অথবা, একটি নির্দিষ্ট উদাহরণ প্রদান করতে, "ফাইল রিড" অপারেশন কি অনুবাদ করতে পারে? এটি কিছু স্টোরেজ থেকে ডেটা পড়তে হবে।

ওয়েবের অ্যাসিঙ্ক্রোনাস মডেল

ওয়েবে বিভিন্ন ধরণের স্টোরেজ বিকল্প রয়েছে যা আপনি ম্যাপ করতে পারেন, যেমন ইন-মেমরি স্টোরেজ (JS অবজেক্ট), localStorage , IndexedDB , সার্ভার-সাইড স্টোরেজ, এবং একটি নতুন ফাইল সিস্টেম অ্যাক্সেস API

যাইহোক, এই API-গুলির মধ্যে শুধুমাত্র দুটি-ইন-মেমরি স্টোরেজ এবং localStorage -সিঙ্ক্রোনাসভাবে ব্যবহার করা যেতে পারে, এবং উভয়ই আপনি কী সঞ্চয় করতে পারেন এবং কতক্ষণ ধরে রাখতে পারেন তার সবচেয়ে সীমিত বিকল্প। অন্যান্য সমস্ত বিকল্প শুধুমাত্র অ্যাসিঙ্ক্রোনাস API প্রদান করে।

এটি ওয়েবে কোড কার্যকর করার মূল বৈশিষ্ট্যগুলির মধ্যে একটি: যেকোন সময়-সাপেক্ষ ক্রিয়াকলাপ, যার মধ্যে যেকোন I/O রয়েছে, অসিঙ্ক্রোনাস হতে হবে।

কারণ হল ওয়েব ঐতিহাসিকভাবে একক-থ্রেডেড, এবং UI-কে স্পর্শ করে এমন যেকোনো ব্যবহারকারী কোডকে UI-এর মতো একই থ্রেডে চলতে হবে। এটি সিপিইউ সময়ের জন্য লেআউট, রেন্ডারিং এবং ইভেন্ট পরিচালনার মতো অন্যান্য গুরুত্বপূর্ণ কাজের সাথে প্রতিযোগিতা করতে হবে। আপনি জাভাস্ক্রিপ্ট বা WebAssembly-এর একটি টুকরা একটি "ফাইল রিড" অপারেশন শুরু করতে সক্ষম হতে চান না এবং অন্য সব কিছুকে ব্লক করতে পারেন—সম্পূর্ণ ট্যাব, বা, অতীতে, সম্পূর্ণ ব্রাউজার—মিলিসেকেন্ড থেকে কয়েক সেকেন্ডের জন্য , এটা শেষ না হওয়া পর্যন্ত।

পরিবর্তে, কোডটিকে শুধুমাত্র একটি I/O অপারেশনের সময়সূচী করার অনুমতি দেওয়া হয় এবং এটি শেষ হয়ে গেলে একটি কলব্যাক নির্বাহ করার জন্য। এই ধরনের কলব্যাকগুলি ব্রাউজারের ইভেন্ট লুপের অংশ হিসাবে কার্যকর করা হয়। আমি এখানে বিশদে যাব না, তবে আপনি যদি ইভেন্ট লুপটি হুডের নীচে কীভাবে কাজ করে তা শিখতে আগ্রহী হন তবে টাস্ক, মাইক্রোটাস্ক, সারি এবং সময়সূচী দেখুন যা এই বিষয়টিকে গভীরভাবে ব্যাখ্যা করে।

সংক্ষিপ্ত সংস্করণটি হল যে ব্রাউজারটি সমস্ত কোডের টুকরোগুলিকে একটি অসীম লুপের মতো চালায়, সেগুলিকে একের পর এক সারি থেকে নিয়ে যায়। যখন কিছু ইভেন্ট ট্রিগার করা হয়, তখন ব্রাউজার সংশ্লিষ্ট হ্যান্ডলারকে সারিবদ্ধ করে এবং পরবর্তী লুপ পুনরাবৃত্তিতে এটি সারি থেকে বের করে দেওয়া হয় এবং কার্যকর করা হয়। এই প্রক্রিয়াটি শুধুমাত্র একটি একক থ্রেড ব্যবহার করার সময় একযোগে অনুকরণ এবং প্রচুর সমান্তরাল ক্রিয়াকলাপ চালানোর অনুমতি দেয়।

এই প্রক্রিয়া সম্পর্কে মনে রাখা গুরুত্বপূর্ণ বিষয় হল যে, আপনার কাস্টম জাভাস্ক্রিপ্ট (বা WebAssembly) কোড কার্যকর করার সময়, ইভেন্ট লুপটি ব্লক করা হয় এবং এটি থাকাকালীন, কোনও বহিরাগত হ্যান্ডলার, ইভেন্ট, I/O-তে প্রতিক্রিয়া জানানোর কোনও উপায় নেই। ইত্যাদি। I/O ফলাফলগুলি ফিরে পাওয়ার একমাত্র উপায় হল একটি কলব্যাক নিবন্ধন করা, আপনার কোড কার্যকর করা শেষ করা এবং ব্রাউজারকে নিয়ন্ত্রণ ফিরিয়ে দেওয়া যাতে এটি কোনো মুলতুবি থাকা কাজগুলিকে প্রক্রিয়াজাত করতে পারে। একবার I/O শেষ হয়ে গেলে, আপনার হ্যান্ডলার সেই কাজগুলির মধ্যে একটি হয়ে উঠবে এবং কার্যকর করা হবে।

উদাহরণস্বরূপ, যদি আপনি আধুনিক জাভাস্ক্রিপ্টে উপরের নমুনাগুলি পুনরায় লিখতে চান এবং একটি দূরবর্তী URL থেকে একটি নাম পড়ার সিদ্ধান্ত নেন, তাহলে আপনি 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));
}

এই ডি-সুগারড উদাহরণে, যা একটু পরিষ্কার, একটি অনুরোধ শুরু হয়েছে এবং প্রতিক্রিয়াগুলি প্রথম কলব্যাকের সাথে সদস্যতা নেওয়া হয়েছে৷ একবার ব্রাউজার প্রাথমিক প্রতিক্রিয়া-শুধুমাত্র HTTP শিরোনামগুলি পায়-এটি অ্যাসিঙ্ক্রোনাসভাবে এই কলব্যাককে আহ্বান করে। কলব্যাক response.text() ব্যবহার করে টেক্সট হিসেবে বডি পড়া শুরু করে এবং অন্য কলব্যাকের সাথে ফলাফলে সদস্যতা নেয়। অবশেষে, একবার fetch সমস্ত বিষয়বস্তু পুনরুদ্ধার করা হলে, এটি শেষ কলব্যাককে আহ্বান করে, যা "হ্যালো, (ব্যবহারকারীর নাম)!" কনসোলে

এই পদক্ষেপগুলির অ্যাসিঙ্ক্রোনাস প্রকৃতির জন্য ধন্যবাদ, মূল ফাংশনটি I/O নির্ধারিত হওয়ার সাথে সাথে ব্রাউজারে নিয়ন্ত্রণ ফিরিয়ে দিতে পারে এবং সম্পূর্ণ UI রেসপন্সিভ ছেড়ে দিতে পারে এবং রেন্ডারিং, স্ক্রলিং ইত্যাদি সহ অন্যান্য কাজের জন্য উপলব্ধ থাকতে পারে। I/O ব্যাকগ্রাউন্ডে কার্যকর হচ্ছে।

একটি চূড়ান্ত উদাহরণ হিসাবে, এমনকি "স্লিপ" এর মতো সাধারণ API, যা একটি অ্যাপ্লিকেশনকে একটি নির্দিষ্ট সংখ্যক সেকেন্ড অপেক্ষা করে, এটিও একটি 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");

প্রকৃতপক্ষে, এমস্ক্রিপ্টেন "স্লিপ" এর ডিফল্ট বাস্তবায়নে ঠিক এটিই করে, তবে এটি খুব অকার্যকর, পুরো UI ব্লক করবে এবং এর মধ্যে অন্য কোনও ইভেন্ট পরিচালনা করার অনুমতি দেবে না। সাধারণত, উত্পাদন কোডে এটি করবেন না।

পরিবর্তে, জাভাস্ক্রিপ্টে "sleep"-এর আরও একটি মূর্খ সংস্করণ setTimeout() কল করা এবং হ্যান্ডলারের সাথে সদস্যতা নেওয়া জড়িত:

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

এই সব উদাহরণ এবং এপিআই সাধারণ কি? প্রতিটি ক্ষেত্রে, মূল সিস্টেমের ভাষায় বাহাদুরি কোড I/O-এর জন্য একটি ব্লকিং API ব্যবহার করে, যেখানে ওয়েবের জন্য একটি সমতুল্য উদাহরণ পরিবর্তে একটি অ্যাসিঙ্ক্রোনাস API ব্যবহার করে। ওয়েবে কম্পাইল করার সময়, আপনাকে সেই দুটি এক্সিকিউশন মডেলের মধ্যে কোনো না কোনোভাবে রূপান্তর করতে হবে এবং WebAssembly-এর এখনও তা করার কোনো অন্তর্নির্মিত ক্ষমতা নেই।

Asyncify দিয়ে ব্যবধান পূরণ করা

এখানেই Asyncify আসে। Asyncify হল Emscripten দ্বারা সমর্থিত একটি কম্পাইল-টাইম বৈশিষ্ট্য যা সম্পূর্ণ প্রোগ্রামকে বিরাম দিতে এবং পরে অ্যাসিঙ্ক্রোনাসভাবে পুনরায় শুরু করতে দেয়।

একটি জাভাস্ক্রিপ্ট -> WebAssembly -> web API -> async টাস্ক ইনভোকেশন বর্ণনা করে একটি কল গ্রাফ, যেখানে 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 হল একটি ম্যাক্রো যা জাভাস্ক্রিপ্টের স্নিপেটগুলিকে C ফাংশন হিসাবে সংজ্ঞায়িত করতে দেয়৷ ভিতরে, একটি ফাংশন Asyncify.handleSleep() ব্যবহার করুন যা Emscripten কে প্রোগ্রাম স্থগিত করতে বলে এবং একটি wakeUp() হ্যান্ডলার প্রদান করে যা অ্যাসিঙ্ক্রোনাস অপারেশন শেষ হলে কল করা উচিত। উপরের উদাহরণে, হ্যান্ডলারটিকে setTimeout() এ পাস করা হয়েছে, কিন্তু এটি অন্য কোনো প্রসঙ্গে ব্যবহার করা যেতে পারে যা কলব্যাক গ্রহণ করে। অবশেষে, আপনি যেকোন জায়গায় async_sleep() কল করতে পারেন ঠিক রেগুলার sleep() বা অন্য কোন সিঙ্ক্রোনাস API এর মত।

এই ধরনের কোড কম্পাইল করার সময়, আপনাকে Asyncify বৈশিষ্ট্যটি সক্রিয় করতে Emscripten কে বলতে হবে। -s ASYNCIFY পাশাপাশি -s ASYNCIFY_IMPORTS=[func1, func2] ফাংশনগুলির একটি অ্যারের মতো তালিকা যা অ্যাসিঙ্ক্রোনাস হতে পারে পাস করে এটি করুন।

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

এটি এমস্ক্রিপ্টেনকে জানতে দেয় যে এই ফাংশনগুলিতে যে কোনও কলের জন্য রাজ্য সংরক্ষণ এবং পুনরুদ্ধারের প্রয়োজন হতে পারে, তাই কম্পাইলার এই ধরনের কলগুলির চারপাশে সমর্থনকারী কোড ইনজেক্ট করবে।

এখন, যখন আপনি ব্রাউজারে এই কোডটি কার্যকর করবেন তখন আপনি একটি বিরামবিহীন আউটপুট লগ দেখতে পাবেন যেমনটি আপনি আশা করেছিলেন, 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() এর মত প্রতিশ্রুতি-ভিত্তিক APIগুলির জন্য, আপনি কলব্যাক-ভিত্তিক API ব্যবহার করার পরিবর্তে 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 এম্বিন্ড নামে একটি বৈশিষ্ট্য প্রদান করে যা আপনাকে জাভাস্ক্রিপ্ট এবং C++ মানগুলির মধ্যে রূপান্তরগুলি পরিচালনা করতে দেয়। এটি Asyncify-এর জন্যও সমর্থন রয়েছে, তাই আপনি বহিরাগত Promise s-এ await() কল করতে পারেন এবং এটি await -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 মহান কাজ করে. অন্যান্য টুলচেইন এবং ভাষা সম্পর্কে কি?

অন্যান্য ভাষা থেকে ব্যবহার

বলুন যে আপনার মরিচা কোডের কোথাও আপনার একটি অনুরূপ সিঙ্ক্রোনাস কল রয়েছে যা আপনি ওয়েবে একটি অ্যাসিঙ্ক 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 আমাদের জন্য এটি করবে, কিন্তু এটি এখানে ব্যবহার করা হয়নি, তাই প্রক্রিয়াটি একটু বেশি ম্যানুয়াল।

ভাগ্যক্রমে, Asyncify রূপান্তর নিজেই সম্পূর্ণরূপে টুলচেন-অজ্ঞেয়বাদী। এটি নির্বিচারে WebAssembly ফাইলগুলিকে রূপান্তর করতে পারে, এটি যে কম্পাইলার দ্বারা উত্পাদিত হোক না কেন। Binaryen টুলচেইন থেকে wasm-opt optimizer-এর অংশ হিসেবে ট্রান্সফর্মটি আলাদাভাবে প্রদান করা হয়েছে এবং এটিকে এভাবে আহ্বান করা যেতে পারে:

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

ট্রান্সফর্ম সক্রিয় করতে --asyncify পাস করুন, এবং তারপর অসিঙ্ক্রোনাস ফাংশনগুলির একটি কমা-বিভাজিত তালিকা প্রদান করতে --pass-arg=… ব্যবহার করুন, যেখানে প্রোগ্রামের অবস্থা স্থগিত করা উচিত এবং পরে পুনরায় চালু করা উচিত।

যা বাকি আছে তা হল সমর্থনকারী রানটাইম কোড প্রদান করা যা আসলে তা করবে—ওয়েবঅ্যাসেম্বলি কোড সাসপেন্ড এবং পুনরায় শুরু করুন। আবার, C/C++ ক্ষেত্রে এটি Emscripten দ্বারা অন্তর্ভুক্ত করা হবে, কিন্তু এখন আপনার কাস্টম জাভাস্ক্রিপ্ট আঠালো কোড প্রয়োজন যা নির্বিচারে 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() - WebAssembly দিক থেকে, লাইব্রেরি ফিরে আসা Promise সনাক্ত করবে, WebAssembly অ্যাপ্লিকেশনটির অবস্থা স্থগিত করবে এবং সংরক্ষণ করবে, প্রতিশ্রুতি সম্পন্ন হওয়ার জন্য সদস্যতা গ্রহণ করবে এবং পরে , একবার এটি সমাধান হয়ে গেলে, নির্বিঘ্নে কল স্ট্যাক এবং স্টেট পুনরুদ্ধার করুন এবং এক্সিকিউশন চালিয়ে যান যেন কিছুই হয়নি।

যেহেতু মডিউলের যেকোন ফাংশন একটি অ্যাসিঙ্ক্রোনাস কল করতে পারে, সমস্ত রপ্তানিও সম্ভাব্য অ্যাসিঙ্ক্রোনাস হয়ে যায়, তাই সেগুলিও মোড়ানো হয়। আপনি হয়ত উপরের উদাহরণে লক্ষ্য করেছেন যে কখন কার্যকর করা সত্যিই শেষ হয়েছে তা জানতে আপনাকে instance.exports.main() এর ফলাফলের await করতে হবে।

কিভাবে এই সব ফণা অধীনে কাজ করে?

যখন Asyncify ASYNCIFY_IMPORTS ফাংশনগুলির একটিতে একটি কল শনাক্ত করে, তখন এটি একটি অ্যাসিঙ্ক্রোনাস অপারেশন শুরু করে, কল স্ট্যাক এবং যেকোনো অস্থায়ী লোকাল সহ অ্যাপ্লিকেশনটির সমগ্র অবস্থা সংরক্ষণ করে এবং পরে, যখন সেই অপারেশনটি শেষ হয়, সমস্ত মেমরি এবং কল পুনরুদ্ধার করে স্ট্যাক এবং একই জায়গা থেকে এবং একই অবস্থার সাথে পুনরায় শুরু হয় যেন প্রোগ্রামটি কখনই বন্ধ হয়নি।

এটি জাভাস্ক্রিপ্টের অ্যাসিঙ্ক-অপেক্ষা বৈশিষ্ট্যের মতো যা আমি আগে দেখিয়েছি, কিন্তু, জাভাস্ক্রিপ্টের বিপরীতে, ভাষা থেকে কোনও বিশেষ সিনট্যাক্স বা রানটাইম সমর্থনের প্রয়োজন হয় না এবং পরিবর্তে কম্পাইল-টাইমে প্লেইন সিঙ্ক্রোনাস ফাংশনগুলি রূপান্তর করে কাজ করে।

পূর্বে দেখানো অ্যাসিঙ্ক্রোনাস ঘুমের উদাহরণ কম্পাইল করার সময়:

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 সমর্থন কোড REWINDINGmode পরিবর্তন করবে এবং ফাংশনটিকে আবার কল করবে। এইবার, "স্বাভাবিক নির্বাহ" শাখাটি বাদ দেওয়া হয়েছে - যেহেতু এটি ইতিমধ্যেই গতবার কাজ করেছে এবং আমি "A" দুবার মুদ্রণ এড়াতে চাই - এবং পরিবর্তে এটি সরাসরি "রিওয়াইন্ডিং" শাখায় আসে। একবার এটি পৌঁছে গেলে, এটি সমস্ত সঞ্চিত লোকালকে পুনরুদ্ধার করে, মোডকে "স্বাভাবিক" এ পরিবর্তন করে এবং এক্সিকিউশনটি চালিয়ে যায় যেন কোডটি প্রথম স্থানে বন্ধ করা হয়নি।

রূপান্তর খরচ

দুর্ভাগ্যবশত, Asyncify ট্রান্সফর্ম সম্পূর্ণ বিনামূল্যে নয়, যেহেতু এই সমস্ত লোকালকে সংরক্ষণ এবং পুনরুদ্ধার করার জন্য, বিভিন্ন মোডের অধীনে কল স্ট্যাক নেভিগেট করার জন্য এটিকে বেশ কিছুটা সমর্থনকারী কোড ইনজেকশন করতে হবে। এটি শুধুমাত্র কমান্ড লাইনে অ্যাসিঙ্ক্রোনাস হিসাবে চিহ্নিত ফাংশনগুলিকে সংশোধন করার চেষ্টা করে, সেইসাথে তাদের সম্ভাব্য কলারগুলির মধ্যে যেকোনও, কিন্তু কোড সাইজ ওভারহেড এখনও কম্প্রেশনের আগে প্রায় 50% পর্যন্ত যোগ করতে পারে।

বিভিন্ন বেঞ্চমার্কের জন্য কোড সাইজ ওভারহেড দেখানো একটি গ্রাফ, সূক্ষ্ম-সংযুক্ত অবস্থার অধীনে প্রায় 0% থেকে খারাপ ক্ষেত্রে 100% পর্যন্ত

এটি আদর্শ নয়, তবে অনেক ক্ষেত্রেই গ্রহণযোগ্য যখন বিকল্পটির কার্যকারিতা সম্পূর্ণরূপে নেই বা মূল কোডে উল্লেখযোগ্য পুনর্লিখন করতে হচ্ছে।

চূড়ান্ত বিল্ডগুলির জন্য সর্বদা অপ্টিমাইজেশান সক্ষম করার বিষয়টি নিশ্চিত করুন যাতে এটি আরও উপরে না যায়। আপনি শুধুমাত্র নির্দিষ্ট ফাংশন এবং/অথবা শুধুমাত্র সরাসরি ফাংশন কলে রূপান্তর সীমিত করে ওভারহেড কমাতে Asyncify-নির্দিষ্ট অপ্টিমাইজেশন বিকল্পগুলি পরীক্ষা করতে পারেন। রানটাইম পারফরম্যান্সের জন্য একটি ছোট খরচও রয়েছে, তবে এটি অ্যাসিঙ্ক কলের মধ্যে সীমাবদ্ধ। যাইহোক, প্রকৃত কাজের খরচের তুলনায়, এটি সাধারণত নগণ্য।

বাস্তব বিশ্বের ডেমো

এখন আপনি সাধারণ উদাহরণগুলি দেখেছেন, আমি আরও জটিল পরিস্থিতিতে চলে যাব।

নিবন্ধের শুরুতে যেমন উল্লেখ করা হয়েছে, ওয়েবে স্টোরেজ বিকল্পগুলির মধ্যে একটি হল একটি অ্যাসিঙ্ক্রোনাস ফাইল সিস্টেম অ্যাক্সেস API । এটি একটি ওয়েব অ্যাপ্লিকেশন থেকে একটি বাস্তব হোস্ট ফাইল সিস্টেমে অ্যাক্সেস প্রদান করে।

অন্যদিকে, কনসোল এবং সার্ভার-সাইডে WebAssembly I/O-এর জন্য WASI নামে একটি ডি-ফ্যাক্টো স্ট্যান্ডার্ড রয়েছে। এটি সিস্টেম ভাষার জন্য একটি সংকলন লক্ষ্য হিসাবে ডিজাইন করা হয়েছিল, এবং এটি একটি ঐতিহ্যগত সিঙ্ক্রোনাস আকারে সমস্ত ধরণের ফাইল সিস্টেম এবং অন্যান্য ক্রিয়াকলাপকে প্রকাশ করে।

যদি আপনি একটি অন্য মানচিত্র করতে পারে? তারপর আপনি WASI টার্গেটকে সমর্থনকারী যেকোন টুলচেনের সাহায্যে যেকোন সোর্স ল্যাঙ্গুয়েজে যেকোন অ্যাপ্লিকেশন কম্পাইল করতে পারেন এবং এটিকে ওয়েবে একটি স্যান্ডবক্সে চালাতে পারেন, যদিও এটিকে বাস্তব ব্যবহারকারী ফাইলে কাজ করার অনুমতি দেয়! Asyncify দিয়ে, আপনি ঠিক এটি করতে পারেন।

এই ডেমোতে, আমি WASI-তে কয়েকটি ছোট প্যাচ সহ রাস্ট কোরিউটিল ক্রেট সংকলন করেছি, যা Asyncify ট্রান্সফর্মের মাধ্যমে পাস করেছি এবং জাভাস্ক্রিপ্টের পাশে WASI থেকে ফাইল সিস্টেম অ্যাক্সেস API-তে অ্যাসিঙ্ক্রোনাস বাইন্ডিং প্রয়োগ করেছি। একবার Xterm.js টার্মিনাল কম্পোনেন্টের সাথে মিলিত হলে, এটি ব্রাউজার ট্যাবে চলমান একটি বাস্তবসম্মত শেল প্রদান করে এবং বাস্তব ব্যবহারকারী ফাইলগুলিতে অপারেটিং করে - ঠিক একটি আসল টার্মিনালের মতো।

এটি https://wasi.rreverser.com/ এ লাইভ দেখুন।

Asyncify ব্যবহার-ক্ষেত্রগুলি শুধুমাত্র টাইমার এবং ফাইল সিস্টেমের মধ্যে সীমাবদ্ধ নয়। আপনি আরও যেতে পারেন এবং ওয়েবে আরও কুলুঙ্গি API ব্যবহার করতে পারেন।

উদাহরণস্বরূপ, Asyncify-এর সাহায্যে, একটি WebUSB API- তে libusb — সম্ভবত USB ডিভাইসগুলির সাথে কাজ করার জন্য সবচেয়ে জনপ্রিয় নেটিভ লাইব্রেরি — ম্যাপ করা সম্ভব, যা ওয়েবে এই জাতীয় ডিভাইসগুলিতে অ্যাসিঙ্ক্রোনাস অ্যাক্সেস দেয়৷ একবার ম্যাপ করা এবং কম্পাইল করা হলে, আমি একটি ওয়েব পৃষ্ঠার স্যান্ডবক্সে সরাসরি নির্বাচিত ডিভাইসগুলির বিরুদ্ধে চালানোর জন্য আদর্শ libusb পরীক্ষা এবং উদাহরণ পেয়েছি।

একটি ওয়েব পৃষ্ঠায় libusb ডিবাগ আউটপুটের স্ক্রিনশট, সংযুক্ত ক্যানন ক্যামেরা সম্পর্কে তথ্য দেখাচ্ছে

এটি সম্ভবত অন্য ব্লগ পোস্টের জন্য একটি গল্প যদিও.

এই উদাহরণগুলি দেখায় যে Asyncify ব্যবধান পূরণ করতে এবং ওয়েবে সমস্ত ধরণের অ্যাপ্লিকেশন পোর্ট করার জন্য কতটা শক্তিশালী হতে পারে, আপনাকে কার্যকারিতা হারানো ছাড়াই ক্রস-প্ল্যাটফর্ম অ্যাক্সেস, স্যান্ডবক্সিং এবং আরও ভাল নিরাপত্তা লাভ করতে দেয়।