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