কখনও কখনও আপনি এমন একটি লাইব্রেরি ব্যবহার করতে চান যা শুধুমাত্র C বা C++ কোড হিসেবে উপলভ্য। সাধারণত, এই পরিস্থিতিতে আপনি হাল ছেড়ে দেন। এখন আর তা নয়, কারণ এখন আমাদের কাছে আছে Emscripten এবং WebAssembly (বা Wasm)!
টুলচেন
আমি নিজেকে লক্ষ্য দিয়েছি যে কীভাবে কিছু আগে থেকে থাকা C কোডকে Wasm-এ কম্পাইল করা যায়। LLVM-এর Wasm ব্যাকএন্ড নিয়ে কিছু আলোচনা হচ্ছিল, তাই আমি সেই বিষয়ে খোঁজখবর নেওয়া শুরু করি। যদিও আপনি কম্পাইল করার জন্য সহজ প্রোগ্রাম পেতে পারেন এইভাবে, আপনি C-এর স্ট্যান্ডার্ড লাইব্রেরি ব্যবহার করতে বা এমনকি একাধিক ফাইল কম্পাইল করতে চাইলে, আপনি সম্ভবত সমস্যার সম্মুখীন হবেন। এর থেকে আমি সবচেয়ে গুরুত্বপূর্ণ শিক্ষা পেয়েছি:
Emscripten আগে C-to-asm.js কম্পাইলার হিসেবে ব্যবহার করা হলেও, এটি এখন Wasm-কে টার্গেট করার জন্য উপযুক্ত হয়ে উঠেছে এবং অভ্যন্তরীণভাবে অফিসিয়াল LLVM ব্যাকএন্ডে সুইচ করার প্রক্রিয়া চলছে। এছাড়াও, Emscripten, C-এর স্ট্যান্ডার্ড লাইব্রেরির Wasm-এর সাথে মানানসই প্রয়োগ প্রদান করে। Emscripten ব্যবহার করুন। এটি অনেক লুকানো কাজ করে, ফাইল সিস্টেম এমুলেট করে, মেমোরি ম্যানেজমেন্ট প্রদান করে, WebGL-এর সাথে OpenGL র্যাপ করে — এমন অনেক কিছু যা ডেভেলপ করার সময় আপনাকে নিজে থেকে করতে হয় না।
যদিও এটি শুনে মনে হতে পারে যে আপনাকে ব্লট নিয়ে চিন্তা করতে হবে — আমি অবশ্যই চিন্তিত ছিলাম — Emscripten কম্পাইলার অপ্রয়োজনীয় সবকিছু সরিয়ে দেয়। আমার পরীক্ষায়, যে Wasm মডিউল তৈরি হয়েছে, সেগুলির সাইজ লজিকের জন্য উপযুক্ত এবং Emscripten ও WebAssembly টিম ভবিষ্যতে এগুলিকে আরও ছোট করার জন্য কাজ করছে।
আপনি Emscripten-এর ওয়েবসাইট-এ দেওয়া নির্দেশাবলী অনুসরণ করে অথবা Homebrew ব্যবহার করে Emscripten পেতে পারেন। আপনি যদি আমার মতো ডকারাইজড কমান্ডের অনুরাগী হন এবং WebAssembly-র সাথে খেলার জন্য আপনার সিস্টেমে জিনিসগুলি ইনস্টল করতে না চান, তাহলে একটি ভালভাবে রক্ষণাবেক্ষণ করা ডকার ইমেজ রয়েছে যা আপনি পরিবর্তে ব্যবহার করতে পারেন:
$ docker pull trzeci/emscripten
$ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>
সহজ কিছু কম্পাইল করা হচ্ছে
C-তে একটি ফাংশন লেখার ক্যানোনিকাল উদাহরণ নেওয়া যাক যা nth ফিবোনাচি সংখ্যা গণনা করে:
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int fib(int n) {
if(n <= 0){
return 0;
}
int i, t, a = 0, b = 1;
for (i = 1; i < n; i++) {
t = a + b;
a = b;
b = t;
}
return b;
}
আপনি C জানলে, ফাংশনটি খুব বেশি আশ্চর্যজনক হওয়া উচিত নয়। আপনি C না জানলেও, জাভাস্ক্রিপ্ট জানলে, আশা করি এখানে কী হচ্ছে তা বুঝতে পারবেন।
emscripten.h হল Emscripten-এর প্রদান করা একটি হেডার ফাইল। আমাদের শুধু এটি প্রয়োজন যাতে আমরা
EMSCRIPTEN_KEEPALIVE ম্যাক্রো অ্যাক্সেস করতে পারি, কিন্তু এটি
আরও অনেক বেশি কার্যকারিতা প্রদান করে।
এই ম্যাক্রো কম্পাইলারকে বলে যে কোনও ফাংশন অব্যবহৃত বলে মনে হলেও সেটি যেন সরানো না হয়। আমরা যদি সেই ম্যাক্রো বাদ দিতাম, তাহলে কম্পাইলার ফাংশনটি অপ্টিমাইজ করে দিত
— কারণ, কেউ এটি ব্যবহার করছে না।
আসুন, fib.c নামের ফাইলে এই সব সেভ করি। এটি .wasm ফাইলে পরিবর্তন করতে
আমাদের Emscripten-এর কম্পাইলার কমান্ড emcc ব্যবহার করতে হবে:
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c
এই কমান্ডটি বিশ্লেষণ করা যাক। emcc হল Emscripten-এর কম্পাইলার। fib.c হল আমাদের C
ফাইল। এখনও পর্যন্ত, সব ঠিক আছে। -s WASM=1 Emscripten-কে asm.js ফাইলের পরিবর্তে
Wasm ফাইল দিতে বলে।
-s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' কম্পাইলারকে জাভাস্ক্রিপ্ট ফাইলে
cwrap() ফাংশনটি ছেড়ে দিতে বলে — এই ফাংশন সম্পর্কে আরও পরে
জানানো হবে। -O3 কম্পাইলারকে আগ্রাসীভাবে অপ্টিমাইজ করতে বলে। বিল্ড টাইম কমাতে আপনি কম সংখ্যা বেছে নিতে পারেন, তবে এর ফলে বান্ডেলের সাইজ বড় হয়ে যাবে, কারণ কম্পাইলার হয়ত অব্যবহৃত কোড সরাবে না।
কমান্ডটি রান করার পরে, আপনি a.out.js নামের একটি জাভাস্ক্রিপ্ট ফাইল এবং a.out.wasm নামের একটি WebAssembly ফাইল পাবেন। Wasm ফাইলে (বা
"মডিউল") আমাদের কম্পাইল করা C কোড থাকে এবং এটি বেশ ছোট হওয়া উচিত। JavaScript ফাইলটি আমাদের Wasm মডিউল লোড ও ইনিশিয়ালাইজ করার এবং আরও ভালো API প্রদান করার দায়িত্ব নেয়। প্রয়োজন হলে, এটি স্ট্যাক, হিপ ও অন্যান্য কার্যকারিতা সেট-আপ করার দায়িত্বও নেবে, যা সাধারণত C কোড লেখার সময় অপারেটিং সিস্টেমের থেকে আশা করা হয়। সেই কারণে, জাভাস্ক্রিপ্ট ফাইলটি একটু
বড়, ১৯কেবি (~৫কেবি gzip'd) সাইজের।
সহজ কিছু চালানো হচ্ছে
আপনার মডিউল লোড ও রান করার সবচেয়ে সহজ উপায় হল জেনারেট করা JavaScript ফাইল
ব্যবহার করা। আপনি সেই ফাইল লোড করলে, আপনার কাছে একটি
Module গ্লোবাল
থাকবে। cwrap
ব্যবহার করে জাভাস্ক্রিপ্ট নেটিভ ফাংশন তৈরি করুন যা প্যারামিটারকে
C-ফ্রেন্ডলি কিছুতে কনভার্ট করে এবং র্যাপ করা ফাংশন ইনভোক করে। cwrap ফাংশনের নাম, রিটার্নের ধরন এবং আর্গুমেন্টের ধরনকে আর্গুমেন্ট হিসেবে নেয়, এই ক্রমে:
<script src="a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const fib = Module.cwrap('fib', 'number', ['number']);
console.log(fib(12));
};
</script>
তুমি যদি এই কোড রান করো, তাহলে কনসোলে "144" দেখতে পাবে, যা হল ১২তম ফিবোন্যাকি নম্বর।
The holy grail: Compiling a C library
এখনও পর্যন্ত, আমরা যে C কোড লিখেছি তা Wasm-এর কথা মাথায় রেখে লেখা হয়েছে। তবে, WebAssembly-এর একটি মূল ব্যবহারের ক্ষেত্র হল, C লাইব্রেরির বর্তমান ইকোসিস্টেম নেওয়া এবং ডেভেলপারদের সেগুলি ওয়েবে ব্যবহার করার অনুমতি দেওয়া। এই লাইব্রেরিগুলি প্রায়শই C-এর স্ট্যান্ডার্ড লাইব্রেরি, একটি অপারেটিং সিস্টেম, একটি ফাইল সিস্টেম এবং অন্যান্য জিনিসের উপর নির্ভর করে। Emscripten এইসব ফিচারের বেশিরভাগই প্রদান করে, যদিও কিছু সীমাবদ্ধতা আছে।
আমার আসল লক্ষ্যে ফিরে যাওয়া যাক: WebP থেকে Wasm-এর জন্য এনকোডার কম্পাইল করা। WebP কোডেক-এর সোর্স C-তে লেখা এবং GitHub-এ উপলভ্য, এর সাথে কিছু বিস্তারিত API ডকুমেন্টেশনও আছে। শুরু করার জন্য এটি বেশ ভালো জায়গা।
$ git clone https://github.com/webmproject/libwebp
সহজভাবে শুরু করতে, webp.c নামের একটি C ফাইল লিখে encode.h থেকে WebPGetEncoderVersion()-কে জাভাস্ক্রিপ্টে এক্সপোজ করার চেষ্টা করা যাক:
#include "emscripten.h"
#include "src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE
int version() {
return WebPGetEncoderVersion();
}
কম্পাইল করার জন্য libwebp-এর সোর্স কোড পাওয়া যাবে কিনা তা পরীক্ষা করার জন্য এটি একটি ভালো ও সহজ প্রোগ্রাম, কারণ এই ফাংশনটি চালু করার জন্য আমাদের কোনও প্যারামিটার বা জটিল ডেটা স্ট্রাকচারের প্রয়োজন নেই।
এই প্রোগ্রাম কম্পাইল করতে, আমাদের কম্পাইলারকে বলতে হবে যে এটি -I ফ্ল্যাগ ব্যবহার করে libwebp-এর হেডার ফাইলগুলি কোথায় খুঁজে পেতে পারে এবং libwebp-এর প্রয়োজনীয় সমস্ত C ফাইলও পাস করতে হবে। আমি সত্যি কথা বলছি: আমি খুঁজে পাওয়া সব C ফাইল দিয়ে দিয়েছি এবং কম্পাইলারের উপর ভরসা করেছি যাতে অপ্রয়োজনীয় সবকিছু বাদ দিয়ে দেওয়া হয়। মনে হল, এটা দারুণ কাজ করেছে!
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
webp.c \
libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c
এখন আমাদের চকচকে নতুন মডিউল লোড করার জন্য শুধু কিছু HTML এবং জাভাস্ক্রিপ্ট প্রয়োজন:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = async (_) => {
const api = {
version: Module.cwrap('version', 'number', []),
};
console.log(api.version());
};
</script>
এবং আমরা আউটপুটে সংশোধিত ভার্সন নম্বর দেখতে পাব:

