Emscripten के साथ C++ में JavaScript स्निपेट एम्बेड करना

बाहर की दुनिया से संपर्क करने के लिए, अपनी WebAssembly लाइब्रेरी में JavaScript कोड को एम्बेड करने का तरीका जानें.

वेब के साथ WebAssembly इंटिग्रेशन पर काम करते समय, आपको बाहरी एपीआई, जैसे कि वेब एपीआई और तीसरे पक्ष की लाइब्रेरी को कॉल करने का तरीका चाहिए. इसके बाद, आपको एपीआई की दिखाई गई वैल्यू और ऑब्जेक्ट इंस्टेंस को सेव करने का तरीका चाहिए होगा. साथ ही, सेव की गई उन वैल्यू को बाद में अन्य एपीआई पर भेजने का तरीका भी चाहिए. एसिंक्रोनस एपीआई के लिए, हो सकता है कि आपको Asyncify एपीआई के साथ अपने सिंक्रोनस C/C++ कोड में प्रॉमिस का इंतज़ार करना होगा और ऑपरेशन खत्म होने पर नतीजे को पढ़ना होगा.

Emscripten ऐसे इंटरैक्शन के लिए कई टूल उपलब्ध कराता है:

  • C++ में JavaScript वैल्यू को सेव और इस्तेमाल करने के लिए emscripten::val.
  • JavaScript स्निपेट एम्बेड करने और उन्हें C/C++ फ़ंक्शन के तौर पर बाइंड करने के लिए EM_JS.
  • EM_ASYNC_JS, जो EM_JS से मिलता-जुलता है, लेकिन इसकी मदद से एसिंक्रोनस JavaScript स्निपेट को आसानी से एम्बेड किया जा सकता है.
  • EM_ASM का इस्तेमाल, छोटे स्निपेट को एम्बेड करने और फ़ंक्शन की जानकारी दिए बिना इन-लाइन एक्ज़ीक्यूट करने के लिए किया जाता है.
  • --js-library उन बेहतर स्थितियों के लिए जहां आपको एक साथ कई JavaScript फ़ंक्शन को एक ही लाइब्रेरी के रूप में एलान करना हो.

इस पोस्ट में बताया गया है कि इन सभी सुविधाओं को एक जैसे कामों के लिए कैसे इस्तेमाल किया जाता है.

emscripten::वाल क्लास

emcripten::val क्लास, Embind उपलब्ध कराता है. यह ग्लोबल एपीआई को शुरू कर सकता है, JavaScript की वैल्यू को C++ इंस्टेंस से बाइंड कर सकता है. साथ ही, वैल्यू को C++ और JavaScript टाइप के बीच बदल सकता है.

कुछ JSON को फ़ेच और पार्स करने के लिए, एसिंक्रोनस के .await() के साथ इसका इस्तेमाल करने का तरीका यहां बताया गया है:

#include <emscripten/val.h>

using namespace emscripten;

val fetch_json(const char *url) {
  // Get and cache a binding to the global `fetch` API in each thread.
  thread_local const val fetch = val::global("fetch");
  // Invoke fetch and await the returned `Promise<Response>`.
  val response = fetch(url).await();
  // Ask to read the response body as JSON and await the returned `Promise<any>`.
  val json = response.call<val>("json").await();
  // Return the JSON object.
  return json;
}

// Example URL.
val example_json = fetch_json("https://httpbin.org/json");

// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();

यह कोड ठीक से काम करता है, लेकिन बीच में कई चरण पूरे करता है. val पर हर कार्रवाई के लिए ये चरण पूरे करने होंगे:

  1. आर्ग्युमेंट के तौर पर भेजी गई C++ वैल्यू को किसी इंटरमीडिएट फ़ॉर्मैट में बदलें.
  2. JavaScript पर जाएं, आर्ग्युमेंट पढ़ें और उन्हें JavaScript वैल्यू में बदलें.
  3. फ़ंक्शन एक्ज़ीक्यूट करें
  4. नतीजे को JavaScript से इंटरमीडिएट फ़ॉर्मैट में बदलें.
  5. बदले गए नतीजे को C++ में देखें और C++ आखिर में उसे वापस पढ़ता है.

हर await() को WebAssembly मॉड्यूल के पूरे कॉल स्टैक को खोलकर, C++ साइड को भी रोकना होता है. साथ ही, कार्रवाई पूरी होने पर JavaScript पर वापस जाकर, इंतज़ार करना होता है, और WebAssembly स्टैक को वापस लाना होता है.

ऐसे कोड को C++ से किसी चीज़ की ज़रूरत नहीं होती. C++ कोड, JavaScript से जुड़ी अलग-अलग कार्रवाइयों के लिए सिर्फ़ ड्राइवर की तरह काम करता है. क्या होगा अगर आप एक ही समय में fetch_json को JavaScript पर ले जा सकें और इंटरमीडिएट चरणों का ओवरहेड कम कर सकें?

