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

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

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

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

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

इस पोस्ट में, आपको मिलते-जुलते टास्क के लिए इन सभी का इस्तेमाल करने का तरीका पता चलेगा.

emscripten::val class

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() को C++ साइड को भी रोकना पड़ता है. इसके लिए, वेबअसेंबली मॉड्यूल के पूरे कॉल स्टैक को अनवाइंड करके, JavaScript पर वापस जाना पड़ता है. साथ ही, वेबअसेंबली स्टैक को इंतज़ार करना पड़ता है और ऑपरेशन पूरा होने पर उसे वापस लाना पड़ता है.

ऐसे कोड को 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>();

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

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 रैपर ही ऐसा है जो अच्छा नहीं दिखता. इसका मकसद सिर्फ़ Asyncify की मदद से 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 ब्लॉक, उसी हिसाब से फ़्लोटिंग-पॉइंट नंबर दिखा सकते हैं.

पास किए गए आर्ग्युमेंट, JavaScript बॉडी में $0, $1 वगैरह के नाम से उपलब्ध होंगे. आम तौर पर, 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 के ज़रिए अभी तक साफ़ तौर पर नहीं बताया जा सकता.