دمج Emscripten

يربط هذا الإجراء JavaScript بتطبيقك المكتوب بلغة wasm.

في مقالتي الأخيرة عن لغة 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++

على الرغم من أنّ تجربة المطوّر ستكون سببًا كافيًا لإنشاء أداة تساعد في إنشاء هذه الروابط، إلا أنّ هناك سببًا أكثر إلحاحًا: عند تجميع رمز 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 image نفسه إذا أردت) كما هو موضّح في المقالة السابقة. لاستخدام 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 ذلك مجانًا، بالإضافة إلى عمليات التحقّق من النوع:

أخطاء &quot;أدوات مطوّري البرامج&quot; عند استدعاء دالة بعدد غير صحيح من الوسيطات
أو إذا كان نوع الوسيطات غير صحيح

وهذا أمر رائع لأنّه يمكننا رصد بعض الأخطاء في وقت مبكر بدلاً من التعامل مع أخطاء 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()، وباقي الخطوات هي سحر. يمكنني الآن استدعاء الدالة 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>

ماذا عن "ج"؟

تم كتابة embind لبرنامج C++ ولا يمكن استخدامه إلا في ملفات C++، ولكن لا يعني ذلك أنّه لا يمكنك الربط بملفات C. لخلط C وC++، ما عليك سوى فصل ملفات الإدخال إلى مجموعتَين: واحدة لملفات C وواحدة لملفات C++، و زيادة علامات CLI لـ 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 قيمتها في بيئة الإنتاج. مع ذلك، ننصحك بتجربة هذه الميزة.