وهو يربط JavaScript بـ WebAssembly.
في مقالة wasm الأخيرة، تحدثتُ عن كيفية تجميع مكتبة C إلى wasm لتتمكّن من استخدامها على الويب. أحد الأمور التي لفتت انتباهي (وانتباه العديد من القرّاء) هو الطريقة البدائية وغير المريحة بعض الشيء التي يجب اتّباعها لتحديد الدوال التي تستخدمها من وحدة wasm يدويًا. لتذكيرك، إليك مقتطف الرمز البرمجي الذي أتحدث عنه:
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
في هذا القسم، نحدّد أسماء الدوال التي أشرنا إليها باستخدام
EMSCRIPTEN_KEEPALIVE
، وأنواع القيم التي تعرضها، وأنواع وسيطاتها. بعد ذلك، يمكننا استخدام الطرق في الكائن api
لاستدعاء هذه الدوال. ومع ذلك، لا يتيح استخدام wasm بهذه الطريقة التعامل مع السلاسل ويتطلّب منك نقل أجزاء من الذاكرة يدويًا، ما يجعل استخدام العديد من واجهات برمجة التطبيقات الخاصة بالمكتبات أمرًا شاقًا للغاية. أليس هناك طريقة أفضل؟ نعم، هناك طريقة أخرى، وإلا
لماذا كتبنا هذه المقالة؟
C++ name mangling
على الرغم من أنّ تجربة المطوّرين هي سبب كافٍ لإنشاء أداة تساعد في عمليات الربط هذه، هناك سبب أكثر إلحاحًا، وهو أنّه عند تجميع رمز C أو C++، يتم تجميع كل ملف على حدة. بعد ذلك، يتولّى برنامج الربط مهمة دمج كل ملفات الكائنات هذه معًا وتحويلها إلى ملف wasm. في لغة C، تظل أسماء الدوال متاحة في ملف الكائن ليستخدمها الرابط. كل ما تحتاجه لاستدعاء دالة C هو الاسم،
الذي نوفّره كسلسلة إلى cwrap()
.
من ناحية أخرى، تتيح لغة C++ إمكانية تحميل الدالة الزائد، ما يعني أنّه يمكنك تنفيذ الدالة نفسها عدة مرات طالما أنّ التوقيع مختلف (على سبيل المثال، معلمات ذات أنواع مختلفة). على مستوى المترجم، سيتم تشويه اسم لطيف مثل add
إلى شيء يشفّر التوقيع في اسم الدالة
لبرنامج الربط. نتيجةً لذلك، لن نتمكّن من البحث عن الدالة باستخدام اسمها.
إدخال embind
embind هي جزء من مجموعة أدوات Emscripten وتوفّر لك مجموعة من وحدات الماكرو C++ التي تتيح لك إضافة تعليقات توضيحية إلى رمز C++. يمكنك تحديد الدوال أو التعدادات أو الفئات أو أنواع القيم التي تخطّط لاستخدامها من JavaScript. لنبدأ ببعض الدوال البسيطة:
#include <emscripten/bind.h>
using namespace emscripten;
double add(double a, double b) {
return a + b;
}
std::string exclaim(std::string message) {
return message + "!";
}
EMSCRIPTEN_BINDINGS(my_module) {
function("add", &add);
function("exclaim", &exclaim);
}
مقارنةً بالمقالة السابقة، لن نضمّن emscripten.h
بعد الآن، لأنّه لم يعُد علينا إضافة التعليقات التوضيحية إلى دوالنا باستخدام EMSCRIPTEN_KEEPALIVE
.
بدلاً من ذلك، لدينا قسم EMSCRIPTEN_BINDINGS
ندرج فيه الأسماء التي نريد أن نتيح من خلالها استخدام دوالنا في JavaScript.
لتجميع هذا الملف، يمكننا استخدام الإعداد نفسه (أو صورة Docker نفسها إذا أردت) كما في المقالة
السابقة. لاستخدام embind،
نضيف العلامة --bind
:
$ emcc --bind -O3 add.cpp
كل ما تبقى الآن هو إنشاء ملف HTML يحمّل وحدة wasm التي أنشأناها حديثًا:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
console.log(Module.add(1, 2.3));
console.log(Module.exclaim("hello world"));
};
</script>
كما تلاحظ، لم نعد نستخدم cwrap()
. يمكنك استخدام هذه الميزة على الفور. والأهم من ذلك، لن نحتاج إلى نسخ أجزاء من الذاكرة يدويًا لجعل السلاسل تعمل، لأنّ embind يوفّر لك ذلك مجانًا، بالإضافة إلى عمليات التحقّق من النوع:

