Emscripten

הוא מקשר בין JS ל-Wasm!

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

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

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

צריך להזין embind

embind הוא חלק מ'צרור הכלים של Emscripten' ומספק כמה פקודות מאקרו של 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 שטוען את נוצר מודול Wam:

<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 משתמשים באובייקטים של אפשרויות. זה נחמד ב-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(), השאר הוא קסם. עכשיו אפשר לקרוא לפונקציה 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 מספקת שיפורים משמעותיים בחוויית הפיתוח בזמן העבודה עם Wam ו-C/C++. המאמר הזה לא עוסק בכל האפשרויות שמשולבות בו מוצרים. אם זה מעניין אותך, מומלץ להמשיך עם embind תיעוד. חשוב לזכור: השימוש ב-embind יכול להפוך את מודול ה-Wasm וגם את קוד דבק של JavaScript גדול בשיעור של עד 11k עם gzip, בעיקר בגודל קטן מודולים. אם יש לך רק משטח Wasm קטן מאוד, embind עשוי לעלות יותר מ- זה שווה בסביבת ייצור! עם זאת, בהחלט צריך לתת כדאי לנסות.