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

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

EM_JS, JavaScript स्निपेट का एलान करने का सुझाया गया तरीका है. यह तरीका असरदार है, क्योंकि यह किसी भी दूसरे 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 की तरह, आर्ग्युमेंट सिर्फ़ संख्या वाली वैल्यू तक सीमित होते हैं—पूर्णांक, फ़्लोटिंग-पॉइंट संख्याएं, पॉइंटर, और हैंडल.

यहां एक उदाहरण दिया गया है, जिसमें कंसोल में किसी भी जेएस वैल्यू को लॉग करने के लिए, 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);

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

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

नतीजा

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

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