জাভাস্ক্রিপ্ট থেকে Wasm-এ ছবি পান
এনকোডারের ভার্সন নম্বর পাওয়া খুবই ভালো, তবে আসল ছবি এনকোড করা আরও বেশি চিত্তাকর্ষক হবে, তাই না? তাহলে, সেটাই করা যাক।
আমাদের প্রথম যে প্রশ্নের উত্তর দিতে হবে তা হল: আমরা কীভাবে Wasm ল্যান্ডে ছবি পাব?
libwebp-এর এনকোডিং API
দেখলে বোঝা যায় যে এটি RGB, RGBA, BGR বা BGRA-তে
বাইটের অ্যারে আশা করে। ভাগ্যক্রমে, Canvas API-তে
getImageData() আছে,
যা আমাদের
Uint8ClampedArray দেয়
যেখানে RGBA-তে ইমেজ ডেটা থাকে:
async function loadImage(src) {
// Load image
const imgBlob = await fetch(src).then((resp) => resp.blob());
const img = await createImageBitmap(imgBlob);
// Make canvas same size as image
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// Draw image onto canvas
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
এখন "শুধু" JavaScript ল্যান্ড থেকে Wasm ল্যান্ডে ডেটা কপি করার ব্যাপার। এর জন্য, আমাদের দুটি অতিরিক্ত ফাংশন প্রকাশ করতে হবে। একটি Wasm ল্যান্ডের মধ্যে ছবির জন্য মেমরি বরাদ্দ করে এবং অন্যটি আবার এটি খালি করে:
EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int width, int height) {
return malloc(width * height * 4 * sizeof(uint8_t));
}
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t* p) {
free(p);
}
create_buffer RGBA ছবির জন্য বাফার বরাদ্দ করে — তাই পিক্সেল প্রতি ৪ বাইট।
malloc()-এর মাধ্যমে ফেরত দেওয়া পয়েন্টার হল সেই বাফারের প্রথম মেমরি সেলের
অ্যাড্রেস। পয়েন্টারটি JavaScript ল্যান্ডে ফেরত এলে, এটিকে শুধু একটি সংখ্যা হিসেবে
বিবেচনা করা হয়। cwrap ব্যবহার করে JavaScript-এ ফাংশনটি এক্সপোজ করার পরে, আমরা
সেই নম্বরটি ব্যবহার করে আমাদের বাফারের শুরু খুঁজে পেতে এবং ইমেজ ডেটা কপি করতে পারি।
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
const image = await loadImage('/image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
// ... call encoder ...
api.destroy_buffer(p);
গ্র্যান্ড ফিনালে: ছবি এনকোড করুন
ছবিটি এখন Wasm ল্যান্ডে উপলভ্য। WebP এনকোডারকে কল করার সময় হয়ে গেছে
যাতে এটি কাজ করতে পারে! WebP ডকুমেন্টেশন
দেখে মনে হচ্ছে, WebPEncodeRGBA
একদম ঠিকঠাক হবে। ফাংশনটি ইনপুট ইমেজ এবং এর ডাইমেনশনের একটি পয়েন্টার নেয়, পাশাপাশি ০ থেকে ১০০-এর মধ্যে একটি কোয়ালিটি বিকল্প নেয়। এটি আমাদের জন্য একটি আউটপুট বাফারও বরাদ্দ করে, যা WebP ইমেজ ব্যবহার করা হয়ে গেলে WebPFree() ব্যবহার করে আমাদের খালি করতে হবে।
এনকোডিং অপারেশনের ফলাফল হল একটি আউটপুট বাফার এবং এর দৈর্ঘ্য। C-তে ফাংশনের রিটার্ন টাইপ হিসেবে অ্যারে থাকতে পারে না (যদি না আমরা ডাইনামিক্যালি মেমরি অ্যালোকেট করি), তাই আমি একটি স্ট্যাটিক গ্লোবাল অ্যারে ব্যবহার করেছি। আমি জানি, C পরিষ্কার নয় (আসলে, এটি Wasm পয়েন্টার যে ৩২বিট চওড়া তার উপর নির্ভর করে), তবে জিনিসগুলি সহজ রাখতে আমি মনে করি এটি একটি ন্যায্য শর্টকাট।
int result[2];
EMSCRIPTEN_KEEPALIVE
void encode(uint8_t* img_in, int width, int height, float quality) {
uint8_t* img_out;
size_t size;
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
result[0] = (int)img_out;
result[1] = size;
}
EMSCRIPTEN_KEEPALIVE
void free_result(uint8_t* result) {
WebPFree(result);
}
EMSCRIPTEN_KEEPALIVE
int get_result_pointer() {
return result[0];
}
EMSCRIPTEN_KEEPALIVE
int get_result_size() {
return result[1];
}
এখন এই সব কিছু ঠিকঠাক হয়ে গেলে, আমরা এনকোডিং ফাংশন কল করতে পারি, পয়েন্টার ও ছবির সাইজ নিতে পারি, আমাদের নিজস্ব জাভাস্ক্রিপ্ট-ল্যান্ড বাফারে এটি রাখতে পারি এবং প্রসেসে আমাদের বরাদ্দ করা সব Wasm-ল্যান্ড বাফার রিলিজ করতে পারি।
api.encode(p, image.width, image.height, 100);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
api.free_result(resultPointer);
আপনার ছবির সাইজের উপর নির্ভর করে, আপনি এমন একটি সমস্যার সম্মুখীন হতে পারেন যেখানে Wasm ইনপুট এবং আউটপুট উভয় ছবিই মানানসই করার জন্য মেমরি যথেষ্ট বাড়াতে পারে না:

ভাগ্যক্রমে, এই সমস্যার সমাধান ত্রুটি মেসেজেই আছে! আমাদের শুধু কম্পাইলেশন কমান্ডে
-s ALLOW_MEMORY_GROWTH=1 যোগ করতে হবে।
আর এই হল আপনার উত্তর! আমরা একটি WebP এনকোডার কম্পাইল করেছি এবং একটি JPEG ছবিকে WebP-তে ট্রান্সকোড করেছি। এটি যে কাজ করেছে তা প্রমাণ করতে, আমরা আমাদের ফলাফল বাফারকে একটি ব্লবে পরিণত করতে পারি এবং
এটি একটি <img> এলিমেন্টে ব্যবহার করতে পারি:
const blob = new Blob([result], { type: 'image/webp' });
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img);

