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

تظهر أخطاء في أدوات مطوّري البرامج عند استدعاء دالة تحتوي على عدد خاطئ من الوسيطات أو أن الوسيطات لها نوع غير صحيح

هذا أمر رائع لأنه يمكننا اكتشاف بعض الأخطاء مبكرًا بدلاً من التعامل مع أخطاء Wasm المزعجة أحيانًا.

أغراض

تستخدم العديد من الدالة الإنشائية ودوال JavaScript كائنات الخيارات. إنّه نمط رائع في JavaScript، ولكنه ممل للغاية عند إدراكه باستخدام Wasm يدويًا. يمكن أن يساعد الدمج في هذه الحالة أيضًا.

على سبيل المثال، ابتكرت دالة 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 لجعل قيمة C++ هذه كائنًا في JavaScript. يمكنني أيضًا استخدام value_array إذا كنت أرغب في استخدام قيمة C++ هذه كصفيف. أقوم أيضًا بربط الدالة processMessage()، والباقي يشمل magic. يمكنني الآن استدعاء الدالة processMessage() من JavaScript بدون أي رمز نموذجي:

console.log(Module.processMessage(
    "hello world",
    {
    reverse: false,
    exclaim: true,
    repeat: 3
    }
)); // Prints "hello world!hello world!hello world!"

صفوف

من أجل الاكتمال، أود أن أوضح لك أيضًا كيف يسمح لك الترابط بعرض الدروس بالكامل، مما يحقق الكثير من التعاون مع صفوف 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++ وزيادة علامات 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 يمكن أن يزيد من حجم وحدة Wasm ورمز غراء JavaScript بما يصل إلى 11 كيلوبايت عند إجراء gzip، وبالأخصّ في الوحدات الصغيرة. إذا لم يكن لديك سوى مساحة صغيرة جدًا من مساحة Wasm، قد يكلف الدمج أكثر ممّا يستحق في بيئة الإنتاج! ومع ذلك، يجب عليك بالتأكيد تجربة ذلك.