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

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

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

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

מזינים את embind

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

שגיאות של כלי פיתוח כשמפעילים פונקציה עם מספר שגוי של ארגומנטים, או כאשר סוג הארגומנטים שגוי

זה די נהדר, כי אנחנו יכולים לזהות כמה שגיאות בשלב מוקדם במקום לטפל בשגיאות עבודה מורכבות מדי פעם.

אובייקטים

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