تضمين مقتطفات JavaScript بلغة C++ باستخدام Emscripten

تعرَّف على طريقة تضمين رمز JavaScript في مكتبة WebAssembly للتواصل مع العالم الخارجي.

عند العمل على دمج WebAssembly مع الويب، تحتاج إلى طريقة لاستدعاء واجهات برمجة تطبيقات خارجية، مثل واجهات برمجة تطبيقات الويب والمكتبات التابعة لجهات خارجية. ستحتاج بعد ذلك إلى طريقة لتخزين القيم ومثيلات الكائنات التي تعرضها واجهات برمجة التطبيقات، وطريقة لتمرير هذه القيم المخزنة إلى واجهات برمجة تطبيقات أخرى لاحقًا. بالنسبة إلى واجهات برمجة التطبيقات غير المتزامنة، قد تحتاج أيضًا إلى انتظار الوعود في رمز C/C++ المتزامن مع Asyncify وقراءة النتيجة بعد انتهاء العملية.

يوفّر Emscripten العديد من الأدوات لمثل هذه التفاعلات:

  • emscripten::val لتخزين قيم JavaScript وتشغيلها في C++.
  • EM_JS لتضمين مقتطفات JavaScript وربطها كدوال C/C++
  • EM_ASYNC_JS مشابه لـ EM_JS، ولكنه يسهّل تضمين مقتطفات JavaScript غير متزامنة.
  • EM_ASM لتضمين مقتطفات قصيرة وتنفيذها بشكل مضمّن، بدون الإعلان عن دالة.
  • --js-library للسيناريوهات المتقدمة التي تريد فيها تعريف الكثير من دوال JavaScript معًا كمكتبة واحدة.

ستتعرف في هذه المشاركة على كيفية استخدامها جميعًا لأداء مهام مماثلة.

فئة emscripten::val

يوفّر Embind الفئة emcripten::val. ويمكنها استدعاء واجهات برمجة تطبيقات عامة وربط قيم JavaScript بمثيلات C++ وتحويل القيم بين أنواع C++ وJavaScript.

إليك كيفية استخدامه مع .await() في Asyncify لاسترجاع ملف JSON وتحليله:

#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. تتيح لك العلامة EM_JS في Emscripten تعريف دالة 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);
});

أخيرًا، بالنسبة إلى أنواع القيم الأكثر تعقيدًا وتعقيدًا، يمكنك استخدام JavaScript API لفئة val المذكورة سابقًا. باستخدامه، يمكنك تحويل قيم 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، والغرض الوحيد منه هو السماح بتنفيذ دوال JavaScript async باستخدام Asyncify. في الواقع، حالة الاستخدام هذه شائعة جدًا لدرجة أنّ هناك الآن وحدة ماكرو 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 أرقام النقاط العائمة بشكل متوازي.

ستكون أي وسيطات تم تمريرها متاحة ضمن الأسماء $0 و$1 وما إلى ذلك في نص JavaScript. كما هو الحال مع EM_JS أو WebAssembly بشكل عام، تقتصر الوسيطات على القيم الرقمية فقط، مثل الأعداد الصحيحة وأرقام النقاط العائمة والمؤشرات والمقبض.

في ما يلي مثال على كيفية استخدام وحدة ماكرو EM_ASM لتسجيل قيمة JavaScript عشوائية في وحدة التحكّم:

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 المقابلة.

ومع ذلك، فإن تنسيق هذه الوحدة غير قياسي ويتطلب تعليقات توضيحية دقيقة للتبعية. ولذلك، يتم حجزها في الغالب للسيناريوهات المتقدمة.

الخاتمة

تناولنا في هذه المشاركة طرقًا مختلفة لدمج رمز JavaScript في C++ عند العمل باستخدام WebAssembly.

ويتيح لك تضمين هذه المقتطفات إمكانية التعبير عن سلاسل طويلة من العمليات بطريقة أكثر وضوحًا وفعالية، والاستفادة من مكتبات الجهات الخارجية وواجهات برمجة تطبيقات JavaScript الجديدة، وحتى ميزات بنية JavaScript التي لا يمكن التعبير عنها بعد باستخدام C++ أو Embind.