הוא מקשר בין 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++ name mangling
חוויית המפתחים היא סיבה מספיק טובה ליצור כלי שיעזור עם הקישורים האלה, אבל יש סיבה חשובה יותר: כשמבצעים קומפילציה של קוד C או C++, כל קובץ עובר קומפילציה בנפרד. לאחר מכן, מקשר דואג לערבב את כל מה שנקרא קבצי אובייקט ולהפוך אותם לקובץ wasm. ב-C, השמות של הפונקציות עדיין זמינים בקובץ האובייקט לשימוש של ה-linker. כדי להפעיל פונקציית C, צריך רק את השם שלה, שאנחנו מספקים כמחרוזת ל-cwrap()
.
לעומת זאת, שפת C++ תומכת בהעמסת פונקציות, כלומר אפשר להטמיע את אותה פונקציה כמה פעמים כל עוד החתימה שונה (למשל, פרמטרים עם סוגים שונים). ברמת הקומפיילר, שם כמו add
יעבור שינוי למשהו שמקודד את החתימה בשם הפונקציה עבור הלינקר. כתוצאה מכך, לא נוכל יותר לחפש את הפונקציה לפי השם שלה.
Enter embind
embind הוא חלק משרשרת הכלים של Emscripten, והוא מספק לכם הרבה פקודות מאקרו של C++ שמאפשרות להוסיף הערות לקוד C++. אפשר להצהיר אילו פונקציות, סוגי enum, מחלקות או סוגי ערכים אתם מתכננים להשתמש מ-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 משתמשים באובייקטים של אפשרויות. זהו דפוס נחמד ב-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);
}
אני מגדיר מבנה (struct) לאפשרויות של הפונקציה processMessage()
. בבלוק EMSCRIPTEN_BINDINGS
, אפשר להשתמש ב-value_object
כדי ש-JavaScript תראה את הערך הזה של C++ כאובייקט. אפשר גם להשתמש ב-value_array
אם רוצים להשתמש בערך הזה ב-C++ כמערך. אני גם קושר את הפונקציה processMessage()
, ושאר הפעולות הן קסם של embind. עכשיו אני יכול להפעיל את הפונקציה processMessage()
מ-JavaScript בלי קוד boilerplate:
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 ואת קוד ה-JavaScript glue ב-11k לכל היותר כשמפעילים gzip – בעיקר במודולים קטנים. אם יש לכם רק שטח קטן מאוד של wasm, יכול להיות שהעלות של embind תהיה גבוהה מדי בסביבת ייצור. למרות זאת, כדאי לנסות.