تعرَّف على كيفية تضمين رمز 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
:
- تحويل قيم C++ التي يتم تمريرها كوسيطات إلى تنسيق وسيط
- انتقِل إلى JavaScript، واقرأ الوسيطات واحوِّلها إلى قيم JavaScript.
- تنفيذ الدالة
- حوِّل النتيجة من JavaScript إلى تنسيق وسيط.
- يتم إرجاع النتيجة المحوَّلة إلى 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
لتسجيل قيمة 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.