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

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

टूलचेन

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

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

ऐसा लग सकता है कि आपको अपना पेट फूलने की चिंता है — मुझे यकीनन इसकी चिंता है — एमस्क्रिप्टन कंपाइलर हर वह चीज़ हटा देता है जिसकी ज़रूरत नहीं है. मेरे इस एक्सपेरिमेंट के तहत, 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 फ़ाइल में बदलने के लिए हम एम्स्क्रिप्टन के कंपाइलर कमांड emcc पर जाना होगा:

    $ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c

चलिए, इस निर्देश को समझने की कोशिश करते हैं. emcc, एंस्क्रिप्टेन का कंपाइलर है. fib.c हमारा C है फ़ाइल से लिए जाते हैं. अभी तक, बहुत बढ़िया. -s WASM=1, Emscripten को एक Wasm फ़ाइल देने के लिए कहता है के बजाय asm.js फ़ाइल का इस्तेमाल करें. -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]', कंपाइलर को JavaScript फ़ाइल में cwrap() फ़ंक्शन उपलब्ध है — इस फ़ंक्शन के बारे में ज़्यादा जानकारी बाद में. -O3, कंपाइलर को एग्रेसिव तरीके से ऑप्टिमाइज़ करने के लिए कहता है. आप इसे कम कर सकते हैं निर्माण समय को कम करने के लिए संख्याओं का उपयोग करते हैं, लेकिन इसकी वजह से बनने वाले बंडल भी बड़ा होने पर, हो सकता है कि कंपाइलर इस्तेमाल न किया गया कोड न हटाए.

आदेश चलाने के बाद, आपको आखिर में a.out.js और a.out.wasm नाम की WebAssembly फ़ाइल. Wasm फ़ाइल (या "मॉड्यूल") में हमारा कंपाइल किया गया C कोड शामिल है और यह काफ़ी छोटा होना चाहिए. कॉन्टेंट बनाने 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वां फिबोनाशी नंबर है.

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

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

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

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

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

    #include "emscripten.h"
    #include "src/webp/encode.h"

    EMSCRIPTEN_KEEPALIVE
    int version() {
      return WebPGetEncoderVersion();
    }

यह एक अच्छा आसान प्रोग्राम है. इससे यह जांच की जा सकती है कि हमें libwebp का सोर्स कोड मिल सकता है या नहीं क्योंकि हमें कंपाइलेशन के लिए किसी पैरामीटर या जटिल डेटा स्ट्रक्चर की ज़रूरत नहीं इस फ़ंक्शन को शुरू करें.

इस प्रोग्राम को कंपाइल करने के लिए हमें कंपाइलर को बताना होगा कि वह libwebp की हेडर फ़ाइलें जो -I फ़्लैग का इस्तेमाल करती हैं और उसे इसकी सभी C फ़ाइलें भी पास करती हैं libwebp को ऐसा करना चाहिए. मैं ईमानदार हूं: मैंने अभी सभी को ऐसी फ़ाइलें जो मुझे मिलती थीं और कंपाइलर पर निर्भर रहती थीं, ताकि वे ग़ैर-ज़रूरी. ऐसा लगा कि यह शानदार काम कर रहा था!

    $ 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 का एनकोडिंग एपीआई, जो आरजीबी, RGBA, BGR या BGRA में बाइट का कलेक्शन. अच्छी बात यह है कि Canvas API में 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 आरजीबीए इमेज के लिए बफ़र तय करता है — इसलिए, हर पिक्सल के लिए 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 लैंड के लिए उपलब्ध है. अब WebP एन्कोडर को कॉल करने का समय है यह काम करता है! व्यू की मदद से, WebP दस्तावेज़, WebPEncodeRGBA वे बिलकुल सटीक लगते हैं. फ़ंक्शन, इनपुट इमेज पर पॉइंटर ले जाता है और और साथ ही 0 और 100 के बीच एक गुणवत्ता विकल्प भी. इसके अलावा, यह भी तय करता है कि हमारे लिए एक आउटपुट बफ़र होता है, जिसे हमें WebPFree() का इस्तेमाल करके खाली करना होगा, WebP इमेज के साथ काम किया जाएगा.

कोड में बदलने की कार्रवाई का नतीजा, आउटपुट बफ़र और उसकी लंबाई होती है. क्योंकि 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 मेमोरी को इतना नहीं बढ़ा सकता कि उसमें इनपुट और आउटपुट इमेज, दोनों को शामिल किया जा सके:

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