ওয়েবের I/O API-গুলো অ্যাসিঙ্ক্রোনাস, কিন্তু বেশিরভাগ সিস্টেম ল্যাঙ্গুয়েজে এগুলো সিঙ্ক্রোনাস। WebAssembly-তে কোড কম্পাইল করার সময়, এক ধরনের API-এর সাথে অন্য ধরনের API-এর সংযোগ স্থাপন করতে হয়—এবং এই সংযোগটিই হলো 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 সিনক্রোনাস এপিআই (API) আকারে উপস্থাপন করে। উদাহরণস্বরূপ, আপনি যদি উদাহরণটি রাস্ট (Rust)-এ অনুবাদ করেন, তাহলে এপিআইটি আরও সরল মনে হতে পারে, কিন্তু একই নীতিগুলি প্রযোজ্য। আপনি শুধু একটি কল করেন এবং ফলাফল ফেরত আসার জন্য সিনক্রোনাসভাবে অপেক্ষা করেন, যখন এটি সমস্ত ব্যয়বহুল অপারেশন সম্পাদন করে এবং অবশেষে একটি একক আহ্বানেই ফলাফল ফেরত দেয়:
fn main() {
let s = std::fs::read_to_string("name.txt");
println!("Hello, {}!", s);
}
কিন্তু কী হবে যখন আপনি সেই নমুনাগুলোর কোনোটিকে ওয়েবঅ্যাসেম্বলিতে কম্পাইল করে ওয়েবের জন্য অনুবাদ করার চেষ্টা করবেন? অথবা, একটি নির্দিষ্ট উদাহরণ দিতে গেলে, "ফাইল রিড" অপারেশনটি কীসে রূপান্তরিত হতে পারে? এর জন্য কোনো স্টোরেজ থেকে ডেটা রিড করার প্রয়োজন হবে।
ওয়েবের অ্যাসিঙ্ক্রোনাস মডেল
ওয়েবে বিভিন্ন ধরনের স্টোরেজ অপশন রয়েছে যা আপনি ব্যবহার করতে পারেন, যেমন ইন-মেমরি স্টোরেজ (JS অবজেক্ট), localStorage , ইনডেক্সডডিবি , সার্ভার-সাইড স্টোরেজ এবং একটি নতুন ফাইল সিস্টেম অ্যাক্সেস এপিআই ।
তবে, এই এপিআইগুলোর মধ্যে কেবল দুটি—ইন-মেমরি স্টোরেজ এবং localStorage —সিঙ্ক্রোনাসভাবে ব্যবহার করা যায়, এবং কী ও কত সময়ের জন্য সংরক্ষণ করা যাবে, তার ক্ষেত্রে এই দুটিই সবচেয়ে সীমাবদ্ধ বিকল্প। বাকি সব বিকল্পে কেবল অ্যাসিঙ্ক্রোনাস এপিআই রয়েছে।
ওয়েবে কোড নির্বাহ করার এটি একটি অন্যতম মূল বৈশিষ্ট্য: যেকোনো সময়সাপেক্ষ অপারেশন, যার মধ্যে যেকোনো ইনপুট/আউটপুট অন্তর্ভুক্ত, অবশ্যই অ্যাসিঙ্ক্রোনাস হতে হবে।
এর কারণ হলো, ওয়েব ঐতিহাসিকভাবেই সিঙ্গেল-থ্রেডেড, এবং ইউজার কোড যা UI-কে প্রভাবিত করে, তাকে UI-এর সাথেই একই থ্রেডে চলতে হয়। সিপিইউ-এর সময়ের জন্য একে লেআউট, রেন্ডারিং এবং ইভেন্ট হ্যান্ডলিং-এর মতো অন্যান্য গুরুত্বপূর্ণ কাজের সাথে প্রতিযোগিতা করতে হয়। আপনি নিশ্চয়ই চাইবেন না যে জাভাস্ক্রিপ্ট বা ওয়েবঅ্যাসেম্বলির কোনো কোড একটি "ফাইল রিড" অপারেশন শুরু করে, কাজটি শেষ না হওয়া পর্যন্ত কয়েক মিলিসেকেন্ড থেকে কয়েক সেকেন্ডের জন্য অন্য সবকিছু—পুরো ট্যাব, বা অতীতে যেমনটা হতো, পুরো ব্রাউজার—ব্লক করে দিক।
এর পরিবর্তে, কোডকে শুধুমাত্র একটি I/O অপারেশনের সাথে একটি কলব্যাক শিডিউল করার অনুমতি দেওয়া হয়, যা অপারেশনটি শেষ হলে কার্যকর হবে। এই ধরনের কলব্যাকগুলো ব্রাউজারের ইভেন্ট লুপের অংশ হিসেবে কার্যকর হয়। আমি এখানে বিস্তারিত আলোচনা করব না, তবে ইভেন্ট লুপ কীভাবে কাজ করে তা শিখতে আগ্রহী হলে, "Tasks, microtasks, queues and schedules" লেখাটি দেখতে পারেন, যেখানে এই বিষয়টি বিশদভাবে ব্যাখ্যা করা হয়েছে।
সংক্ষেপে বলতে গেলে, ব্রাউজারটি কিউ থেকে এক এক করে কোডের অংশগুলো নিয়ে এক প্রকার অসীম লুপের মধ্যে চালায়। যখন কোনো ইভেন্ট ট্রিগার হয়, ব্রাউজারটি সংশ্লিষ্ট হ্যান্ডলারটিকে কিউতে যুক্ত করে, এবং পরবর্তী লুপ ইটারেশনে সেটিকে কিউ থেকে বের করে এক্সিকিউট করা হয়। এই পদ্ধতিটি শুধুমাত্র একটি থ্রেড ব্যবহার করেই কনকারেন্সি অনুকরণ করতে এবং প্রচুর প্যারালাল অপারেশন চালাতে সাহায্য করে।
এই প্রক্রিয়াটি সম্পর্কে মনে রাখার গুরুত্বপূর্ণ বিষয়টি হলো, যখন আপনার নিজস্ব জাভাস্ক্রিপ্ট (বা ওয়েবঅ্যাসেম্বলি) কোড চলে, তখন ইভেন্ট লুপটি ব্লক হয়ে থাকে এবং এই অবস্থায় কোনো বাহ্যিক হ্যান্ডলার, ইভেন্ট, 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 সমস্ত বিষয়বস্তু সংগ্রহ করার পরে, এটি শেষ কলব্যাকটিকে কল করে, যা কনসোলে "Hello, (username)!" প্রিন্ট করে।
এই ধাপগুলোর অ্যাসিঙ্ক্রোনাস প্রকৃতির কারণে, I/O নির্ধারিত হওয়ার সাথে সাথেই মূল ফাংশনটি ব্রাউজারে নিয়ন্ত্রণ ফিরিয়ে দিতে পারে এবং I/O ব্যাকগ্রাউন্ডে চলমান থাকাকালীন সম্পূর্ণ UI-কে রেসপন্সিভ ও রেন্ডারিং, স্ক্রলিং ইত্যাদি অন্যান্য কাজের জন্য উপলব্ধ রাখতে পারে।
শেষ উদাহরণ হিসেবে বলা যায়, 'sleep'-এর মতো সাধারণ এপিআই-ও, যা কোনো অ্যাপ্লিকেশনকে নির্দিষ্ট সংখ্যক সেকেন্ড অপেক্ষা করায়, সেটিও এক প্রকার আই/ও অপারেশন।
#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'-এর ডিফল্ট বাস্তবায়নে ঠিক এটাই করে, কিন্তু এটি খুবই অদক্ষ একটি পদ্ধতি, যা পুরো UI-কে ব্লক করে দেয় এবং এই সময়ের মধ্যে অন্য কোনো ইভেন্টকে হ্যান্ডেল করার সুযোগ দেয় না। সাধারণত, প্রোডাকশন কোডে এমনটা করবেন না।
এর পরিবর্তে, জাভাস্ক্রিপ্টে 'sleep'-এর একটি আরও প্রচলিত সংস্করণ হলো setTimeout() কল করা এবং একটি হ্যান্ডলারের মাধ্যমে সাবস্ক্রাইব করা:
console.log("A");
setTimeout(() => {
console.log("B");
}, 1000);
এই সমস্ত উদাহরণ এবং এপিআই-গুলোর মধ্যে সাধারণ মিল কী? প্রতিটি ক্ষেত্রেই, মূল সিস্টেম ল্যাঙ্গুয়েজের প্রচলিত কোড ইনপুট/আউটপুটের জন্য একটি ব্লকিং এপিআই ব্যবহার করে, যেখানে ওয়েবের জন্য এর সমতুল্য উদাহরণে একটি অ্যাসিঙ্ক্রোনাস এপিআই ব্যবহৃত হয়। ওয়েবের জন্য কম্পাইল করার সময়, আপনাকে কোনোভাবে এই দুটি এক্সিকিউশন মডেলের মধ্যে রূপান্তর করতে হয়, এবং ওয়েবঅ্যাসেম্বলিতে এখনও পর্যন্ত তা করার মতো কোনো অন্তর্নির্মিত ক্ষমতা নেই।
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 একটি ম্যাক্রো যা জাভাস্ক্রিপ্ট কোডকে C ফাংশনের মতো করে সংজ্ঞায়িত করার সুযোগ দেয়। এর ভেতরে Asyncify.handleSleep() ফাংশনটি ব্যবহার করুন, যা Emscripten-কে প্রোগ্রামটি স্থগিত করতে বলে এবং একটি wakeUp() হ্যান্ডলার সরবরাহ করে, যা অ্যাসিঙ্ক্রোনাস অপারেশনটি শেষ হয়ে গেলে কল করা উচিত। উপরের উদাহরণে, হ্যান্ডলারটি setTimeout() ফাংশনে পাস করা হয়েছে, কিন্তু এটি কলব্যাক গ্রহণ করে এমন অন্য যেকোনো প্রসঙ্গেও ব্যবহার করা যেতে পারে। সবশেষে, আপনি async_sleep() সাধারণ sleep() বা অন্য যেকোনো সিঙ্ক্রোনাস API-এর মতোই আপনার ইচ্ছামতো যেকোনো জায়গায় কল করতে পারেন।
এই ধরনের কোড কম্পাইল করার সময়, আপনাকে Emscripten-কে Asyncify ফিচারটি সক্রিয় করতে বলতে হবে। এটি করার জন্য -s ASYNCIFY এবং সেইসাথে -s ASYNCIFY_IMPORTS=[func1, func2] পাস করুন, যেখানে 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() মতো Promise-ভিত্তিক API-গুলোর জন্য, আপনি কলব্যাক-ভিত্তিক API ব্যবহার না করে Asyncify-কে JavaScript-এর async-await ফিচারের সাথেও যুক্ত করতে পারেন। এর জন্য, Asyncify.handleSleep() এর পরিবর্তে Asyncify.handleAsync() কল করুন। তাহলে, wakeUp() কলব্যাক শিডিউল করার পরিবর্তে, আপনি একটি async JavaScript ফাংশন পাস করতে পারবেন এবং এর ভেতরে await ও return ব্যবহার করতে পারবেন, যা কোডকে আরও স্বাভাবিক ও সিনক্রোনাস করে তুলবে, অথচ asynchronous 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 নামে একটি ফিচার রয়েছে যা জাভাস্ক্রিপ্ট এবং C++ ভ্যালুর মধ্যে রূপান্তর পরিচালনা করতে সাহায্য করে। এতে Asyncify-এরও সাপোর্ট রয়েছে, ফলে আপনি এক্সটার্নাল Promise এর উপর await() কল করতে পারেন এবং এটি async-await জাভাস্ক্রিপ্ট কোডের 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-এ খুব ভালোভাবে কাজ করে। অন্যান্য টুলচেইন এবং ভাষাগুলোর ক্ষেত্রে কী হবে?
অন্যান্য ভাষা থেকে ব্যবহার
ধরুন, আপনার রাস্ট কোডের কোথাও একটি অনুরূপ সিনক্রোনাস কল আছে, যেটিকে আপনি ওয়েবের কোনো অ্যাসিঙ্ক এপিআই-এর সাথে ম্যাপ করতে চান। দেখা যাচ্ছে, আপনি সেটাও করতে পারেন!
প্রথমে, আপনাকে extern ব্লকের মাধ্যমে (অথবা আপনার নির্বাচিত ভাষার ফরেন ফাংশনের সিনট্যাক্স ব্যবহার করে) একটি সাধারণ ইম্পোর্ট হিসেবে এই ধরনের একটি ফাংশন সংজ্ঞায়িত করতে হবে।
extern {
fn get_answer() -> i32;
}
println!("Getting answer...");
let answer = get_answer();
println!("Answer is {}", answer);
এবং আপনার কোডটি ওয়েবঅ্যাসেম্বলিতে কম্পাইল করুন:
cargo build --target wasm32-unknown-unknown
এখন আপনাকে স্ট্যাক সংরক্ষণ/পুনরুদ্ধারের কোড দিয়ে ওয়েবঅ্যাসেম্বলি ফাইলটি ইন্সট্রুমেন্ট করতে হবে। C / C++ এর ক্ষেত্রে Emscripten এই কাজটি করে দিত, কিন্তু এখানে তা ব্যবহার করা হচ্ছে না, তাই প্রক্রিয়াটি কিছুটা ম্যানুয়াল।
সৌভাগ্যবশত, Asyncify ট্রান্সফর্মটি নিজেই সম্পূর্ণরূপে টুলচেইন-নিরপেক্ষ। এটি যেকোনো WebAssembly ফাইলকে ট্রান্সফর্ম করতে পারে, ফাইলটি যে কম্পাইলার দ্বারাই তৈরি হোক না কেন। এই ট্রান্সফর্মটি Binaryen টুলচেইনের wasm-opt অপটিমাইজারের অংশ হিসেবে আলাদাভাবে সরবরাহ করা হয় এবং এটিকে এইভাবে চালু করা যায়:
wasm-opt -O2 --asyncify \
--pass-arg=asyncify-imports@env.get_answer \
[...]
ট্রান্সফর্মটি চালু করতে --asyncify পাস করুন, এবং তারপর --pass-arg=… ব্যবহার করে অ্যাসিঙ্ক্রোনাস ফাংশনগুলোর একটি কমা-বিভক্ত তালিকা প্রদান করুন, যেখানে প্রোগ্রামের অবস্থা স্থগিত করে পরে আবার চালু করা হবে।
এখন শুধু সহায়ক রানটাইম কোড সরবরাহ করা বাকি, যা আসলে এই কাজটি করবে—অর্থাৎ ওয়েবঅ্যাসেম্বলি কোড স্থগিত ও পুনরায় চালু করবে। আবারও বলছি, C / C++ এর ক্ষেত্রে এটি Emscripten-এর মধ্যেই অন্তর্ভুক্ত থাকত, কিন্তু এখন আপনার নিজস্ব জাভাস্ক্রিপ্ট গ্লু কোড প্রয়োজন যা যেকোনো ওয়েবঅ্যাসেম্বলি ফাইল পরিচালনা করতে পারবে। আমরা ঠিক এই কাজের জন্যই একটি লাইব্রেরি তৈরি করেছি।
আপনি এটি গিটহাবে https://github.com/GoogleChromeLabs/asyncify ঠিকানায় অথবা এনপিএম-এ asyncify-wasm নামে খুঁজে পেতে পারেন।
এটি একটি স্ট্যান্ডার্ড ওয়েবঅ্যাসেম্বলি ইনস্ট্যানসিয়েশন এপিআই-কে অনুকরণ করে, কিন্তু নিজস্ব নেমস্পেসের অধীনে। একমাত্র পার্থক্য হলো, একটি সাধারণ ওয়েবঅ্যাসেম্বলি এপিআই-এর অধীনে আপনি ইম্পোর্ট হিসেবে শুধুমাত্র সিনক্রোনাস ফাংশন সরবরাহ করতে পারেন, কিন্তু 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 শনাক্ত করবে, ওয়েবঅ্যাসেম্বলি অ্যাপ্লিকেশনটির স্টেট সাসপেন্ড ও সেভ করবে, প্রমিজটি সম্পন্ন হওয়ার জন্য সাবস্ক্রাইব করবে এবং পরে, এটি রিজলভ হয়ে গেলে, নির্বিঘ্নে কল স্ট্যাক ও স্টেট পুনরুদ্ধার করে এমনভাবে এক্সিকিউশন চালিয়ে যাবে যেন কিছুই ঘটেনি।
যেহেতু মডিউলের যেকোনো ফাংশন একটি অ্যাসিঙ্ক্রোনাস কল করতে পারে, তাই সমস্ত এক্সপোর্টও সম্ভাব্য অ্যাসিঙ্ক্রোনাস হয়ে যায়, ফলে সেগুলোকেও র্যাপ করা হয়। আপনি উপরের উদাহরণে হয়তো লক্ষ্য করেছেন যে, এক্সিকিউশন কখন সত্যিই শেষ হয়েছে তা জানার জন্য instance.exports.main() এর ফলাফলের জন্য await করতে হয়।
অভ্যন্তরীণভাবে এই সবকিছু কীভাবে কাজ করে?
যখন Asyncify, ASYNCIFY_IMPORTS ফাংশনগুলোর কোনো একটিতে কল শনাক্ত করে, তখন এটি একটি অ্যাসিঙ্ক্রোনাস অপারেশন শুরু করে, কল স্ট্যাক এবং যেকোনো টেম্পোরারি লোকাল ভেরিয়েবলসহ অ্যাপ্লিকেশনটির সম্পূর্ণ স্টেট সংরক্ষণ করে এবং পরে, সেই অপারেশনটি শেষ হলে, সমস্ত মেমোরি ও কল স্ট্যাক পুনরুদ্ধার করে এবং ঠিক সেই একই জায়গা ও স্টেট থেকে পুনরায় চালু হয়, যেন প্রোগ্রামটি কখনোই বন্ধ হয়নি।
এটি জাভাস্ক্রিপ্টের async-await ফিচারের মতোই, যা আমি আগে দেখিয়েছিলাম। কিন্তু জাভাস্ক্রিপ্টেরটির মতো এর জন্য কোনো বিশেষ সিনট্যাক্স বা ল্যাঙ্গুয়েজের রানটাইম সাপোর্টের প্রয়োজন হয় না, বরং এটি কম্পাইল-টাইমে সাধারণ সিনক্রোনাস ফাংশনগুলোকে রূপান্তর করে কাজ করে।
পূর্বে দেখানো অ্যাসিঙ্ক্রোনাস স্লিপ উদাহরণটি কম্পাইল করার সময়:
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 এ চলে যাবে এবং ফাংশনটিকে আবার কল করবে। এবার, 'normal execution' ব্রাঞ্চটি স্কিপ করা হয় — কারণ এটি গতবারই কাজটি করে ফেলেছে এবং আমি 'A' অক্ষরটি দুবার প্রিন্ট করা এড়াতে চাই — এবং এর পরিবর্তে এটি সরাসরি 'rewinding' ব্রাঞ্চে চলে আসে। সেখানে পৌঁছানোর পর, এটি সমস্ত স্টোর করা লোকাল ভেরিয়েবল পুনরুদ্ধার করে, মোড আবার 'normal'-এ পরিবর্তন করে এবং এক্সিকিউশন এমনভাবে চালিয়ে যায় যেন কোডটি কখনোই থামানো হয়নি।
রূপান্তর খরচ
দুর্ভাগ্যবশত, Asyncify transform সম্পূর্ণ বিনামূল্যে নয়, কারণ এটিকে বিভিন্ন লোকাল ভেরিয়েবল সংরক্ষণ ও পুনরুদ্ধার, বিভিন্ন মোডে কল স্ট্যাক নেভিগেট করা ইত্যাদির জন্য বেশ কিছু সহায়ক কোড যুক্ত করতে হয়। এটি শুধুমাত্র কমান্ড লাইনে অ্যাসিঙ্ক্রোনাস হিসেবে চিহ্নিত ফাংশন এবং তাদের সম্ভাব্য কলারদের পরিবর্তন করার চেষ্টা করে, কিন্তু কম্প্রেশনের আগে এই অতিরিক্ত কোড সাইজ প্রায় ৫০% পর্যন্ত বেড়ে যেতে পারে।

