एमस्क्रिप्टन का एंबाइंड

यह JS को आपके Wasm से बाइंड करता है!

अपने पिछले vasm लेख में, मैंने C लाइब्रेरी को Wasm में कंपाइल करने के बारे में बात की थी, ताकि आप उसे वेब पर इस्तेमाल कर सकें. एक चीज़ जो मुझे (और कई पाठकों को) सबसे अलग लगी, वह है गंदे और थोड़ा अजीब तरीके से. आपको मैन्युअल तौर पर यह बताना होगा कि आप Wasm मॉड्यूल के कौनसे फ़ंक्शन इस्तेमाल कर रहे हैं. आपका ध्यान रीफ़्रेश करने के लिए, मैं इस कोड स्निपेट के बारे में बात कर रहा/रही हूं:

const api = {
    version: Module.cwrap('version', 'number', []),
    create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
    destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};

यहां हम उन फ़ंक्शन के नाम बताते हैं जिन्हें हमने EMSCRIPTEN_KEEPALIVE से मार्क किया है. साथ ही, उनके रिटर्न टाइप क्या हैं, और उनके तर्क किस तरह के हैं. इसके बाद, हम इन फ़ंक्शन को शुरू करने के लिए, api ऑब्जेक्ट पर मौजूद तरीकों का इस्तेमाल कर सकते हैं. हालांकि, इस तरीके से Wasm के इस्तेमाल करने पर स्ट्रिंग काम नहीं करती हैं और आपको मैन्युअल रूप से मेमोरी के कई हिस्सों को एक जगह से दूसरी जगह ले जाना पड़ता है. इससे, कई लाइब्रेरी एपीआई को इस्तेमाल करना बहुत मुश्किल हो जाता है. क्या इससे बेहतर कोई तरीका नहीं है? हां क्यों है, नहीं तो यह लेख किस बारे में होगा?

C++ नाम प्रबंधन

हालांकि, डेवलपर का अनुभव एक ऐसा टूल बनाने के लिए काफ़ी होगा जो इन बाइंडिंग में मदद कर सके, लेकिन असल में इसकी एक और वजह है: C या C++ कोड को कंपाइल करते समय, हर फ़ाइल को अलग-अलग कंपाइल किया जाता है. इसके बाद, लिंकर इन सभी तथाकथित ऑब्जेक्ट फ़ाइलों को एक साथ मिटा देता है और उन्हें Wasm फ़ाइल में बदल देता है. C में, लिंकर के इस्तेमाल के लिए फ़ंक्शन के नाम अब भी ऑब्जेक्ट फ़ाइल में उपलब्ध रहते हैं. आपको सिर्फ़ C फ़ंक्शन को कॉल करने की अनुमति होनी चाहिए, वह नाम है, जिसे हम cwrap() को एक स्ट्रिंग के तौर पर उपलब्ध करा रहे हैं.

वहीं, C++ का इस्तेमाल, फ़ंक्शन ओवरलोड करने के लिए किया जा सकता है.इसका मतलब है कि एक ही फ़ंक्शन को कई बार लागू किया जा सकता है. इसके लिए ज़रूरी है कि सिग्नेचर अलग-अलग हो (जैसे कि अलग-अलग टाइप किए गए पैरामीटर). कंपाइलर लेवल पर, add जैसा अच्छा नाम किसी ऐसी चीज़ में घुल-मिल सकता है जो लिंकर के फ़ंक्शन नाम में सिग्नेचर को कोड में बदलता है. इस वजह से, अब हम अपने काम के नाम को नहीं खोज पाएंगे.

एंबिंड डालें

embind, Emscripten टूलचेन का हिस्सा है और इससे आपको कई C++ मैक्रो मिलते हैं. इनकी मदद से, C++ कोड के बारे में जानकारी दी जा सकती है. आपके पास यह एलान करने का विकल्प है कि JavaScript से आपको किन फ़ंक्शन, इनम, क्लास या वैल्यू टाइप का इस्तेमाल करना है. आइए, कुछ सादे फ़ंक्शन से आसान शुरुआत करते हैं:

#include <emscripten/bind.h>

using namespace emscripten;

double add(double a, double b) {
    return a + b;
}

std::string exclaim(std::string message) {
    return message + "!";
}

EMSCRIPTEN_BINDINGS(my_module) {
    function("add", &add);
    function("exclaim", &exclaim);
}

मेरे पिछले लेख की तुलना में, अब हम emscripten.h को शामिल नहीं कर रहे हैं, क्योंकि हमें अब अपने फ़ंक्शन के बारे में EMSCRIPTEN_KEEPALIVE के साथ एनोटेट करने की ज़रूरत नहीं है. इसके बजाय, हमारे पास एक EMSCRIPTEN_BINDINGS सेक्शन है जिसमें हम उन नामों की सूची बनाते हैं जिनके तहत हम अपने फ़ंक्शन को JavaScript में दिखाना चाहते हैं.

इस फ़ाइल को कंपाइल करने के लिए, हम उसी सेटअप (या अगर आप चाहें, तो उसी डॉकर इमेज) का इस्तेमाल कर सकते हैं जिसका इस्तेमाल पिछले लेख में किया गया था. एम्बाइंड का इस्तेमाल करने के लिए, हम --bind फ़्लैग जोड़ते हैं:

$ emcc --bind -O3 add.cpp

अब सिर्फ़ एक ऐसी एचटीएमएल फ़ाइल तैयार की जा रही है जो हमारे हाल ही में बनाए गए Wasm मॉड्यूल को लोड करती है:

<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
    console.log(Module.add(1, 2.3));
    console.log(Module.exclaim("hello world"));
};
</script>

जैसा कि आपको दिख रहा है, अब हम cwrap() का इस्तेमाल नहीं कर रहे हैं. यह आसानी से काम करता है. उससे भी ज़्यादा ज़रूरी बात, हमें स्ट्रिंग के काम करने के लिए मेमोरी के अलग-अलग हिस्सों को मैन्युअल रूप से कॉपी करने के बारे में चिंता करने की ज़रूरत नहीं है! embind आपको टाइप जांच के साथ-साथ यह मुफ़्त भी देता है:

DevTools से जुड़ी गड़बड़ियां तब होती हैं, जब आर्ग्युमेंट की गलत संख्या वाले फ़ंक्शन को शुरू किया जाता है
या आर्ग्युमेंट का टाइप गलत होता है.

यह बहुत बढ़िया होता है, क्योंकि कभी-कभी होने वाली काफ़ी भारी गड़बड़ियों को ठीक करने के बजाय, हम कुछ गड़बड़ियों को शुरुआत में ही ठीक कर लेते हैं.

ऑब्जेक्ट

कई JavaScript कंस्ट्रक्टर और फ़ंक्शन, विकल्प ऑब्जेक्ट का इस्तेमाल करते हैं. JavaScript में यह एक अच्छा पैटर्न है, लेकिन Wasm में इसे मैन्युअल तरीके से समझना बहुत मुश्किल है. embind से भी इसमें मदद मिल सकती है!

उदाहरण के लिए, मुझे बहुत ही काम का C++ फ़ंक्शन मिला है, जो मेरी स्ट्रिंग को प्रोसेस करता है और मुझे वेब पर इसका तुरंत इस्तेमाल करना है. मैंने यह काम इस तरह किया:

#include <emscripten/bind.h>
#include <algorithm>

using namespace emscripten;

struct ProcessMessageOpts {
    bool reverse;
    bool exclaim;
    int repeat;
};

std::string processMessage(std::string message, ProcessMessageOpts opts) {
    std::string copy = std::string(message);
    if(opts.reverse) {
    std::reverse(copy.begin(), copy.end());
    }
    if(opts.exclaim) {
    copy += "!";
    }
    std::string acc = std::string("");
    for(int i = 0; i < opts.repeat; i++) {
    acc += copy;
    }
    return acc;
}