وهذا أمر رائع لأنّه يتيح لنا رصد بعض الأخطاء مبكرًا بدلاً من التعامل مع أخطاء wasm التي تكون غير عملية في بعض الأحيان.
العناصر
تستخدم العديد من دوال JavaScript الإنشائية ودوالها كائنات الخيارات. هذا النمط مفيد في JavaScript، ولكن تنفيذه يدويًا في wasm صعب للغاية، ويمكن أن تساعدك embind في ذلك أيضًا.
على سبيل المثال، لقد توصلت إلى دالة C++ مفيدة للغاية تعالج السلاسل الخاصة بي، وأريد استخدامها بشكل عاجل على الويب. إليك كيفية إجراء ذلك:
#include <emscripten/bind.h>
#include <algorithm>
using namespace emscripten;
struct ProcessMessageOpts {
bool reverse;
bool exclaim;
int repeat;
};
std::string processMessage(std::string message, ProcessMessageOpts opts) {
std::string copy = std::string(message);
if(opts.reverse) {
std::reverse(copy.begin(), copy.end());
}
if(opts.exclaim) {
copy += "!";
}
std::string acc = std::string("");
for(int i = 0; i < opts.repeat; i++) {
acc += copy;
}
return acc;
}
EMSCRIPTEN_BINDINGS(my_module) {
value_object<ProcessMessageOpts>("ProcessMessageOpts")
.field("reverse", &ProcessMessageOpts::reverse)
.field("exclaim", &ProcessMessageOpts::exclaim)
.field("repeat", &ProcessMessageOpts::repeat);
function("processMessage", &processMessage);
}
أنا أعرّف بنية لخيارات الدالة processMessage()
. في كتلة
EMSCRIPTEN_BINDINGS
، يمكنني استخدام value_object
لكي يتعرّف JavaScript على
قيمة C++ هذه كعنصر. يمكنني أيضًا استخدام value_array
إذا كنت أفضل استخدام قيمة C++ هذه كمصفوفة. أربط أيضًا الدالة processMessage()
،
والباقي هو سحر embind. يمكنني الآن استدعاء الدالة processMessage()
من JavaScript بدون أي رموز نموذجية:
console.log(Module.processMessage(
"hello world",
{
reverse: false,
exclaim: true,
repeat: 3
}
)); // Prints "hello world!hello world!hello world!"
صفوف
لإكمال الصورة، يجب أن أريك أيضًا كيف يتيح لك embind عرض فئات كاملة، ما يؤدي إلى توافق كبير مع فئات ES6. من المحتمل أنّك بدأت تلاحظ نمطًا الآن:
#include <emscripten/bind.h>
#include <algorithm>
using namespace emscripten;
class Counter {
public:
int counter;
Counter(int init) :
counter(init) {
}
void increase() {
counter++;
}
int squareCounter() {
return counter * counter;
}
};
EMSCRIPTEN_BINDINGS(my_module) {
class_<Counter>("Counter")
.constructor<int>()
.function("increase", &Counter::increase)
.function("squareCounter", &Counter::squareCounter)
.property("counter", &Counter::counter);
}
من جهة JavaScript، يبدو هذا وكأنه فئة أصلية:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const c = new Module.Counter(22);
console.log(c.counter); // prints 22
c.increase();
console.log(c.counter); // prints 23
console.log(c.squareCounter()); // prints 529
};
</script>
ماذا عن C؟
تمت كتابة embind للغة C++ ويمكن استخدامه فقط في ملفات C++، ولكن هذا لا يعني أنّه لا يمكنك الربط بملفات C. لدمج C وC++، ما عليك سوى فصل ملفات الإدخال إلى مجموعتين: مجموعة لملفات C ومجموعة لملفات C++، ثم إضافة علامات واجهة سطر الأوامر الخاصة بـ emcc
على النحو التالي:
$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp
الخاتمة
توفّر لك embind تحسينات كبيرة في تجربة المطوّرين عند العمل باستخدام wasm وC/C++. ولا تغطي هذه المقالة جميع الخيارات التي توفّرها embind. إذا كنت مهتمًا بذلك، أنصحك بمواصلة قراءة مستندات embind. يُرجى العِلم أنّ استخدام embind يمكن أن يزيد حجم وحدة wasm ورمز الربط JavaScript بما يصل إلى 11 ألف بايت عند ضغطه باستخدام gzip، لا سيما في الوحدات الصغيرة. إذا كان لديك مساحة صغيرة جدًا من wasm، قد تكون تكلفة embind أعلى من قيمتها في بيئة الإنتاج. ومع ذلك، ننصحك بتجربتها.