कभी-कभी आपको ऐसी लाइब्रेरी का इस्तेमाल करना होता है जो सिर्फ़ C या C++ कोड के तौर पर उपलब्ध होती है. आम तौर पर, इस स्टेज पर लोग हार मान लेते हैं. अब ऐसा नहीं है, क्योंकि अब हमारे पास Emscripten और WebAssembly (या Wasm) है!
टूलचेन
मैंने खुद के लिए, कुछ मौजूदा C कोड को Wasm में कंपाइल करने का तरीका जानने का लक्ष्य तय किया. LLVM के Wasm बैकएंड को लेकर काफ़ी चर्चा हो रही थी, इसलिए मैंने इस बारे में ज़्यादा जानकारी इकट्ठा करना शुरू किया. हालांकि, इस तरीके से आसान प्रोग्राम कंपाइल किए जा सकते हैं, लेकिन C की स्टैंडर्ड लाइब्रेरी का इस्तेमाल करने या कई फ़ाइलों को कंपाइल करने पर, आपको समस्याएं आ सकती हैं. इससे मुझे एक बहुत बड़ी सीख मिली:
Emscripten का इस्तेमाल C-to-asm.js कंपाइलर के तौर पर किया जाता था. हालांकि, अब यह Wasm को टारगेट करने के लिए तैयार हो गया है. साथ ही, यह आधिकारिक LLVM बैकएंड पर स्विच करने की प्रोसेस में है. Emscripten, C की स्टैंडर्ड लाइब्रेरी को Wasm के साथ काम करने लायक भी बनाता है. Emscripten का इस्तेमाल करें. यह कई छिपे हुए काम करता है, फ़ाइल सिस्टम को एम्युलेट करता है, मेमोरी मैनेज करता है, OpenGL को WebGL के साथ रैप करता है — ऐसी कई चीज़ें हैं जिन्हें आपको खुद डेवलप करने की ज़रूरत नहीं है.
ऐसा लग सकता है कि आपको ब्लोट की चिंता करनी होगी. मुझे भी चिंता हुई थी. हालांकि, Emscripten कंपाइलर उन सभी चीज़ों को हटा देता है जिनकी ज़रूरत नहीं होती. मेरे प्रयोगों में, नतीजे के तौर पर मिले Wasm मॉड्यूल का साइज़, उनमें मौजूद लॉजिक के हिसाब से सही होता है. साथ ही, Emscripten और WebAssembly की टीमें, आने वाले समय में इन्हें और छोटा बनाने पर काम कर रही हैं.
Emscripten को डाउनलोड करने के लिए, उनकी वेबसाइट पर दिए गए निर्देशों का पालन करें या Homebrew का इस्तेमाल करें. अगर आपको मेरी तरह डॉकर वाले निर्देश पसंद हैं और आपको 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 भाषा नहीं आती, लेकिन JavaScript आती है, तो उम्मीद है कि आपको समझ आ जाएगा कि यहां क्या हो रहा है.
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"]', कंपाइलर को JavaScript फ़ाइल में cwrap() फ़ंक्शन को उपलब्ध रखने के लिए कहता है. इस फ़ंक्शन के बारे में ज़्यादा जानकारी बाद में दी जाएगी. -O3, कंपाइलर को आक्रामक तरीके से ऑप्टिमाइज़ करने के लिए कहता है. बिल्ड टाइम कम करने के लिए, कम संख्याएं चुनी जा सकती हैं. हालांकि, इससे बंडल का साइज़ बड़ा हो जाएगा, क्योंकि हो सकता है कि कंपाइलर, इस्तेमाल न किए गए कोड को न हटाए.
कमांड चलाने के बाद, आपको a.out.js नाम की JavaScript फ़ाइल और a.out.wasm नाम की WebAssembly फ़ाइल मिलनी चाहिए. Wasm फ़ाइल (या "मॉड्यूल") में हमारा कंपाइल किया गया C कोड होता है. यह फ़ाइल काफ़ी छोटी होनी चाहिए. JavaScript फ़ाइल, हमारे Wasm मॉड्यूल को लोड और शुरू करने के साथ-साथ बेहतर एपीआई उपलब्ध कराने का काम करती है. ज़रूरत पड़ने पर, यह स्टैक, हीप, और अन्य फ़ंक्शन को सेट अप करने का भी ध्यान रखेगा. आम तौर पर, C कोड लिखते समय ऑपरेटिंग सिस्टम से इन फ़ंक्शन की उम्मीद की जाती है. इसलिए, JavaScript फ़ाइल का साइज़ थोड़ा बड़ा होता है. यह 19 केबी (~5 केबी gzip'd) की होती है.
कोई आसान टास्क पूरा करना
अपने मॉड्यूल को लोड और रन करने का सबसे आसान तरीका है, जनरेट की गई JavaScript फ़ाइल का इस्तेमाल करना. उस फ़ाइल को लोड करने के बाद, आपके पास एक
Module ग्लोबल
ऑब्जेक्ट होगा. cwrap
का इस्तेमाल करके, JavaScript नेटिव फ़ंक्शन बनाएं. यह फ़ंक्शन, पैरामीटर को 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वीं फ़िबोनाची संख्या है.
सबसे अहम काम: C लाइब्रेरी को कंपाइल करना
अब तक, हमने जो C कोड लिखा है वह Wasm को ध्यान में रखकर लिखा गया है. हालांकि, WebAssembly का एक मुख्य इस्तेमाल, C लाइब्रेरी के मौजूदा नेटवर्क को लेना और डेवलपर को वेब पर उनका इस्तेमाल करने की अनुमति देना है. ये लाइब्रेरी अक्सर C की स्टैंडर्ड लाइब्रेरी, ऑपरेटिंग सिस्टम, फ़ाइल सिस्टम, और अन्य चीज़ों पर निर्भर करती हैं. Emscripten इनमें से ज़्यादातर सुविधाएं उपलब्ध कराता है. हालांकि, इसकी कुछ सीमाएं हैं.
चलिए, अपने मूल लक्ष्य पर वापस चलते हैं: WebP से Wasm के लिए एन्कोडर को कंपाइल करना. WebP कोडेक का सोर्स C में लिखा गया है. यह GitHub पर उपलब्ध है. साथ ही, इसके बारे में ज़्यादा जानकारी देने वाला एपीआई दस्तावेज़ भी उपलब्ध है. यह शुरुआत करने का एक अच्छा तरीका है.
$ git clone https://github.com/webmproject/libwebp
इसे आसान बनाने के लिए, आइए encode.h से WebPGetEncoderVersion() को JavaScript में एक्सपोज़ करने की कोशिश करें. इसके लिए, webp.c नाम की C फ़ाइल लिखें:
#include "emscripten.h"
#include "src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE
int version() {
return WebPGetEncoderVersion();
}
यह एक अच्छा और आसान प्रोग्राम है. इससे यह टेस्ट किया जा सकता है कि libwebp के सोर्स कोड को कंपाइल किया जा सकता है या नहीं. ऐसा इसलिए, क्योंकि इस फ़ंक्शन को शुरू करने के लिए, हमें किसी पैरामीटर या मुश्किल डेटा स्ट्रक्चर की ज़रूरत नहीं होती.
इस प्रोग्राम को कंपाइल करने के लिए, हमें कंपाइलर को बताना होगा कि उसे libwebp की हेडर फ़ाइलें कहां मिल सकती हैं. इसके लिए, -I फ़्लैग का इस्तेमाल करें. साथ ही, 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
अब हमें अपने नए मॉड्यूल को लोड करने के लिए, सिर्फ़ कुछ एचटीएमएल और JavaScript की ज़रूरत है:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = async (_) => {
const api = {
version: Module.cwrap('version', 'number', []),
};
console.log(api.version());
};
</script>
इसके बाद, हमें आउटपुट में सही वर्शन नंबर दिखेगा:

JavaScript से Wasm में इमेज पाना
एन्कोडर का वर्शन नंबर पाना अच्छी बात है, लेकिन किसी इमेज को एन्कोड करना ज़्यादा बेहतर होगा, है न? चलिए, फिर ऐसा ही करते हैं.
सबसे पहले हमें इस सवाल का जवाब देना होगा: हम इमेज को Wasm लैंड में कैसे लाएं?
libwebp के एन्कोडिंग एपीआई को देखने पर पता चलता है कि इसे आरजीबी, आरजीबीए, बीजीआर या बीजीआरए में बाइट के कलेक्शन की ज़रूरत होती है. अच्छी बात यह है कि 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, आरजीबीए इमेज के लिए बफ़र तय करता है. इसलिए, हर पिक्सल के लिए चार बाइट तय होते हैं.
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 सबसे सही फ़ॉर्मैट लगता है. यह फ़ंक्शन, इनपुट इमेज और उसके डाइमेंशन के लिए एक पॉइंटर लेता है. साथ ही, 0 से 100 के बीच क्वालिटी का विकल्प लेता है. यह हमारे लिए एक आउटपुट बफ़र भी तय करता है. WebP इमेज का इस्तेमाल करने के बाद, हमें WebPFree() का इस्तेमाल करके इसे खाली करना होगा.
एन्कोडिंग की प्रोसेस के नतीजे के तौर पर, आउटपुट बफ़र और उसकी लंबाई मिलती है. C में फ़ंक्शन, रिटर्न टाइप के तौर पर कलेक्शन नहीं रख सकते. ऐसा तब तक होता है, जब तक हम डाइनैमिक तौर पर मेमोरी असाइन नहीं करते. इसलिए, मैंने स्टैटिक ग्लोबल कलेक्शन का इस्तेमाल किया. मुझे पता है कि यह क्लीन C नहीं है. असल में, यह इस बात पर निर्भर करता है कि Wasm पॉइंटर 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];
}
अब इन सभी चीज़ों के साथ, हम एन्कोडिंग फ़ंक्शन को कॉल कर सकते हैं. साथ ही, पॉइंटर और इमेज का साइज़ लेकर, उसे अपने JavaScript-लैंड बफ़र में डाल सकते हैं. इसके अलावा, इस प्रोसेस में असाइन किए गए सभी 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 से कंपाइल किए गए मॉड्यूल, JavaScript एनवायरमेंट को लोड करने से कुछ चीज़ों की उम्मीद करते हैं:
- सबसे पहले,
env.memoryहै. Wasm मॉड्यूल को बाहरी दुनिया के बारे में कुछ नहीं पता होता. इसलिए, इसे काम करने के लिए कुछ मेमोरी की ज़रूरत होती है.WebAssembly.Memoryडालें. यह लीनियर मेमोरी के एक हिस्से को दिखाता है. इसे ज़रूरत के हिसाब से बढ़ाया जा सकता है. साइज़ तय करने वाले पैरामीटर, "WebAssembly पेजों की यूनिट" में होते हैं. इसका मतलब है कि ऊपर दिया गया कोड, मेमोरी का एक पेज असाइन करता है. हर पेज का साइज़ 64 KiB होता है.maximumविकल्प दिए बिना, मेमोरी को सैद्धांतिक तौर पर अनलिमिटेड बढ़ाया जा सकता है. फ़िलहाल, Chrome में मेमोरी की सीमा 2 जीबी है. ज़्यादातर WebAssembly मॉड्यूल के लिए, ज़्यादा से ज़्यादा साइज़ सेट करने की ज़रूरत नहीं होती. env.STACKTOPसे यह तय होता है कि स्टैक कहां से बढ़ना शुरू होगा. फ़ंक्शन कॉल करने और लोकल वैरिएबल के लिए मेमोरी असाइन करने के लिए, स्टैक की ज़रूरत होती है. हम अपने छोटे से फ़िबोनाची प्रोग्राम में, डाइनैमिक मेमोरी मैनेजमेंट से जुड़ी कोई भी गड़बड़ी नहीं करते हैं. इसलिए, हम पूरी मेमोरी को स्टैक के तौर पर इस्तेमाल कर सकते हैं. इसलिए,STACKTOP = 0.