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, שמות הפונקציות עדיין זמינים בקובץ האובייקט כדי שה-Linker יוכל להשתמש בו. כל מה שאתם צריכים כדי לקרוא לפונקציה C הוא השם, שאותו אנחנו מספקים כמחרוזת ל-cwrap().

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

הזנת embind

embind הוא חלק מאוסף הכלים של Emscripten, והוא כולל כמה פקודות מאקרו של C++ שמאפשרות להוסיף הערות לקוד C++. אפשר להצהיר ב-JavaScript באילו פונקציות, טיפוסים בני מנייה (enums), מחלקות או סוגי ערכים אתם מתכננים להשתמש. נתחיל בצורה פשוטה עם כמה פונקציות פשוטות:

#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 מסורבלות למדי.

Objects

בנאים ופונקציות רבים של 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 כדי לגרום ל-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>

מה לגבי C?

embind נכתב עבור 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 יכול להגדיל בשיעור של עד 11k את מודול Wasm וגם את קוד ההדבקה של JavaScript — בעיקר במודולים קטנים. אם יש לכם רק משטח קטן מאוד של Wasm, השימוש ב-embind עלול להיות גבוה יותר מהשווי שלו בסביבת ייצור! עם זאת, בהחלט כדאי לכם לנסות.