Wasm के लिए C लाइब्रेरी को एम्स्क्रिप्ट करना

कभी-कभी, आपको ऐसी लाइब्रेरी का इस्तेमाल करना होता है जो सिर्फ़ 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 का इस्तेमाल करें. अगर आप भी मेरे जैसे, Dockerized निर्देशों के प्रशंसक हैं और आपको सिर्फ़ WebAssembly का इस्तेमाल करने के लिए, अपने सिस्टम पर कुछ इंस्टॉल नहीं करना है, तो इसके बजाय एक अच्छी तरह से मैनेज की गई Docker इमेज का इस्तेमाल किया जा सकता है:

    $ 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

आसानी से शुरू करने के लिए, webp.c नाम की C फ़ाइल लिखकर, encode.h से WebPGetEncoderVersion() को JavaScript में एक्सपोर्ट करने की कोशिश करते हैं:

    #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

अब हमें अपने नए मॉड्यूल को लोड करने के लिए, सिर्फ़ कुछ एचटीएमएल और JavaScript की ज़रूरत है:

<script src="/a.out.js"></script>
<script>
  Module.onRuntimeInitialized = async (_) => {
    const api = {
      version: Module.cwrap('version', 'number', []),
    };
    console.log(api.version());
  };
</script>

साथ ही, हमें आउटपुट में, सही किए गए वर्शन का नंबर दिखेगा:

DevTools कंसोल का स्क्रीनशॉट, जिसमें वर्शन का सही नंबर दिख रहा हो.

JavaScript से Wasm में इमेज पाना

एन्कोडर का वर्शन नंबर पाना अच्छा है, लेकिन किसी असल इमेज को एन्कोड करना ज़्यादा बेहतर होगा, है न? ठीक है, चलिए ऐसा करते हैं.

हमें सबसे पहले यह सवाल पूछना होगा: हम इमेज को Wasm लैंड में कैसे लाते हैं? libwebp के एन्कोडिंग एपीआई को देखते हुए, यह 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, आरजीबीए इमेज के लिए बफ़र को असाइन करता है — इसलिए, हर पिक्सल के लिए चार बाइट. 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 land में उपलब्ध है. अब 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-land बफ़र में डाल सकते हैं. साथ ही, प्रोसेस में जोड़े गए सभी Wasm-land बफ़र को रिलीज़ कर सकते हैं.

    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, इनपुट और आउटपुट, दोनों इमेज को स्टोर करने के लिए ज़रूरत के मुताबिक मेमोरी नहीं बढ़ा पाता:

DevTools कंसोल का स्क्रीनशॉट, जिसमें गड़बड़ी दिख रही है.

अच्छी बात यह है कि इस समस्या का समाधान, गड़बड़ी के मैसेज में दिया गया है! इसके लिए, हमें सिर्फ़ अपने कंपाइलेशन कमांड में -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 इमेज की शान देखें!

DevTools का नेटवर्क पैनल और जनरेट की गई इमेज.

नतीजा

ब्राउज़र में 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 मॉड्यूल के पास काम करने के लिए कोई मेमोरी नहीं होती. हालांकि, उन्हें मेमोरी देने पर वे काम कर सकते हैं. imports ऑब्जेक्ट का इस्तेमाल करके, किसी भी वस्तु के साथ Wasm मॉड्यूल को उपलब्ध कराया जा सकता है. यह ऑब्जेक्ट, instantiateStreaming फ़ंक्शन का दूसरा पैरामीटर होता है. Wasm मॉड्यूल, इंपोर्ट ऑब्जेक्ट के अंदर मौजूद सभी चीज़ों को ऐक्सेस कर सकता है, लेकिन इसके बाहर मौजूद किसी भी चीज़ को ऐक्सेस नहीं कर सकता. Emscripting की मदद से कॉम्पाइल किए गए मॉड्यूल, लोड होने वाले JavaScript एनवायरमेंट से कुछ चीज़ें उम्मीद करते हैं:

  • सबसे पहले, env.memory है. Wasm मॉड्यूल को बाहरी दुनिया के बारे में पता नहीं होता. इसलिए, उसे काम करने के लिए कुछ मेमोरी चाहिए. WebAssembly.Memory डालें. यह लीनियर मेमोरी का एक हिस्सा होता है, जिसे ज़रूरत के हिसाब से बड़ा किया जा सकता है. साइज़ तय करने वाले पैरामीटर, "WebAssembly पेजों की यूनिट में" होते हैं. इसका मतलब है कि ऊपर दिया गया कोड, मेमोरी का एक पेज असाइन करता है. हर पेज का साइज़ 64 KiB होता है. maximum विकल्प दिए बिना, मेमोरी को सैद्धांतिक तौर पर बढ़ाया जा सकता है. फ़िलहाल, Chrome में 2 जीबी तक मेमोरी का इस्तेमाल किया जा सकता है. ज़्यादातर WebAssembly मॉड्यूल के लिए, ज़्यादा से ज़्यादा वैल्यू सेट करने की ज़रूरत नहीं होती.
  • env.STACKTOP से यह तय होता है कि स्टैक को कहां से बढ़ाना है. फ़ंक्शन कॉल करने और लोकल वैरिएबल के लिए मेमोरी को ऐलोकेट करने के लिए, स्टैक की ज़रूरत होती है. हम अपने छोटे फ़ाइबोनैची प्रोग्राम में, डाइनैमिक मेमोरी मैनेजमेंट की कोई शरारत नहीं करते. इसलिए, हम पूरी मेमोरी को स्टैक के तौर पर इस्तेमाल कर सकते हैं. इसलिए, STACKTOP = 0.