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::val क्लास

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

कुछ JSON को फ़ेच और पार्स करने के लिए, Asyncify के .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++ साइड को रोकना होगा. इसके लिए, WebAssembly मॉड्यूल के पूरे कॉल स्टैक को खोलना, 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>();

इन एपीआई को ध्यान में रखकर, 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 के साथ काम करते समय C++ में JavaScript कोड को इंटिग्रेट करने के अलग-अलग तरीकों के बारे में जाना.

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