এটি আদর্শ নয়, কিন্তু অনেক ক্ষেত্রে গ্রহণযোগ্য, যখন এর বিকল্প হিসেবে কার্যকারিতাটি একেবারেই না থাকা অথবা মূল কোডে উল্লেখযোগ্য পরিবর্তন আনার প্রয়োজন হয়।
চূড়ান্ত বিল্ডের জন্য অপটিমাইজেশন সবসময় চালু রাখতে ভুলবেন না, যাতে এটি আরও বেড়ে না যায়। আপনি Asyncify-এর নির্দিষ্ট অপটিমাইজেশন অপশনগুলোও দেখতে পারেন, যা ট্রান্সফর্মকে শুধুমাত্র নির্দিষ্ট ফাংশন এবং/অথবা সরাসরি ফাংশন কলের মধ্যে সীমাবদ্ধ রেখে ওভারহেড কমাতে সাহায্য করে। এর ফলে রানটাইম পারফরম্যান্সেও সামান্য প্রভাব পড়ে, কিন্তু তা কেবল অ্যাসিঙ্ক কলগুলোর মধ্যেই সীমাবদ্ধ থাকে। তবে, প্রকৃত কাজের খরচের তুলনায় এটি সাধারণত নগণ্য।
বাস্তব জগতের ডেমো
এখন যেহেতু আপনারা সহজ উদাহরণগুলো দেখেছেন, আমি আরও জটিল পরিস্থিতিগুলোর দিকে এগিয়ে যাব।
প্রবন্ধের শুরুতে যেমন উল্লেখ করা হয়েছে, ওয়েবের স্টোরেজ বিকল্পগুলোর মধ্যে একটি হলো অ্যাসিঙ্ক্রোনাস ফাইল সিস্টেম অ্যাক্সেস এপিআই (Asynchronous File System Access API)। এটি একটি ওয়েব অ্যাপ্লিকেশন থেকে বাস্তব হোস্ট ফাইলসিস্টেমে অ্যাক্সেস প্রদান করে।
অন্যদিকে, কনসোল এবং সার্ভার-সাইডে ওয়েবঅ্যাসেম্বলি আই/ও-এর জন্য WASI নামে একটি কার্যত স্বীকৃত স্ট্যান্ডার্ড রয়েছে। এটি সিস্টেম ল্যাঙ্গুয়েজগুলোর জন্য একটি কম্পাইলেশন টার্গেট হিসেবে ডিজাইন করা হয়েছিল এবং এটি সব ধরনের ফাইল সিস্টেম ও অন্যান্য অপারেশনকে একটি প্রচলিত সিনক্রোনাস ফর্মে প্রকাশ করে।
কেমন হতো যদি আপনি একটিকে অন্যটির সাথে ম্যাপ করতে পারতেন? তাহলে আপনি WASI টার্গেট সমর্থনকারী যেকোনো টুলচেইন ব্যবহার করে যেকোনো সোর্স ল্যাঙ্গুয়েজে যেকোনো অ্যাপ্লিকেশন কম্পাইল করতে পারতেন এবং ওয়েবের একটি স্যান্ডবক্সে তা চালাতে পারতেন, আর একই সাথে সেটিকে আসল ইউজার ফাইলে কাজ করার সুযোগও দিতে পারতেন! Asyncify-এর মাধ্যমে আপনি ঠিক এটাই করতে পারেন।
এই ডেমোতে, আমি WASI-তে কয়েকটি ছোটখাটো প্যাচ যোগ করে Rust coreutils ক্রেটটি কম্পাইল করেছি, যা Asyncify ট্রান্সফর্মের মাধ্যমে পাস করা হয়েছে এবং জাভাস্ক্রিপ্ট সাইডে WASI থেকে ফাইল সিস্টেম অ্যাক্সেস এপিআই-তে অ্যাসিঙ্ক্রোনাস বাইন্ডিং ইমপ্লিমেন্ট করেছি। Xterm.js টার্মিনাল কম্পোনেন্টের সাথে যুক্ত হলে, এটি ব্রাউজার ট্যাবে একটি বাস্তবসম্মত শেল প্রদান করে যা ব্যবহারকারীর আসল ফাইলগুলিতে কাজ করে – ঠিক একটি আসল টার্মিনালের মতোই।
https://wasi.rreverser.com/ -এ সরাসরি দেখুন।
Asyncify-এর ব্যবহার শুধু টাইমার এবং ফাইলসিস্টেমের মধ্যেই সীমাবদ্ধ নয়। আপনি আরও এগিয়ে গিয়ে ওয়েবে থাকা আরও বিশেষায়িত API ব্যবহার করতে পারেন।
উদাহরণস্বরূপ, Asyncify-এর সাহায্যে libusb-কে —যা সম্ভবত USB ডিভাইস নিয়ে কাজ করার জন্য সবচেয়ে জনপ্রিয় নেটিভ লাইব্রেরি—একটি WebUSB API-এর সাথে ম্যাপ করা সম্ভব, যা ওয়েবে থাকা এই ধরনের ডিভাইসগুলোতে অ্যাসিঙ্ক্রোনাস অ্যাক্সেস দেয়। একবার ম্যাপ এবং কম্পাইল করা হয়ে গেলে, আমি একটি ওয়েব পেজের স্যান্ডবক্সের মধ্যেই নির্বাচিত ডিভাইসগুলোর ওপর স্ট্যান্ডার্ড libusb টেস্ট এবং উদাহরণগুলো চালাতে পেরেছিলাম।

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