Emscripten

הוא מקשר את JS ל-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 באופן הזה לא תומך במחרוזות, ונדרש להעביר באופן ידני קטעי זיכרון, מה שמקשה מאוד על השימוש ב-API של ספריות רבות. האם יש דרך טובה יותר? כן, אחרת למה היינו כותבים את המאמר הזה?

עיוות שמות ב-C++‎

חוויית המפתחים היא סיבה מספקת לפתח כלי שעוזר בקישור הזה, אבל יש סיבה דחופה יותר: כשמפעילים קומפילציה של קוד C או C++, כל קובץ מופעל בנפרד. לאחר מכן, הקישור (linker) יטפל בחיבור של כל קבצי האובייקטים האלה יחד והפיכתם לקובץ wasm. ב-C, שמות הפונקציות עדיין זמינים בקובץ האובייקט לשימוש של הקישור. כל מה שצריך כדי לקרוא לפונקציה ב-C הוא השם, שאנחנו מספקים כמחרוזת ל-cwrap().

לעומת זאת, ב-C++ יש תמיכה בעומס יתר של פונקציות, כלומר אפשר להטמיע את אותה פונקציה כמה פעמים כל עוד החתימה שלה שונה (למשל, פרמטרים מסוגים שונים). ברמת המהדר, שם נחמד כמו add ייגרם למשהו שמקודד את החתימה בשם הפונקציה עבור הקישור. כתוצאה מכך, לא נוכל יותר לחפש את הפונקציה לפי השם שלה.

מזינים את embind

embind הוא חלק מ-Emscripten toolchain ומספק לכם כמה מאקרוסים של 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 עושה את זה בחינם, יחד עם בדיקות טיפוס:

שגיאות ב-DevTools כשמפעילים פונקציה עם מספר שגוי של ארגומנטים או שהארגומנטים הם מסוג שגוי

זה מצוין כי אנחנו יכולים לזהות שגיאות מסוימות בשלב מוקדם, במקום להתמודד עם שגיאות 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++. לאחר מכן, צריך להוסיף את הדגלים של 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 ואת קוד ה-glue של JavaScript ב-gzip עד 11,000 בייטים – במיוחד במודולים קטנים. אם יש לכם רק ממשק wasm קטן מאוד, יכול להיות שהעלות של embind תהיה גבוהה יותר מהערך שלו בסביבת הייצור. עם זאת, כדאי מאוד לנסות.