איך מטמיעים קוד JavaScript בספריית WebAssembly כדי לתקשר עם העולם החיצון
כשעובדים על שילוב של WebAssembly עם האינטרנט, צריך דרך לבצע קריאה לממשקי API חיצוניים, כמו ממשקי API של אינטרנט וספריות של צד שלישי. לאחר מכן, תצטרכו דרך לאחסן את הערכים ואת המופעים של האובייקטים שממשקי ה-API האלה מחזירים, ודרך להעביר את הערכים השמורים האלה לממשקי API אחרים מאוחר יותר. בממשקי API אסינכררוניים, יכול להיות שתצטרכו גם להמתין להבטחות בקוד הסינכרוני של C/C++ באמצעות Asyncify ולקרוא את התוצאה אחרי שהפעולה תסתיים.
ב-Emscripten יש כמה כלים לאינטראקציות כאלה:
emscripten::val
לאחסון ולהפעלה של ערכי JavaScript ב-C++.EM_JS
להטמעת קטעי JavaScript ולקישור שלהם כפונקציות C/C++.EM_ASYNC_JS
, שדומה ל-EM_JS
, אבל מאפשר להטמיע קטעי JavaScript אסינכרוניים בקלות רבה יותר.EM_ASM
להטמעת קטעי קוד קצרים והרצה שלהם בתוך שורה, בלי להצהיר על פונקציה.--js-library
בתרחישים מתקדמים שבהם רוצים להצהיר על הרבה פונקציות JavaScript יחד כספרייה אחת.
בפוסט הזה תלמדו איך להשתמש בכולן לביצוע משימות דומות.
emscripten::val class
המחלקה emcripten::val
סופקה על ידי Embind. הוא יכול להפעיל ממשקי API גלובליים, לקשר ערכים של JavaScript למופעים של C++ ולהמיר ערכים בין סוגי C++ ל-JavaScript.
כך משתמשים בו עם .await()
של Asyncify כדי לאחזר ולנתח קובץ JSON:
#include <emscripten/val.h>
using namespace emscripten;
val fetch_json(const char *url) {
// Get and cache a binding to the global `fetch` API in each thread.
thread_local const val fetch = val::global("fetch");
// Invoke fetch and await the returned `Promise<Response>`.
val response = fetch(url).await();
// Ask to read the response body as JSON and await the returned `Promise<any>`.
val json = response.call<val>("json").await();
// Return the JSON object.
return json;
}
// Example URL.
val example_json = fetch_json("https://httpbin.org/json");
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
הקוד הזה פועל היטב, אבל הוא מבצע הרבה שלבים ביניים. בכל פעולה ב-val
צריך לבצע את השלבים הבאים:
- המרת ערכים של C++ שמועברים כארגומנטים לפורמט ביניים כלשהו.
- עוברים ל-JavaScript, קוראים את הארגומנטים וממירים אותם לערכים של JavaScript.
- ביצוע הפונקציה
- המרת התוצאה מ-JavaScript לפורמט ביניים.
- מחזירים את התוצאה המומרת ל-C++, ובסופו של דבר היא נקראת בחזרה ב-C++.
כל await()
צריך גם להשהות את הצד של C++ על ידי ביטול כל סטאק הקריאות של מודול WebAssembly, חזרה ל-JavaScript, המתנה ושחזור של סטאק WebAssembly בסיום הפעולה.
קוד כזה לא צריך שום דבר מ-C++. קוד C++ פועל רק כמפעיל של סדרה של פעולות JavaScript. מה אם תוכלו להעביר את fetch_json
ל-JavaScript ולהפחית בו-זמנית את התקורה של השלבים הביניים?
מאקרו EM_JS
האפשרות EM_JS macro
מאפשרת להעביר את fetch_json
ל-JavaScript. EM_JS
ב-Emscripten מאפשר להצהיר על פונקציית C/C++ שמוטמעת באמצעות קטע קוד של JavaScript.
כמו WebAssembly עצמו, יש לו מגבלה של תמיכה רק בארגומנטים ובערכים חזרה מספריים. כדי להעביר ערכים אחרים, צריך להמיר אותם באופן ידני באמצעות ממשקי ה-API המתאימים. הנה מספר דוגמאות.
העברת מספרים לא דורשת המרה:
// Passing numbers, doesn't need any conversion.
EM_JS(int, add_one, (int x), {
return x + 1;
});
int x = add_one(41);
כשמעבירים מחרוזות אל JavaScript וממנו, צריך להשתמש בפונקציות ההמרה וההקצאה המתאימות מ-preamble.js:
EM_JS(void, log_string, (const char *msg), {
console.log(UTF8ToString(msg));
});
EM_JS(const char *, get_input, (), {
let str = document.getElementById('myinput').value;
// Returns heap-allocated string.
// C/C++ code is responsible for calling `free` once unused.
return allocate(intArrayFromString(str), 'i8', ALLOC_NORMAL);
});
לסיום, לסוגים מורכבים יותר של ערכים שרירותיים, אפשר להשתמש ב-JavaScript API של הכיתה val
שצוינה קודם. בעזרתו אפשר להמיר ערכים של JavaScript וסיווגים של C++ למזהים ביניים ולהפך:
EM_JS(void, log_value, (EM_VAL val_handle), {
let value = Emval.toValue(val_handle);
console.log(value);
});
EM_JS(EM_VAL, find_myinput, (), {
let input = document.getElementById('myinput');
return Emval.toHandle(input);
});
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
log_value(obj.as_handle()); // logs { x: 1, y: 2 }
val myinput = val::take_ownership(find_input());
// Now you can store the `find_myinput` DOM element for as long as you like, and access it later like:
std::string value = input["value"].as<std::string>();
בהתאם לממשקי ה-API האלה, אפשר לכתוב מחדש את הדוגמה fetch_json
כך שרוב העבודה תתבצע בלי לצאת מ-JavaScript:
EM_JS(EM_VAL, fetch_json, (const char *url), {
return Asyncify.handleAsync(async () => {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
עדיין יש לנו כמה המרות מפורשות בנקודות הכניסה והיציאה של הפונקציה, אבל שאר הקוד הוא עכשיו קוד JavaScript רגיל. להבדיל מ-val
מקבילה, עכשיו אפשר לבצע אופטימיזציה שלה באמצעות מנוע JavaScript, וצריך להשהות את הצד C++ פעם אחת בלבד עבור כל הפעולות האסינכרוניות.
מאקרו EM_ASYNC_JS
החלק היחיד שלא נראה יפה הוא ה-wrapper של Asyncify.handleAsync
— המטרה היחידה שלו היא לאפשר הפעלת פונקציות JavaScript של async
באמצעות Asyncify. למעשה, התרחיש לדוגמה הזה נפוץ כל כך שיש עכשיו מאקרו EM_ASYNC_JS
מיוחד שמחבר ביניהם.
כך משתמשים בו כדי ליצור את הגרסה הסופית של הדוגמה fetch
:
EM_ASYNC_JS(EM_VAL, fetch_json, (const char *url), {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
EM_ASM
EM_JS
היא הדרך המומלצת להצהיר על קטעי JavaScript. היתרון של השיטה הזו הוא שהיא מקשרת את קטעי הקוד המוצהרים ישירות, כמו כל ייבוא אחר של פונקציית JavaScript. בנוסף, היא מספקת נוחות רבה, כי היא מאפשרת להצהיר באופן מפורש על כל סוגי הפרמטרים והשמות שלהם.
עם זאת, במקרים מסוימים תרצו להוסיף קטע קוד קצר לקריאה ל-console.log
, הצהרה ב-debugger;
או משהו דומה, ואתם לא רוצים לטפח הצהרה על פונקציה נפרדת לגמרי. במקרים הנדירים האלה, EM_ASM macros family
(EM_ASM
, EM_ASM_INT
ו-EM_ASM_DOUBLE
) עשוי להיות פתרון פשוט יותר. המאקרואים האלה דומים למאקרו EM_JS
, אבל הם מריצים קוד בתוך שורת הקוד שבה הם מוכנסים, במקום להגדיר פונקציה.
מאחר שהם לא מצהירים על אב טיפוס של פונקציה, הם צריכים דרך אחרת לציין את סוג ההחזרה ולגשת לארגומנטים.
כדי לבחור את סוג ההחזרה, צריך להשתמש בשם המאקרו הנכון. צפוי שבלוקים מסוג EM_ASM
יפעלו כמו פונקציות void
, שבלוקים מסוג EM_ASM_INT
יוכלו להחזיר ערך שלם, ובלוקים מסוג EM_ASM_DOUBLE
יוכלו להחזיר מספרים בספרות עשרוניות בהתאם.
כל הארגומנטים שהועברו יהיו זמינים בשמות $0
, $1
וכן הלאה בגוף ה-JavaScript. בדומה ל-EM_JS
או ל-WebAssembly באופן כללי, הארגומנטים מוגבלים רק לערכים מספריים – מספרים שלמים, מספרים עם נקודה צפה (floating-point), מצביעים וכינויים.
דוגמה לשימוש במאקרו EM_ASM
כדי לתעד במסוף ערך JS שרירותי:
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
// executes inline immediately
EM_ASM({
// convert handle passed under $0 into a JavaScript value
let obj = Emval.fromHandle($0);
console.log(obj); // logs { x: 1, y: 2 }
}, obj.as_handle());
--js-library
לבסוף, Emscripten תומך בהצהרה על קוד JavaScript בקובץ נפרד בפורמט ספרייה משלו בהתאמה אישית:
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
אם כן, תצטרכו להצהיר באופן ידני על אבות טיפוס תואמים בצד C++:
extern "C" void log_value(EM_VAL val_handle);
אחרי שמצהירים על שני הצדדים, אפשר לקשר את ספריית JavaScript לקוד הראשי דרך --js-library option
ולחבר אבות טיפוס להטמעות תואמות של JavaScript.
עם זאת, פורמט מודול זה אינו סטנדרטי ודורש הערות תלות זהירות. לכן, הוא מיועד בעיקר לתרחישי שימוש מתקדמים.
סיכום
בפוסט הזה התייחסנו לדרכים שונות לשילוב קוד JavaScript ב-C++ כשעובדים עם WebAssembly.
הכללת קטעי קוד כאלה מאפשרת להביע רצפים ארוכים של פעולות בצורה נקייה ויעילה יותר, וליהנות מספריות של צד שלישי, ממשקי API חדשים של JavaScript ואפילו תכונות של תחביר JavaScript שעדיין לא ניתן להביע באמצעות C++ או Embind.