EM_JS मैक्रो

EM_JS macro की मदद से, fetch_json को JavaScript पर ले जाया जा सकता है. Emscripten में EM_JS की मदद से, C/C++ फ़ंक्शन के बारे में जानकारी दी जा सकती है. यह फ़ंक्शन, JavaScript स्निपेट से लागू किया जाता है.

WebAssembly की तरह ही, इसमें भी एक सीमा है कि इस पर सिर्फ़ न्यूमेरिक आर्ग्युमेंट और रिटर्न वैल्यू ही इस्तेमाल की जा सकती हैं. किसी अन्य वैल्यू को पास करने के लिए, आपको उनसे जुड़े एपीआई के ज़रिए उन्हें मैन्युअल तरीके से बदलना होगा. यहां कुछ उदाहरण दिए गए हैं.

पासिंग नंबर के लिए किसी कन्वर्ज़न की ज़रूरत नहीं है:

// Passing numbers, doesn't need any conversion.
EM_JS(int, add_one, (int x), {
  return x + 1;
});

int x = add_one(41);

JavaScript में और उससे स्ट्रिंग पास करते समय, आपको preamble.js से जुड़े कन्वर्ज़न और एलोकेशन फ़ंक्शन का इस्तेमाल करना होगा:

EM_JS(void, log_string, (const char *msg), {
  console.log(UTF8ToString(msg));
});

EM_JS(const char *, get_input, (), {
  let str = document.getElementById('myinput').value;
  // Returns heap-allocated string.
  // C/C++ code is responsible for calling `free` once unused.
  return allocate(intArrayFromString(str), 'i8', ALLOC_NORMAL);
});

आखिर में, ज़्यादा कॉम्प्लेक्स, आर्बिट्रेरी, वैल्यू टाइप के लिए, पहले बताई गई val क्लास के लिए JavaScript API का इस्तेमाल किया जा सकता है. इसका इस्तेमाल करके, JavaScript वैल्यू और C++ क्लास को इंटरमीडिएट हैंडल में बदला जा सकता है. साथ ही, पहले जैसा किया जा सकता है:

EM_JS(void, log_value, (EM_VAL val_handle), {
  let value = Emval.toValue(val_handle);
  console.log(value);
});

EM_JS(EM_VAL, find_myinput, (), {
  let input = document.getElementById('myinput');
  return Emval.toHandle(input);
});

val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
log_value(obj.as_handle()); // logs { x: 1, y: 2 }

val myinput = val::take_ownership(find_input());
// Now you can store the `find_myinput` DOM element for as long as you like, and access it later like:
std::string value = input["value"].as<std::string>();

उन एपीआई को ध्यान में रखते हुए, JavaScript को छोड़े बिना ज़्यादातर काम करने के लिए, fetch_json के उदाहरण को दोबारा लिखा जा सकता है:

EM_JS(EM_VAL, fetch_json, (const char *url), {
  return Asyncify.handleAsync(async () => {
    url = UTF8ToString(url);
    // Invoke fetch and await the returned `Promise<Response>`.
    let response = await fetch(url);
    // Ask to read the response body as JSON and await the returned `Promise<any>`.
    let json = await response.json();
    // Convert JSON into a handle and return it.
    return Emval.toHandle(json);
  });
});

// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));

// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();

हमारे पास फ़ंक्शन के एंट्री और एग्ज़िट पॉइंट पर अब भी कुछ खास कन्वर्ज़न उपलब्ध हैं, लेकिन बाकी का नाम अब सामान्य JavaScript कोड है. val से मिलते-जुलते वर्शन के उलट, अब इसे JavaScript इंजन से ऑप्टिमाइज़ किया जा सकता है. साथ ही, सभी एसिंक्रोनस कार्रवाइयों के लिए, C++ साइड को सिर्फ़ एक बार रोकना ज़रूरी है.

EM_ASYNC_JS मैक्रो

सिर्फ़ Asyncify.handleAsync रैपर ही शानदार नहीं लगता. इसका मकसद, एसिंक्रोनसता के साथ async JavaScript फ़ंक्शन को एक्ज़ीक्यूट करने की अनुमति देना है. असल में, इस्तेमाल का यह उदाहरण काफ़ी आम है. इसलिए, अब एक खास EM_ASYNC_JS मैक्रो है जो उन्हें एक साथ जोड़ता है.

यहां बताया गया है कि fetch के उदाहरण का फ़ाइनल वर्शन बनाने के लिए, इसका इस्तेमाल कैसे किया जा सकता है:

EM_ASYNC_JS(EM_VAL, fetch_json, (const char *url), {
  url = UTF8ToString(url);
  // Invoke fetch and await the returned `Promise<Response>`.
  let response = await fetch(url);
  // Ask to read the response body as JSON and await the returned `Promise<any>`.
  let json = await response.json();
  // Convert JSON into a handle and return it.
  return Emval.toHandle(json);
});

// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));

// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();

EM_ASM

JavaScript स्निपेट बताने के लिए, EM_JS सुझाया गया तरीका है. यह सुविधा काफ़ी कारगर है, क्योंकि यह तय किए गए स्निपेट को किसी भी दूसरे JavaScript फ़ंक्शन के इंपोर्ट की तरह ही बाइंड करता है. यह आपको सभी पैरामीटर टाइप और नामों के बारे में साफ़ तौर पर बताने की सुविधा देता है. इससे आपको अच्छे एर्गोनॉमिक्स मिलते हैं.

हालांकि, कुछ मामलों में आपको console.log कॉल, debugger; स्टेटमेंट या इससे मिलते-जुलते किसी स्टेटमेंट के लिए क्विक स्निपेट डालना है. साथ ही, आपको पूरे अलग फ़ंक्शन का एलान करने की चिंता नहीं करनी है. ऐसे मामलों में, EM_ASM macros family (EM_ASM, EM_ASM_INT, और EM_ASM_DOUBLE) को इस्तेमाल करना आसान हो सकता है. वे मैक्रो EM_JS मैक्रो की तरह होते हैं, लेकिन वे कोई फ़ंक्शन तय करने के बजाय, कोड को वहीं से एक्ज़ीक्यूट करते हैं जहां उन्हें डाला जाता है.

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

रिटर्न टाइप चुनने के लिए, आपको सही मैक्रो नाम इस्तेमाल करना होगा. EM_ASM ब्लॉक, void फ़ंक्शन की तरह काम करेंगे, EM_ASM_INT ब्लॉक एक पूर्णांक वैल्यू दे सकते हैं, और EM_ASM_DOUBLE ब्लॉक, उसी हिसाब से फ़्लोटिंग-पॉइंट नंबर दिखा सकते हैं.

पास किया गया कोई भी आर्ग्युमेंट, $0, $1 वगैरह नाम से JavaScript के मुख्य हिस्से में उपलब्ध होगा. आम तौर पर, EM_JS या WebAssembly की तरह आर्ग्युमेंट, सिर्फ़ संख्या वाली वैल्यू तक सीमित होते हैं. जैसे: पूर्णांक, फ़्लोटिंग-पॉइंट नंबर, पॉइंटर, और हैंडल.

यहां दिए गए उदाहरण में बताया गया है कि किसी आर्बिट्रेरी JS वैल्यू को कंसोल में लॉग करने के लिए, EM_ASM मैक्रो का इस्तेमाल कैसे किया जा सकता है:

val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
// executes inline immediately
EM_ASM({
  // convert handle passed under $0 into a JavaScript value
  let obj = Emval.fromHandle($0);
  console.log(obj); // logs { x: 1, y: 2 }
}, obj.as_handle());

--js-library

आखिर में, Emscripten, कस्टम लाइब्रेरी फ़ॉर्मैट में एक अलग फ़ाइल में JavaScript कोड का एलान करने की सुविधा देता है:

mergeInto(LibraryManager.library, {
  log_value: function (val_handle) {
    let value = Emval.toValue(val_handle);
    console.log(value);
  }
});

इसके बाद, आपको C++ साइड पर, उससे जुड़े प्रोटोटाइप मैन्युअल तौर पर एलान करने होंगे:

extern "C" void log_value(EM_VAL val_handle);

दोनों तरफ़ से तय किए जाने के बाद, JavaScript लाइब्रेरी को --js-library option की मदद से मुख्य कोड के साथ लिंक किया जा सकता है. साथ ही, प्रोटोटाइप को उससे जुड़े JavaScript लागू करने के तरीके से जोड़ा जा सकता है.

हालांकि, यह मॉड्यूल फ़ॉर्मैट नॉन-स्टैंडर्ड है और इसके लिए सावधानी से डिपेंडेंसी एनोटेशन की ज़रूरत होती है. इसलिए, इसे ज़्यादातर बेहतर स्थितियों के लिए रिज़र्व किया जाता है.

नतीजा

इस पोस्ट में हमने WebAssembly के साथ काम करते समय, JavaScript कोड को C++ में इंटिग्रेट करने के कई तरीके बताए हैं.

इस तरह के स्निपेट को शामिल करने से, कार्रवाइयों के लंबे क्रम साफ़ और बेहतर तरीके से बताए जा सकते हैं. साथ ही, तीसरे पक्ष की लाइब्रेरी, नए JavaScript API, और JavaScript सिंटैक्स की ऐसी सुविधाओं का भी इस्तेमाल किया जा सकता है जिन्हें C++ या Embind के ज़रिए अभी तक साफ़ तौर पर नहीं बताया जा सकता.