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