EMSCRIPTEN_BINDINGS(my_module) {
    value_object<ProcessMessageOpts>("ProcessMessageOpts")
    .field("reverse", &ProcessMessageOpts::reverse)
    .field("exclaim", &ProcessMessageOpts::exclaim)
    .field("repeat", &ProcessMessageOpts::repeat);

    function("processMessage", &processMessage);
}

मैं अपने processMessage() फ़ंक्शन के विकल्पों के लिए स्ट्रक्चर तय कर रहा/रही हूं. EMSCRIPTEN_BINDINGS ब्लॉक में, value_object का इस्तेमाल करके JavaScript को इस C++ वैल्यू को एक ऑब्जेक्ट के तौर पर दिखाया जा सकता है. अगर मुझे इस C++ वैल्यू को अरे के तौर पर इस्तेमाल करना है, तो value_array का भी इस्तेमाल किया जा सकता है. मैंने processMessage() फ़ंक्शन को भी बाइंड किया है और बाकी चीज़ें मैजिक हैं. अब मैं किसी भी बॉयलरप्लेट कोड के बिना, JavaScript से processMessage() फ़ंक्शन को कॉल कर सकता हूं:

console.log(Module.processMessage(
    "hello world",
    {
    reverse: false,
    exclaim: true,
    repeat: 3
    }
)); // Prints "hello world!hello world!hello world!"

क्लास

पूरी जानकारी के लिए, मुझे आपको यह भी दिखाना चाहिए कि कैसे embind आपको पूरी क्लास का पता लगाने में मदद करता है, जिससे ES6 क्लास के साथ काफ़ी तालमेल बनता है. आपको शायद अब तक पैटर्न दिखने लगा होगा:

#include <emscripten/bind.h>
#include <algorithm>

using namespace emscripten;

class Counter {
public:
    int counter;

    Counter(int init) :
    counter(init) {
    }

    void increase() {
    counter++;
    }

    int squareCounter() {
    return counter * counter;
    }
};

EMSCRIPTEN_BINDINGS(my_module) {
    class_<Counter>("Counter")
    .constructor<int>()
    .function("increase", &Counter::increase)
    .function("squareCounter", &Counter::squareCounter)
    .property("counter", &Counter::counter);
}

JavaScript की ओर, यह करीब एक नेटिव क्लास जैसा लगता है:

<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
    const c = new Module.Counter(22);
    console.log(c.counter); // prints 22
    c.increase();
    console.log(c.counter); // prints 23
    console.log(c.squareCounter()); // prints 529
};
</script>

C कैसा रहेगा?

embind को C++ के लिए लिखा गया था और इसका इस्तेमाल सिर्फ़ C++ फ़ाइलों में किया जा सकता है. हालांकि, इसका मतलब यह नहीं है कि C फ़ाइलों से इसे लिंक नहीं किया जा सकता! C और C++ को मिक्स करने के लिए, आपको सिर्फ़ अपनी इनपुट फ़ाइलों को दो ग्रुप में अलग करना होगा: एक C के लिए और दूसरा C++ फ़ाइलों के लिए और emcc के लिए CLI फ़्लैग इस तरह से जोड़ें:

$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp

नतीजा

embind आपको Wasm और C/C++ के साथ काम करने के दौरान डेवलपर के अनुभव को बेहतर बनाने का मौका देता है. इस लेख में सभी तरह के ऑफ़र शामिल नहीं किए गए हैं. अगर इस प्रक्रिया में आपकी दिलचस्पी है, तो हमारा सुझाव है कि आप embind के दस्तावेज़ देखें. ध्यान रखें कि embind का इस्तेमाल करने से, Wasm मॉड्यूल और JavaScript ग्लू कोड, दोनों की क्वालिटी 11 हज़ार तक बढ़ सकती है. खास तौर पर, छोटे मॉड्यूल पर. अगर आपके वॉम की सतह बहुत छोटी है, तो एंबाइंड की कीमत, प्रोडक्शन एनवायरमेंट में कीमत से ज़्यादा हो सकती है! जो भी हो, आपको इसे ज़रूर आज़माना चाहिए.