دمج 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 قد يشوّه في شيء يشفّر التوقيع في الدالة اسم الرابط. فنتيجة لذلك، لن نتمكن من البحث عن الدالة باسمه بعد الآن.

الدخول إلى مجموعة

دمج جزءًا من سلسلة أدوات 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 كائنات الخيارات. إنّه في جافا سكريبت، إلا أن إدراكه في 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 لإظهار 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!"

صفوف

من أجل الاكتمال، يجب أن أوضح لك أيضًا كيف يسمح لك الدمج بإظهار لصفوف كاملة، مما ينتج عنه انسجام كبير مع صفوف 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 صغير جدًا، فقد تكلف عملية الدمج أكثر من وهي تستحق في بيئة الإنتاج! ومع ذلك، ينبغي عليك بالتأكيد منح جربها.