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

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

إنغفار ستيبانيان
إنغفار ستيبانيان

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

توفّر 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 لفئة 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، والغرض منه الوحيد هو السماح بتنفيذ دوال async JavaScript باستخدام 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 لتسجيل قيمة JS عشوائية في وحدة التحكّم:

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.