উপসংহার
ব্রাউজারে C লাইব্রেরি কাজ করানো সহজ নয়, তবে আপনি একবার সামগ্রিক প্রসেস এবং ডেটা ফ্লো কীভাবে কাজ করে তা বুঝে গেলে, এটি সহজ হয়ে যায় এবং ফলাফলও অসাধারণ হতে পারে।
WebAssembly, প্রসেসিং, সংখ্যা ক্রাঞ্চিং এবং গেমিংয়ের জন্য ওয়েবে অনেক নতুন সম্ভাবনা খুলে দেয়। মনে রাখবেন যে Wasm কোনও ম্যাজিক নয় যে সব কিছুতেই প্রয়োগ করা যাবে, তবে আপনি যখন কোনও সমস্যার সম্মুখীন হবেন, তখন Wasm একটি অবিশ্বাস্যভাবে সহায়ক টুল হতে পারে।
বোনাস কন্টেন্ট: কঠিন উপায়ে সহজ কিছু চালানো
আপনি যদি তৈরি করা JavaScript ফাইল এড়াতে চান, তাহলে আপনি তা করতে পারবেন। ফিবোনাচি উদাহরণে ফিরে যাওয়া যাক। এটি লোড ও রান করতে, আমরা নিম্নলিখিত কাজগুলি করতে পারি:
<!DOCTYPE html>
<script>
(async function () {
const imports = {
env: {
memory: new WebAssembly.Memory({ initial: 1 }),
STACKTOP: 0,
},
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/a.out.wasm'),
imports,
);
console.log(instance.exports._fib(12));
})();
</script>
Emscripten-এর মাধ্যমে তৈরি করা WebAssembly মডিউলে কাজ করার জন্য কোনও মেমরি থাকে না,
যদি না আপনি সেটিকে মেমরি প্রদান করেন। Wasm মডিউলে কিছু প্রদান করার উপায় হল imports অবজেক্ট ব্যবহার করা — এটি instantiateStreaming ফাংশনের দ্বিতীয় প্যারামিটার। Wasm মডিউল ইমপোর্ট অবজেক্টের মধ্যে থাকা সবকিছু অ্যাক্সেস করতে পারে, তবে এর বাইরে আর কিছুই নয়। প্রথাগতভাবে, Emscripting দ্বারা কম্পাইল করা মডিউল, লোড করা জাভাস্ক্রিপ্ট এনভায়রনমেন্ট থেকে কয়েকটি জিনিস আশা করে:
- প্রথমত,
env.memoryআছে। Wasm মডিউল বাইরের দুনিয়া সম্পর্কে কিছুই জানে না, তাই কাজ করার জন্য এর কিছু মেমরির প্রয়োজন হয়।WebAssembly.Memoryলিখুন। এটি লিনিয়ার মেমরির একটি অংশ (ঐচ্ছিকভাবে বাড়ানো যায়) দেখায়। সাইজ সংক্রান্ত প্যারামিটার "WebAssembly পৃষ্ঠার ইউনিট" হিসেবে দেখানো হয়, অর্থাৎ উপরের কোডটি মেমরির ১টি পৃষ্ঠা বরাদ্দ করে, যার প্রতিটি পৃষ্ঠার সাইজ হল ৬৪ KiB।maximumবিকল্প প্রদান না করলে, মেমরি তাত্ত্বিকভাবে সীমাহীনভাবে বৃদ্ধি পায় (Chrome-এ বর্তমানে ২ জিবি হার্ড লিমিট আছে)। বেশিরভাগ WebAssembly মডিউলের ক্ষেত্রে সর্বাধিক সীমা সেট করার প্রয়োজন হয় না। env.STACKTOPথেকে বোঝা যায় যে স্ট্যাক কোথা থেকে শুরু হওয়ার কথা। ফাংশন কল করতে এবং লোকাল ভেরিয়েবলের জন্য মেমরি বরাদ্দ করতে স্ট্যাকের প্রয়োজন হয়। যেহেতু আমরা আমাদের ছোট ফিবোনাচি প্রোগ্রামে কোনও ডাইনামিক মেমোরি ম্যানেজমেন্ট শেনানিগান করি না, তাই আমরা পুরো মেমোরিটিকে একটি স্ট্যাক হিসাবে ব্যবহার করতে পারি, তাইSTACKTOP = 0।