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

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

टूलचेन

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

Emscripten को C-to-asm.js कंपाइलर के तौर पर इस्तेमाल किया गया था. हालांकि, अब यह Wasm को टारगेट करने के लिए परिपक्व हो गया है और अंदरूनी तौर पर आधिकारिक LLVM बैकएंड पर स्विच करने की प्रोसेस में है. Emscripten, C की स्टैंडर्ड लाइब्रेरी को Wasm के साथ काम करने लायक बनाने की सुविधा भी देता है. Emscripten का इस्तेमाल करें. इसमें बहुत सारा छिपा हुआ काम होता है, फ़ाइल सिस्टम को एम्युलेट किया जाता है, मेमोरी मैनेजमेंट की सुविधा देता है, और WebGL के साथ OpenGL को रैप करता है. इसमें ऐसी कई चीज़ें हैं जिनकी ज़रूरत आपको नहीं है.

हालांकि, ऐसा लग सकता है कि आपको पेट फूलने की चिंता करने की ज़रूरत है, लेकिन मुझे चिंता थी कि Emscripten कंपाइलर हर वह चीज़ हटा देता है जिसकी ज़रूरत नहीं है. मेरे प्रयोगों में, नतीजे में मिलने वाले Wasm मॉड्यूल को उनके लॉजिक के हिसाब से साइज़ किया गया है. साथ ही, Emscripten और WebAssembly टीम इन्हें आने वाले समय में और छोटा बनाने पर काम कर रही हैं.

Emscripten की वेबसाइट पर दिए गए निर्देशों का पालन करके या Homebrew का इस्तेमाल करके, इसे पाया जा सकता है. अगर आपको मेरी तरह डॉक्यूमेंट्री वाले निर्देश पसंद हैं और सिर्फ़ WebAssembly के साथ दिखाने के लिए, आपको अपने सिस्टम पर चीज़ें इंस्टॉल नहीं करनी हैं, तो एक अच्छी तरह से व्यवस्थित Docker इमेज मौजूद है. इसके बजाय, आप इनका इस्तेमाल कर सकते हैं:

    $ docker pull trzeci/emscripten
    $ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>

किसी आसान तरीके को कंपाइल करना

चलिए, C में एक ऐसे फ़ंक्शन को लिखने का सबसे कैननिकल उदाहरण लेते हैं जो नवें फ़ाइबोनाशी नंबर की गणना करता है:

    #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 के बारे में नहीं जानते, लेकिन 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 हमारी सी फ़ाइल है. अब तक, बहुत बढ़िया. -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 फ़ाइल (या "मॉड्यूल") में हमारा कंपाइल किया गया सी कोड मौजूद है और यह काफ़ी छोटा होना चाहिए. JavaScript फ़ाइल हमारे Wasm मॉड्यूल को लोड करने और शुरू करने का काम करती है. साथ ही, यह ज़्यादा अच्छे एपीआई उपलब्ध कराती है. ज़रूरत पड़ने पर, यह स्टैक, हीप, और ऐसे ही दूसरे फ़ंक्शन को भी सेट अप करेगा आम तौर पर, सी कोड लिखते समय ऑपरेटिंग सिस्टम से इसकी उम्मीद की जाती है. वैसे, JavaScript फ़ाइल थोड़ी बड़ी होती है, जिसका वज़न 19 केबी (~5 केबी gzip'd) होता है.

कुछ आसान चलाना

जनरेट की गई JavaScript फ़ाइल का इस्तेमाल करना, मॉड्यूल को लोड करने और चलाने का सबसे आसान तरीका है. उस फ़ाइल को लोड करने के बाद, आपके पास Module ग्लोबल होगा. cwrap का इस्तेमाल करके, JavaScript का नेटिव फ़ंक्शन बनाएं. यह फ़ंक्शन पैरामीटर को सी-फ़्रेंडली बनाने और रैप किए गए फ़ंक्शन को शुरू करने का काम करता है. cwrap, फ़ंक्शन का नाम, रिटर्न टाइप, और आर्ग्युमेंट के टाइप को आर्ग्युमेंट के तौर पर इसी क्रम में लेता है:

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

अगर इस कोड को चलाया जाता है, तो आपको कंसोल में "144" दिखेगा, जो कि 12वां फ़िबोनाशी नंबर है.

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

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

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

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

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

    #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

अब हमें अपने नए और शानदार मॉड्यूल को लोड करने के लिए, सिर्फ़ कुछ एचटीएमएल और 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 का एन्कोडर एपीआई देखें, तो हमें उम्मीद है कि आरजीबी, आरजीबीए, BGR या BGRA में बाइट की एक रेंज मौजूद होगी. अच्छी बात यह है कि कैनवस एपीआई में getImageData() है, इससे हमें एक ऐसा 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);
}

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

नतीजा

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

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