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

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

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

Enter embind

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

#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 के लिए अपने फ़ंक्शन दिखाने होते हैं.

इस फ़ाइल को कंपाइल करने के लिए, हम उसी सेटअप का इस्तेमाल कर सकते हैं जिसका इस्तेमाल पिछले लेख में किया गया था. इसके अलावा, अगर आपको चाहिए, तो उसी Docker इमेज का इस्तेमाल किया जा सकता है. embind का इस्तेमाल करने के लिए, हम --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 में गड़बड़ियां दिखती हैं

यह बहुत अच्छा है, क्योंकि इससे हमें कभी-कभी होने वाली मुश्किल wasm गड़बड़ियों से निपटने के बजाय, कुछ गड़बड़ियों का पता पहले ही चल जाता है.

ऑब्जेक्ट

कई 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() फ़ंक्शन को भी बाइंड किया है. बाकी काम embind मैजिक करता है. अब मैं 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>

सी के बारे में क्या ख़्याल है?

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

$ 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 के दस्तावेज़ पढ़ें. ध्यान रखें कि embind का इस्तेमाल करने से, आपके wasm मॉड्यूल और JavaScript ग्लू कोड, दोनों का साइज़ 11 केबी तक बढ़ सकता है. ऐसा तब होता है, जब gzip का इस्तेमाल किया जाता है. खास तौर पर, छोटे मॉड्यूल के मामले में ऐसा होता है. अगर आपके पास बहुत छोटा wasm सर्फ़ेस है, तो प्रोडक्शन एनवायरमेंट में embind की लागत, फ़ायदे से ज़्यादा हो सकती है! हालांकि, आपको इसे ज़रूर आज़माना चाहिए.