تعرَّف على طريقة تضمين رمز 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
لتسجيل قيمة 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.