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

कभी-कभी, आपको ऐसी लाइब्रेरी का इस्तेमाल करना होता है जो सिर्फ़ C या C++ कोड के तौर पर उपलब्ध होती है. आम तौर पर, लोग यहीं पर काम छोड़ देते हैं. अब नहीं, क्योंकि अब हमारे पास Emscripten और WebAssembly (या Wasm) हैं!

टूल चेन

मैंने खुद को कुछ मौजूदा C कोड को Wasm में इकट्ठा करने का तरीका पता करने का लक्ष्य तय किया है. LLVM के Wasm बैकएंड के आस-पास कुछ गड़बड़ी थी, इसलिए मैंने इस बारे में पता लगाना शुरू किया. इस तरीके से कंपाइल करने के लिए आसान प्रोग्राम हासिल किए जा सकते हैं. हालांकि, दूसरा तरीका यह है कि आप C की स्टैंडर्ड लाइब्रेरी का इस्तेमाल करें या कई फ़ाइलें कंपाइल करें. ऐसा करने पर, आपको कई समस्याएं आ सकती हैं. इससे मुझे एक बहुत बड़ी सीख मिली:

Emscripten, C-to-asm.js कंपाइलर के तौर पर इस्तेमाल किया गया है. इसके बाद, यह Wasm को टारगेट करने के लिए मैच्योर हो गया. साथ ही, यह अंदरूनी तौर पर आधिकारिक एलएलवीएम बैकएंड पर स्विच करने की प्रोसेस में है. Emscripten, C की स्टैंडर्ड लाइब्रेरी को भी Wasm के साथ काम करने वाला बनाता है. एमस्क्रिप्टन का इस्तेमाल करें. इसमें बहुत से छिपे हुए काम होते हैं, फ़ाइल सिस्टम की नकल की जाती है, मेमोरी मैनेज करने की सुविधा दी जाती है, और 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 ग्लोबल विकल्प होगा. ऐसा JavaScript नेटिव फ़ंक्शन बनाने के लिए, 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वां नंबर है.

द होली ग्रेल: कंपाइलिंग ए सी लाइब्रेरी

अब तक, हमने जो C कोड लिखा था वह Wasm को ध्यान में रखकर लिखा गया था. हालांकि, WebAssembly का मुख्य इस्तेमाल यह है कि मौजूदा लाइब्रेरी के नेटवर्क को डेवलपर ही वेब पर इस्तेमाल कर सकें. ये लाइब्रेरी अक्सर C की स्टैंडर्ड लाइब्रेरी, किसी ऑपरेटिंग सिस्टम, फ़ाइल सिस्टम, और दूसरी चीज़ों पर होती हैं. Emscripten इनमें से ज़्यादातर सुविधाएं उपलब्ध कराता है. हालांकि, इसमें कुछ सीमाएं भी हैं.

आइए अपने मूल लक्ष्य पर वापस जाएं: WebP को Wasm में बदलने के लिए, एन्कोडर को कंपाइल करना. WebP कोडेक का सोर्स, C में लिखा गया है. यह GitHub पर उपलब्ध है. साथ ही, एपीआई के बारे में ज़्यादा जानकारी देने वाले दस्तावेज़ भी उपलब्ध हैं. यह शुरुआत करने का एक अच्छा तरीका है.

    $ git clone https://github.com/webmproject/libwebp

आसान तरीके से शुरुआत करने के लिए, आइए webp.c नाम की एक C फ़ाइल लिखकर, WebPGetEncoderVersion() को encode.h से 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 land में इमेज के लिए मेमोरी को ऐलोकेट करता है और दूसरा फ़ंक्शन, उसे फिर से खाली करता है:

    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-लैंड बफ़र में डाल सकते हैं और इस प्रक्रिया में असाइन किए गए सभी 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 का नेटवर्क पैनल और जनरेट की गई इमेज.

नतीजा

ब्राउज़र में काम करने के लिए सी लाइब्रेरी मिल जाए, यह आसान नहीं है, लेकिन एक बार जब आप पूरी प्रोसेस और डेटा फ़्लो के काम करने के तरीके को समझ लेते हैं, तो यह और भी आसान हो जाता है और नतीजे हैरान कर देने वाले होते हैं.

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 केबी होता है. maximum विकल्प दिए बिना, मेमोरी की सीमा तय नहीं होती है. फ़िलहाल, Chrome में 2 जीबी की स्टोरेज इस्तेमाल की जा सकती है. ज़्यादातर WebAssembly मॉड्यूल के लिए, ज़्यादा से ज़्यादा वैल्यू सेट करने की ज़रूरत नहीं होती.
  • env.STACKTOP तय करता है कि स्टैक का बढ़ना कहां से शुरू होना चाहिए. फ़ंक्शन कॉल करने और लोकल वैरिएबल के लिए मेमोरी को ऐलोकेट करने के लिए, स्टैक की ज़रूरत होती है. हम अपने छोटे से फ़ाइबोनाची प्रोग्राम में, डाइनैमिक मेमोरी मैनेजमेंट के बारे में कोई जानकारी नहीं करते हैं. इसलिए, हम पूरी मेमोरी को सिर्फ़ स्टैक के तौर पर इस्तेमाल कर सकते हैं. इसलिए, STACKTOP = 0.