কখনও কখনও আপনি একটি লাইব্রেরি ব্যবহার করতে চান যা শুধুমাত্র C বা C++ কোড হিসাবে উপলব্ধ। ঐতিহ্যগতভাবে, এখানেই আপনি হাল ছেড়ে দেন। আচ্ছা, আর নয়, কারণ এখন আমাদের কাছে Emscripten এবং WebAssembly (বা Wasm) আছে!
টুলচেইন
আমি নিজেকে Wasm-এ কিছু বিদ্যমান সি কোড কম্পাইল করার জন্য কাজ করার লক্ষ্য নির্ধারণ করেছি। LLVM এর Wasm ব্যাকএন্ডের চারপাশে কিছু গোলমাল হয়েছে, তাই আমি এটিতে খনন শুরু করেছি। যদিও আপনি এইভাবে কম্পাইল করার জন্য সাধারণ প্রোগ্রাম পেতে পারেন , দ্বিতীয়বার আপনি C এর স্ট্যান্ডার্ড লাইব্রেরি ব্যবহার করতে চান বা এমনকি একাধিক ফাইল কম্পাইল করতে চান, আপনি সম্ভবত সমস্যায় পড়বেন। এটি আমাকে প্রধান পাঠের দিকে নিয়ে গেছে যা আমি শিখেছি:
যদিও Emscripten একটি C-to-asm.js কম্পাইলার ছিল , এটি তখন থেকে Wasm কে লক্ষ্য করার জন্য পরিপক্ক হয়েছে এবং অভ্যন্তরীণভাবে অফিসিয়াল LLVM ব্যাকএন্ডে স্যুইচ করার প্রক্রিয়াধীন রয়েছে। Emscripten C এর স্ট্যান্ডার্ড লাইব্রেরির একটি Wasm-সামঞ্জস্যপূর্ণ বাস্তবায়নও প্রদান করে। এমস্ক্রিপ্টেন ব্যবহার করুন । এটি অনেক লুকানো কাজ বহন করে , একটি ফাইল সিস্টেমকে অনুকরণ করে, মেমরি ব্যবস্থাপনা প্রদান করে, OpenGL কে WebGL-এর সাথে মোড়ানো — এমন অনেক কিছু যা আপনার নিজের জন্য বিকাশ করার অভিজ্ঞতার প্রয়োজন নেই৷
যদিও এটি মনে হতে পারে যে আপনাকে ফোলা নিয়ে চিন্তা করতে হবে — আমি অবশ্যই চিন্তিত — এমস্ক্রিপ্টেন কম্পাইলার প্রয়োজন নেই এমন সমস্ত কিছু সরিয়ে দেয়। আমার পরীক্ষা-নিরীক্ষায়, ফলস্বরূপ Wasm মডিউলগুলি যে যুক্তিতে রয়েছে তার জন্য যথাযথভাবে মাপ করা হয়েছে এবং Emscripten এবং WebAssembly টিম ভবিষ্যতে তাদের আরও ছোট করার জন্য কাজ করছে।
আপনি তাদের ওয়েবসাইটের নির্দেশাবলী অনুসরণ করে বা Homebrew ব্যবহার করে Emscripten পেতে পারেন। আপনি যদি আমার মতো ডকারাইজড কমান্ডের অনুরাগী হন এবং WebAssembly-এর সাথে একটি খেলার জন্য আপনার সিস্টেমে জিনিসগুলি ইনস্টল করতে না চান, তাহলে একটি ভাল-রক্ষণাবেক্ষণ করা ডকার ইমেজ রয়েছে যা আপনি পরিবর্তে ব্যবহার করতে পারেন:
$ docker pull trzeci/emscripten
$ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>
সহজ কিছু কম্পাইল করা
আসুন সি-তে একটি ফাংশন লেখার প্রায় প্রামাণিক উদাহরণ নেওয়া যাক যা n তম ফিবোনাচি সংখ্যা গণনা করে:
#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 জানেন না কিন্তু জাভাস্ক্রিপ্ট জানেন, আপনি আশা করি বুঝতে সক্ষম হবেন এখানে কি হচ্ছে।
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
আমাদের সি ফাইল। এই পর্যন্ত, তাই ভাল. -s WASM=1
Emscripten কে asm.js ফাইলের পরিবর্তে আমাদের একটি Wasm ফাইল দিতে বলে। -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'
কম্পাইলারকে জাভাস্ক্রিপ্ট ফাইলে উপলব্ধ cwrap()
ফাংশনটি ছেড়ে যেতে বলে — পরে এই ফাংশন সম্পর্কে আরও। -O3
কম্পাইলারকে আক্রমণাত্মকভাবে অপ্টিমাইজ করতে বলে। আপনি বিল্ড টাইম কমাতে কম সংখ্যা বেছে নিতে পারেন, তবে এটি ফলস্বরূপ বান্ডিলগুলিকে আরও বড় করে তুলবে কারণ কম্পাইলারটি অব্যবহৃত কোড মুছে ফেলতে পারে না।
কমান্ডটি চালানোর পরে, আপনার a.out.js
নামে একটি জাভাস্ক্রিপ্ট ফাইল এবং a.out.wasm
নামক একটি WebAssembly ফাইলের সাথে শেষ হওয়া উচিত। Wasm ফাইলে (বা "মডিউল") আমাদের সংকলিত সি কোড রয়েছে এবং এটি মোটামুটি ছোট হওয়া উচিত। JavaScript ফাইলটি আমাদের Wasm মডিউল লোড করা এবং শুরু করার এবং একটি সুন্দর API প্রদানের যত্ন নেয়। প্রয়োজন হলে, এটি স্ট্যাক, হিপ এবং অন্যান্য কার্যকারিতা সেট আপ করার যত্ন নেবে যা সাধারণত সি কোড লেখার সময় অপারেটিং সিস্টেম দ্বারা সরবরাহ করা হবে বলে প্রত্যাশিত৷ যেমন, জাভাস্ক্রিপ্ট ফাইলটি একটু বড়, যার ওজন 19KB (~5KB gzip'd)।
সহজ কিছু চালানো
আপনার মডিউল লোড এবং চালানোর সবচেয়ে সহজ উপায় হল জেনারেট করা জাভাস্ক্রিপ্ট ফাইল ব্যবহার করা। একবার আপনি সেই ফাইলটি লোড করলে, আপনার নিষ্পত্তিতে একটি 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" দেখতে হবে, যা 12তম ফিবোনাচি নম্বর।
পবিত্র গ্রেইল: একটি সি লাইব্রেরি সংকলন করা
এখন পর্যন্ত, আমরা যে সি কোডটি লিখেছি তা ওয়াসমকে মাথায় রেখে লেখা হয়েছিল। WebAssembly-এর জন্য একটি মূল ব্যবহারের ক্ষেত্রে, তবে, C লাইব্রেরির বিদ্যমান ইকোসিস্টেম গ্রহণ করা এবং বিকাশকারীদের ওয়েবে সেগুলি ব্যবহার করার অনুমতি দেওয়া। এই লাইব্রেরিগুলি প্রায়শই C-এর স্ট্যান্ডার্ড লাইব্রেরি, একটি অপারেটিং সিস্টেম, একটি ফাইল সিস্টেম এবং অন্যান্য জিনিসের উপর নির্ভর করে। Emscripten এই বৈশিষ্ট্যগুলির অধিকাংশ প্রদান করে, যদিও কিছু সীমাবদ্ধতা রয়েছে।
আসুন আমার আসল লক্ষ্যে ফিরে যাই: WebP থেকে Wasm-এর জন্য একটি এনকোডার কম্পাইল করা। WebP কোডেক এর উৎস C তে লেখা এবং GitHub- এর পাশাপাশি কিছু বিস্তৃত API ডকুমেন্টেশন পাওয়া যায়। যে একটি চমত্কার ভাল শুরু বিন্দু.
$ git clone https://github.com/webmproject/libwebp
সহজভাবে শুরু করার জন্য, আসুন webp.c
নামে একটি C ফাইল লিখে encode.h
থেকে JavaScript-এ WebPGetEncoderVersion()
প্রকাশ করার চেষ্টা করি:
#include "emscripten.h"
#include "src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE
int version() {
return WebPGetEncoderVersion();
}
আমরা কম্পাইল করার জন্য libwebp এর সোর্স কোড পেতে পারি কিনা তা পরীক্ষা করার জন্য এটি একটি ভাল সহজ প্রোগ্রাম, কারণ এই ফাংশনটি চালু করার জন্য আমাদের কোন প্যারামিটার বা জটিল ডেটা স্ট্রাকচারের প্রয়োজন নেই।
এই প্রোগ্রামটি কম্পাইল করার জন্য, আমাদের কম্পাইলারকে বলতে হবে যেখানে এটি -I
ফ্ল্যাগ ব্যবহার করে libwebp-এর হেডার ফাইলগুলি খুঁজে পেতে পারে এবং এটির প্রয়োজনে libwebp-এর সমস্ত 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 এবং JavaScript দরকার:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = async (_) => {
const api = {
version: Module.cwrap('version', 'number', []),
};
console.log(api.version());
};
</script>
এবং আমরা আউটপুটে সংশোধন সংস্করণ নম্বর দেখতে পাব:
জাভাস্ক্রিপ্ট থেকে Wasm এ একটি ছবি পান
এনকোডারের সংস্করণ নম্বর পাওয়া দুর্দান্ত এবং সব, কিন্তু একটি প্রকৃত চিত্র এনকোড করা আরও চিত্তাকর্ষক হবে, তাই না? চলুন, তাহলে.
আমাদের প্রথম প্রশ্নটির উত্তর দিতে হবে: আমরা কীভাবে ওয়াসম ল্যান্ডে ছবিটি পেতে পারি? libwebp-এর এনকোডিং API-এর দিকে তাকিয়ে, এটি আরজিবি, আরজিবিএ, বিজিআর বা বিজিআরএ-তে বাইটের একটি অ্যারে আশা করে। সৌভাগ্যবশত, ক্যানভাস এপিআই-এ getImageData()
রয়েছে, যা আমাদেরকে RGBA-তে চিত্র ডেটা সহ একটি Uint8ClampedArray দেয়:
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);
}
এখন এটি "শুধুমাত্র" জাভাস্ক্রিপ্ট ল্যান্ড থেকে ওয়াসম ল্যান্ডে ডেটা কপি করার বিষয়। এর জন্য, আমাদের দুটি অতিরিক্ত ফাংশন প্রকাশ করতে হবে। একটি যেটি ওয়াসম ল্যান্ডের ভিতরে চিত্রটির জন্য মেমরি বরাদ্দ করে এবং একটি যা এটিকে আবার মুক্ত করে:
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 ইমেজের জন্য একটি বাফার বরাদ্দ করে — তাই প্রতি পিক্সেল 4 বাইট। malloc()
দ্বারা ফিরে আসা পয়েন্টারটি সেই বাফারের প্রথম মেমরি সেলের ঠিকানা। যখন পয়েন্টারটি জাভাস্ক্রিপ্ট ল্যান্ডে ফেরত দেওয়া হয়, তখন এটি একটি সংখ্যা হিসাবে বিবেচিত হয়। cwrap
ব্যবহার করে জাভাস্ক্রিপ্টে ফাংশনটি প্রকাশ করার পরে, আমরা আমাদের বাফারের শুরু খুঁজে পেতে এবং চিত্র ডেটা অনুলিপি করতে সেই নম্বরটি ব্যবহার করতে পারি।
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);
গ্র্যান্ড ফিনালে: ছবিটি এনকোড করুন
ছবিটি এখন ওয়াসম ল্যান্ডে পাওয়া যাচ্ছে। ওয়েবপি এনকোডারকে তার কাজ করার জন্য কল করার সময় এসেছে! WebP ডকুমেন্টেশনের দিকে তাকিয়ে, WebPEncodeRGBA
একটি নিখুঁত ফিট বলে মনে হচ্ছে। ফাংশনটি ইনপুট ইমেজ এবং এর ডাইমেনশনের জন্য একটি পয়েন্টার নেয়, সেইসাথে 0 এবং 100 এর মধ্যে একটি মানের বিকল্প। এটি আমাদের জন্য একটি আউটপুট বাফারও বরাদ্দ করে, যেটি WebP ইমেজটি সম্পন্ন করার পরে আমাদের WebPFree()
ব্যবহার করে বিনামূল্যে করতে হবে। .
এনকোডিং অপারেশনের ফলাফল হল একটি আউটপুট বাফার এবং এর দৈর্ঘ্য। যেহেতু C-এর ফাংশনগুলিতে রিটার্ন টাইপ হিসাবে অ্যারে থাকতে পারে না (যদি না আমরা গতিশীলভাবে মেমরি বরাদ্দ করি), আমি একটি স্ট্যাটিক গ্লোবাল অ্যারে অবলম্বন করেছি। আমি জানি, C পরিষ্কার নয় (আসলে, এটি ওয়াসম পয়েন্টার 32 বিট প্রশস্ত হওয়ার উপর নির্ভর করে), তবে জিনিসগুলি সহজ রাখতে আমি মনে করি এটি একটি ন্যায্য শর্টকাট।
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);
দেখুন, একটি নতুন WebP ছবির মহিমা !
উপসংহার
ব্রাউজারে কাজ করার জন্য একটি সি লাইব্রেরি পেতে পার্কে হাঁটা নয়, তবে একবার আপনি সামগ্রিক প্রক্রিয়া এবং ডেটা প্রবাহ কীভাবে কাজ করে তা বুঝতে পারলে, এটি সহজ হয়ে যায় এবং ফলাফলগুলি মন ছুঁয়ে যেতে পারে৷
WebAssembly প্রক্রিয়াকরণ, নম্বর ক্রাঞ্চিং এবং গেমিংয়ের জন্য ওয়েবে অনেকগুলি নতুন সম্ভাবনা উন্মুক্ত করে৷ মনে রাখবেন যে Wasm একটি রূপালী বুলেট নয় যা সবকিছুতে প্রয়োগ করা উচিত, তবে আপনি যখন সেই বাধাগুলির মধ্যে একটিকে আঘাত করেন, তখন Wasm একটি অবিশ্বাস্যভাবে সহায়ক হাতিয়ার হতে পারে।
বোনাস বিষয়বস্তু: কঠিন উপায়ে সহজ কিছু চালানো
আপনি যদি জেনারেট করা জাভাস্ক্রিপ্ট ফাইলটি চেষ্টা করতে এবং এড়াতে চান তবে আপনি সক্ষম হতে পারেন। ফিবোনাচি উদাহরণে ফিরে যাওয়া যাক। এটি নিজেরাই লোড এবং চালানোর জন্য, আমরা নিম্নলিখিতগুলি করতে পারি:
<!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 মডিউল আমদানি বস্তুর ভিতরে সবকিছু অ্যাক্সেস করতে পারে, কিন্তু এর বাইরে আর কিছুই নয়। নিয়ম অনুসারে, এমস্ক্রিপ্টিং দ্বারা সংকলিত মডিউলগুলি লোডিং জাভাস্ক্রিপ্ট পরিবেশ থেকে কয়েকটি জিনিস আশা করে:
- প্রথমত,
env.memory
আছে। Wasm মডিউল বাইরের জগত সম্পর্কে অজানা তাই কথা বলার জন্য, তাই এটির সাথে কাজ করার জন্য কিছু মেমরি পেতে হবে।WebAssembly.Memory
লিখুন। এটি রৈখিক মেমরির একটি (ঐচ্ছিকভাবে বৃদ্ধিযোগ্য) অংশ উপস্থাপন করে। সাইজিং প্যারামিটারগুলি "ওয়েব অ্যাসেম্বলি পৃষ্ঠাগুলির ইউনিটগুলিতে" রয়েছে, যার অর্থ উপরের কোডটি মেমরির 1 পৃষ্ঠা বরাদ্দ করে, প্রতিটি পৃষ্ঠার আকার 64 KiB ।maximum
বিকল্প প্রদান না করে, মেমরি তাত্ত্বিকভাবে বৃদ্ধির সীমাহীন (Chrome বর্তমানে 2GB এর কঠোর সীমা রয়েছে)। বেশিরভাগ WebAssembly মডিউলের সর্বোচ্চ সেট করার প্রয়োজন নেই। -
env.STACKTOP
সংজ্ঞায়িত করে যেখানে স্ট্যাকটি বাড়তে শুরু করবে। ফাংশন কল করতে এবং স্থানীয় ভেরিয়েবলের জন্য মেমরি বরাদ্দ করতে স্ট্যাকের প্রয়োজন। যেহেতু আমরা আমাদের ছোট্ট ফিবোনাচি প্রোগ্রামে কোনো গতিশীল মেমরি ম্যানেজমেন্ট শেনানিগ্যান করি না, তাই আমরা পুরো মেমরিটিকে স্ট্যাক হিসাবে ব্যবহার করতে পারি, তাইSTACKTOP = 0
।