שימוש בממשקי API אסינכרוניים באינטרנט מ-WebAssembly

ממשקי ה-API של הקלט/פלט באינטרנט הם אסינכרוניים, אבל הם מסונכרנים ברוב שפות המערכת. מתי בהידור של קוד ל-WebAssembly, צריך לגשר בין ממשקי API מסוג אחד לשני — והגשר הזה אסינכרוני. בפוסט הזה תלמדו מתי ואיך להשתמש ב-Asyncify ואיך הוא פועל לעומק.

קלט/פלט בשפות מערכת

אתחיל בדוגמה פשוטה באות ג'. נניח שאתם רוצים לקרוא את שם המשתמש בקובץ, ולקבל ברכה אותם עם הכיתוב "שלום, (שם משתמש)!" message:

#include <stdio.h>

int main() {
    FILE *stream = fopen("name.txt", "r");
    char name[20+1];
    size_t len = fread(&name, 1, 20, stream);
    name[len] = '\0';
    fclose(stream);
    printf("Hello, %s!\n", name);
    return 0;
}

אומנם הדוגמה לא מניבה הרבה תוצאות, אבל היא כבר ממחישה משהו שתמצאו באפליקציה בכל גודל: קורא קלט מסוים מהעולם החיצוני, מעבד אותם באופן פנימי וכותב כפלט בחזרה לעולם החיצוני. כל אינטראקציה כזו עם העולם החיצוני מתרחשת דרך שנקראות פונקציות קלט-פלט, והן מקוצרות גם ל-I/O.

כדי לקרוא את השם ב-C, צריך לפחות שתי קריאות קלט/פלט חיוניות: fopen, לפתיחת הקובץ, fread כדי לקרוא ממנו נתונים. אחרי אחזור הנתונים, אפשר להשתמש בפונקציית קלט/פלט אחרת printf כדי להדפיס את התוצאה במסוף.

במבט ראשון הפונקציות האלה נראות די פשוטות, ואין צורך לחשוב פעמיים מכונות קריאה או כתיבה של נתונים. אבל בהתאם לסביבה, יכולות להיות הרבה דברים קורה בפנים:

  • אם קובץ הקלט נמצא בכונן מקומי, האפליקציה צריכה לבצע סדרה של לזיכרון ולדיסק ניגשים כדי לאתר את הקובץ, לבדוק את ההרשאות, לפתוח אותו לקריאה ולאחר מכן לקרוא חסימה לפי בלוק עד לאחזור מספר הבייטים המבוקש. זה יכול להיות די איטי, בהתאם למהירות הדיסק ולגודל המבוקש.
  • לחלופין, קובץ הקלט עשוי להיות ממוקם במיקום של רשת טעונה, ובמקרה כזה גם סטאק ניסיונות חוזרים לכל פעולה.
  • לסיום, לא בטוח ש-printf ידפיס פריטים במסוף ויכול להיות שהוא יופנה אוטומטית לקובץ או למיקום ברשת, ובמקרה כזה הוא יצטרך לבצע את אותם השלבים שלמעלה.

בקיצור, קלט/פלט עשוי להיות איטי ואי אפשר לחזות כמה זמן תימשך שיחה מסוימת במבט מהיר בקוד. בזמן שהפעולה פועלת, כל האפליקציה תיראה קפוא ולא מגיבים למשתמש.

הן לא מוגבלות ל-C או ל-C++. רוב שפות המערכת מציגות את כל קלט/פלט (I/O) בצורה ממשקי API סינכרוניים. לדוגמה, אם מתרגמים את הדוגמה ל-Rust, יכול להיות שה-API ייראה פשוט יותר, אבל אותם עקרונות חלים. אתם פשוט מבצעים שיחה ומחכים באופן סינכרוני עד שתחזיר את התוצאה, בזמן שהוא מבצע את כל הפעולות היקרות ובסופו של דבר מחזיר את התוצאה הפעלה:

fn main() {
    let s = std::fs::read_to_string("name.txt");
    println!("Hello, {}!", s);
}

אבל מה קורה כשמנסים להרכיב כל אחת מהדוגמאות האלה ל-WebAssembly ולתרגם אותן באינטרנט? או, כדי לספק דוגמה ספציפית, מה יכול להיות "קובץ נקרא" תרגום הפעולה ל-? כן צריכים לקרוא נתונים מאחסון מסוים.

מודל אסינכרוני של האינטרנט

באינטרנט יש מגוון אפשרויות אחסון שונות שאפשר למפות, כמו אחסון בזיכרון (JS אובייקטים), localStorage, IndexedDB, אחסון בצד השרת, ו-File System Access API חדש.

עם זאת, אפשר להשתמש רק בשניים מממשקי ה-API האלה – האחסון בזיכרון ו-localStorage – באופן סינכרוני, ושתי האפשרויות הן האפשרויות המגבילות ביותר מבחינת מה שאפשר לאחסן ולמשך כמה זמן. הכול האפשרויות האחרות מספקות רק ממשקי API אסינכרוניים.

זהו אחד ממאפייני הליבה של הרצת קוד באינטרנט: כל פעולה שגוזלת זמן רב, שכולל כל קלט/פלט (I/O), חייב להיות אסינכרוני.

הסיבה לכך היא שכל קוד של משתמש נוגע בממשק המשתמש, ומובילים אותו בעבר לשרשורים בודדים. חייבת לרוץ באותו שרשור שבו נמצא ממשק המשתמש. הוא צריך להתחרות במשימות חשובות אחרות, הפריסה, הרינדור והטיפול באירועים של זמן המעבד (CPU). שאין צורך בקטע של JavaScript WebAssembly יכולה להתחיל 'קריאת קובץ' פעולה וחסימה של כל השאר - כל הכרטיסייה, או בעבר, הדפדפן כולו - למשך טווח שבין אלפיות שנייה לכמה שניות, עד שהוא מסתיים.

במקום זאת, הקוד יכול לתזמן רק פעולת קלט/פלט (I/O) יחד עם קריאה חוזרת לביצוע כשהסרטון יסתיים. קריאות חוזרות כאלה מבוצעות כחלק מלולאת האירועים של הדפדפן. אני לא אהיה בהמשך לפרטים נוספים, אבל אם אתם מעוניינים ללמוד איך לולאת האירועים פועלת מאחורי הקלעים, לתשלום משימות, מיקרו-משימות, תורים ולוחות זמנים שמסביר את הנושא הזה לעומק.

הגרסה הקצרה היא שהדפדפן מריץ את כל קטעי הקוד בסוג של לולאה אינסופית, על ידי לוקחים אותן אחת אחרי השנייה. כשאירוע כלשהו מופעל, הדפדפן מוסיף את התג לרשימת 'הבאים בתור' ובאיטרציה הבאה של ה-handler, הוא מוציא מהתור ומריץ. המנגנון הזה מאפשר לבצע סימולציות של בו-זמניות ולהפעיל הרבה פעולות מקבילות תוך שימוש רק שרשור יחיד.

הדבר שחשוב לזכור לגבי מנגנון זה הוא שבעוד שלמרות שה-JavaScript המותאם אישית (או הקוד של WebAssembly) פועל, לולאת האירוע חסומה, ולמרות שהיא פועלת אין דרך להגיב רכיבי handler חיצוניים, אירועים, קלט/פלט (I/O) וכו'. הדרך היחידה להחזיר את תוצאות הקלט/פלט היא לרשום קריאה חוזרת, סיום הרצת הקוד והעברת הבקרה חזרה לדפדפן, כדי שהוא יוכל לשמור בתהליך עיבוד של כל המשימות הממתינות. לאחר שהקלט/פלט (I/O) יסתיים, ה-handler יהפוך לאחת מהמשימות האלה יבוצע.

לדוגמה, אם רציתם לשכתב את הדוגמאות שלמעלה ב-JavaScript מודרני והחלטתם לקרוא מכתובת אתר מרוחקת, עליך להשתמש ב- Fetch API ובתחביר async-await:

async function main() {
  let response = await fetch("name.txt");
  let name = await response.text();
  console.log("Hello, %s!", name);
}

למרות שהוא נראה סינכרוני, כל await הוא למעשה תחביר סוכר, התקשרות חזרה:

function main() {
  return fetch("name.txt")
    .then(response => response.text())
    .then(name => console.log("Hello, %s!", name));
}

בדוגמה הזו ללא הסוכרים, שהיא קצת יותר ברורה, מתחילה בקשה והתגובות מתבצעות בהתקשרות החוזרת הראשונה. אחרי שהדפדפן מקבל את התגובה הראשונית — רק ה-HTTP כותרות - הוא מפעיל את הקריאה החוזרת באופן אסינכרוני. הקריאה החוזרת מתחילה להקריא את הגוף כטקסט באמצעות response.text(), והרשמה לתוצאה באמצעות קריאה חוזרת (callback) אחרת. לבסוף, ברגע ש-fetch מאחזר את כל התוכן, מפעיל את הקריאה החוזרת האחרונה, שמדפיסה את הכיתוב 'שלום, (שם משתמש)!' ל במסוף.

בשל האופי האסינכרוני של השלבים האלה, הפונקציה המקורית יכולה להחזיר את השליטה ברגע שהקלט/פלט תוזמן, ולהשאיר את כל ממשק המשתמש פעיל וזמין לבצע משימות אחרות, כולל עיבוד, גלילה וכן הלאה, בזמן שהקלט/פלט (I/O) מבוצע ברקע.

לדוגמה אחרונה, אפילו ממשקי API פשוטים כמו "שינה", שגורמים לאפליקציה להמתין מספר השניות הן גם סוג של פעולת קלט/פלט (I/O):

#include <stdio.h>
#include <unistd.h>
// ...
printf("A\n");
sleep(1);
printf("B\n");

בטח, יש לך אפשרות לתרגם את זה בצורה פשוטה, שתחסום את השרשור הנוכחי עד לסיום הזמן:

console.log("A");
for (let start = Date.now(); Date.now() - start < 1000;);
console.log("B");

למעשה, זה בדיוק מה ש-Emscripten עושה ביישום ברירת המחדל שלו "שינה", אבל זה מאוד לא יעיל, הוא יחסום את כל ממשק המשתמש ולא יאפשר טיפול באירועים אחרים בינתיים. בדרך כלל אין לעשות זאת בקוד הייצור.

במקום זאת, יש גרסה אידיומטית יותר של 'שינה' ב-JavaScript כרוך בקריאה ל-setTimeout(), הרשמה באמצעות handler:

console.log("A");
setTimeout(() => {
    console.log("B");
}, 1000);

מה משותף לכל הדוגמאות וממשקי ה-API האלה? בכל מקרה, הקוד האידיומטי בשפת המערכות נעשה שימוש ב-API לחסימה עבור הקלט/פלט, ואילו דוגמה מקבילה לאינטרנט משתמשת ב- במקום זאת, הוא אסינכרוני. במהלך ההידור לאינטרנט, צריך לבצע איכשהו טרנספורמציה בין שני סוגי התווים האלה וב-WebAssembly אין עדיין יכולת מובנית לעשות זאת.

גשר על הפער באמצעות Asyncify

כאן נכנסים לתמונה Asyncify. אסינכרוני הוא תכונה של זמן הידור (compile-time) שנתמכת על ידי Emscripten, שמאפשרת להשהות את התוכנית כולה להמשיך אותו באופן אסינכרוני מאוחר יותר.

תרשים שיחה
תיאור JavaScript -> WebAssembly - > Web API -> הפעלה של משימה אסינכרונית, שבה
התוצאה של המשימה האסינכרונית בחזרה ל-WebAssembly

שימוש ב-C / C++ עם Emscripten

אם רוצים להשתמש ב-Asyncify כדי להטמיע שינה אסינכרונית בדוגמה האחרונה, אפשר לבצע את הפעולות הבאות כך:

#include <stdio.h>
#include <emscripten.h>

EM_JS(void, async_sleep, (int seconds), {
    Asyncify.handleSleep(wakeUp => {
        setTimeout(wakeUp, seconds * 1000);
    });
});

puts("A");
async_sleep(1);
puts("B");

EM_JS הוא מאקרו שמאפשר להגדיר קטעי קוד של JavaScript כאילו הם פונקציות C. בתוכו, יש להשתמש בפונקציה Asyncify.handleSleep() שמנחה את Emscripten להשעות את התוכנית ומספק handler של wakeUp() שאמור להיות בוצעה קריאה אחרי שהפעולה האסינכרונית הסתיימה. בדוגמה שלמעלה, ה-handler מועבר אל setTimeout(), אבל אפשר להשתמש בו בכל הקשר אחר שבו אפשר לבצע קריאה חוזרת (callback). לבסוף, אפשר קוראים לפונקציה async_sleep() בכל מקום שרוצים, בדיוק כמו sleep() רגיל או כל API סינכרוני אחר.

במהלך הידור של קוד כזה, עליכם לומר ל-Emscripten להפעיל את התכונה Asyncify. עושים זאת עד העברת -s ASYNCIFY וגם -s ASYNCIFY_IMPORTS=[func1, func2] עם רשימת פונקציות דמויות מערך של פונקציות שעשויות להיות אסינכרוניות.

emcc -O2 \
    -s ASYNCIFY \
    -s ASYNCIFY_IMPORTS=[async_sleep] \
    ...

כך מערכת Emscripten יודעת שקריאות לפונקציות האלה עשויות לדרוש שמירה ושחזור של ולכן המהדר יוסיף קוד תומך סביב קריאות כאלה.

עכשיו, כשמריצים את הקוד בדפדפן, רואים יומן פלט חלק כמו שציפיתם, עם B מגיע אחרי עיכוב קצר אחרי A.

A
B

אפשר להחזיר ערכים מ- גם פונקציות Asyncify. מה צריך להחזיר את התוצאה של handleSleep() ולהעביר אותה אל wakeUp() קריאה חוזרת. לדוגמה, אם במקום לקרוא מקובץ, רוצים לאחזר מספר מרחוק משאב, תוכלו להשתמש בקטע קוד כמו זה שבהמשך כדי לשלוח בקשה, להשעות את קוד ה-C להמשיך את ההפעלה לאחר אחזור גוף התשובה — והכול נעשה בצורה חלקה כאילו השיחה הייתה מסונכרנת.

EM_JS(int, get_answer, (), {
     return Asyncify.handleSleep(wakeUp => {
        fetch("answer.txt")
            .then(response => response.text())
            .then(text => wakeUp(Number(text)));
    });
});
puts("Getting answer...");
int answer = get_answer();
printf("Answer is %d\n", answer);

למעשה, בממשקי API שמבוססים על Promise כמו fetch(), אפשר אפילו לשלב את Asyncify עם את התכונה async-await במקום להשתמש ב-API שמבוסס על קריאה חוזרת. לכן, במקום Asyncify.handleSleep(), שיחה אל Asyncify.handleAsync(). לאחר מכן, במקום שתצטרכו לתזמן קריאה חוזרת של wakeUp(), אפשר להעביר פונקציית JavaScript async ולהשתמש ב-await וב-return ולהבטיח שהקוד ייראה אפילו יותר טבעי וסינכרוני, מבלי לוותר על היתרונות של את הקלט/פלט האסינכרוני.

EM_JS(int, get_answer, (), {
     return Asyncify.handleAsync(async () => {
        let response = await fetch("answer.txt");
        let text = await response.text();
        return Number(text);
    });
});

int answer = get_answer();

בהמתנה לערכים מורכבים

אך הדוגמה הזו עדיין מגבילה אותך למספרים בלבד. מה אם רוצים ליישם את הגרסה המקורית לדוגמה, איפה ניסיתי לקבל שם של משתמש מקובץ כמחרוזת? ובכן, אתם יכולים לעשות גם את זה!

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

val fetch = val::global("fetch");
val response = fetch(std::string("answer.txt")).await();
val text = response.call<val>("text").await();
auto answer = text.as<std::string>();

כשמשתמשים בשיטה הזו, אין אפילו צורך להעביר את ASYNCIFY_IMPORTS כדגל הידור (compile), נכללות כבר כברירת מחדל.

טוב, אז הכול עובד מצוין ב-Emscripten. מה לגבי שפות וכלים אחרים?

שימוש משפות אחרות

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

קודם כול צריך להגדיר פונקציה כזו כייבוא רגיל באמצעות בלוק extern (או לחלופין של שפה זרה לפונקציות זרות).

extern {
    fn get_answer() -> i32;
}

println!("Getting answer...");
let answer = get_answer();
println!("Answer is {}", answer);

וכדי להדר את הקוד שלכם ל-WebAssembly:

cargo build --target wasm32-unknown-unknown

עכשיו צריך להגדיר אינסטרומנטציה לקובץ WebAssembly באמצעות קוד לאחסון או לשחזור של המקבץ. עבור C / C++ ו-Emscripten יעשה זאת עבורנו, אבל לא נעשה בו שימוש כאן, ולכן התהליך קצת יותר ידני.

למרבה המזל, הטרנספורמציה האסינכרונית היא לחלוטין נטולת כלים. היא יכולה לבצע טרנספורמציה שרירותית קובצי WebAssembly, לא משנה איזה מהדר הפיק אותו. הטרנספורמציה מסופקת בנפרד כחלק מהאופטימיזציה של wasm-opt מ-Binaryen Toolchain ואפשר להפעיל אותו כך:

wasm-opt -O2 --asyncify \
      --pass-arg=asyncify-imports@env.get_answer \
      [...]

מעבירים את הפרמטר --asyncify כדי להפעיל את הטרנספורמציה, ואז משתמשים ב---pass-arg=… כדי לציין רשימה של פונקציות אסינכרוניות, שבהן יש להשעות את מצב התוכנית ולהמשיך מאוחר יותר.

כל מה שנשאר זה לספק קוד תומך בזמן הריצה שבאמת יעשה את זה – להשעות ולהמשיך קוד WebAssembly. שוב, במקרה של C / C++ זה ייכלל ב-Emscripten, אבל עכשיו צריך קוד דבק מותאם אישית של JavaScript שיטפל בקובצי WebAssembly שרירותיים. יצרנו ספרייה בדיוק בשביל זה.

אפשר למצוא אותו ב-GitHub בכתובת https://github.com/GoogleChromeLabs/asyncify or npm מתחת לשם asyncify-wasm.

הוא מדמה יצירה של WebAssembly רגיל API, אבל במרחב שמות משלו. היחיד ההבדל הוא שבממשק API רגיל של WebAssembly אפשר לספק רק פונקציות סינכרוניות בזמן שבתוך Asyncify wrapper, אתם יכולים לספק גם ייבוא אסינכרוני:

const { instance } = await Asyncify.instantiateStreaming(fetch('app.wasm'), {
    env: {
        async get_answer() {
            let response = await fetch("answer.txt");
            let text = await response.text();
            return Number(text);
        }
    }
});

await instance.exports.main();

לאחר שתנסו לקרוא לפונקציה אסינכרונית כזו, כמו get_answer() בדוגמה שלמעלה – מתוך בצד WebAssembly, הספרייה תזהה את ה-Promise שהוחזר, תשהה ותשמור את המצב של את היישום WebAssembly, להירשם להשלמות ההבטחה, ולאחר מכן, לאחר שהבעיה תיפתר, לשחזר את מקבץ הקריאה והמצב בצורה חלקה ולהמשיך לבצע כאילו כלום לא קרה.

מכיוון שכל פונקציה במודול עשויה לבצע קריאה אסינכרונית, כל פעולות הייצוא עשויות להפוך אסינכרוניות, כדי שגם הם יהיו גלישה. ייתכן שבדוגמה שלמעלה הבחנתם צריך await את התוצאה של instance.exports.main() כדי לדעת מתי הביצוע באמת הסתיים.

איך כל זה פועל מאחורי הקלעים?

כש-Asyncify מזהה קריאה לאחת מהפונקציות של ASYNCIFY_IMPORTS, הוא מפעיל שיחה אסינכרונית שומר את כל המצב של האפליקציה, כולל מקבץ שיחות מקומיים ובהמשך, בסיום הפעולה, ישחזר את כל הזיכרון להמשיך מאותו מקום ובאותו מצב, כאילו התוכנית מעולם לא הופסקה.

התכונה הזו די דומה לתכונת ה-async-await שהראיתי קודם לכן ב-JavaScript, אבל בניגוד ל- JavaScript הראשון, לא מצריך תחביר מיוחד או תמיכה בסביבת זמן ריצה מהשפה. במקום זאת, עובדת על ידי טרנספורמציה של פונקציות סינכרוניות פשוטות בזמן הידור (compile).

בהרכבת הדוגמה לשינה אסינכרונית שהוצגה קודם לכן:

puts("A");
async_sleep(1);
puts("B");

אסינכרוני משתמש בקוד הזה ומשנה אותו כך שיהיה דומה לקוד הבא (פסאודו-קוד, אמיתי הטרנספורמציה הזו מעורבת יותר מזה):

if (mode == NORMAL_EXECUTION) {
    puts("A");
    async_sleep(1);
    saveLocals();
    mode = UNWINDING;
    return;
}
if (mode == REWINDING) {
    restoreLocals();
    mode = NORMAL_EXECUTION;
}
puts("B");

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

לאחר מכן, אחרי שהפלט של async_sleep() ישתנה, קוד התמיכה באסינכרוני ישתנה מ-mode ל-REWINDING. קוראים לפונקציה שוב. הפעם מבצעים את הפעולה 'הפעלה רגילה'. המערכת מדלגת על הסתעפות - מכיוון שכבר בוצע את העבודה בפעם האחרונה ואני רוצה להימנע מהדפסת האות A פעמיים, ובמקום זאת זה מגיע ישירות "rewinding" הסתעפות. ברגע שהוא יגיע, כל המיקומים השמורים ישוחזרו, והמצב ישתנה חזרה ל- 'רגיל' וממשיך לבצע את ההרצה כאילו הקוד מעולם לא הופסק מלכתחילה.

עלויות טרנספורמציה

למרבה הצער, הטרנספורמציה אסינכרונית אינה חינמית לחלוטין, מכיוון שהיא צריכה להזריק לא מעט קוד תומך לאחסון ולשחזור של כל התושבים המקומיים, מעבר לערימת השיחות מתחת מצבים שונים וכו'. הוא מנסה לשנות רק פונקציות שמסומנות כאסינכרוניות בפקודה וכל המתקשרים הפוטנציאליים שלהם, אך התקורה של גודל הקוד עדיין עשויה להסתכם בכ-50% לפני הדחיסה.

תרשים שמראה קוד
גודל תקורה עבור נקודות השוואה שונות, מ-0% בתנאים עדינים ועד יותר מ-100% במקרה הגרוע ביותר
מקרים

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

חשוב להקפיד להפעיל תמיד אופטימיזציה לגרסאות ה-build הסופיות כדי להימנע מהגדלה נוספת. אפשר בדוק גם אופטימיזציה ספציפית לאסינכרונית כדי להפחית את התקורה שמגבילה טרנספורמציות רק לפונקציות ספציפיות ו/או רק להפעלות של פונקציות ישירות. יש גם עלות מזערית לביצועים בזמן ריצה, אבל היא מוגבלת לקריאות האסינכרוניות עצמן. אבל, לעומת זאת, בהשוואה לעלות העבודה בפועל, היא בדרך כלל זניחה.

הדגמות בעולם האמיתי

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

כפי שהזכרנו בתחילת המאמר, אחת מאפשרויות האחסון באינטרנט היא File System Access API אסינכרוני. הוא מספק גישה את מערכת הקבצים האמיתית של המארח מאפליקציית אינטרנט.

לעומת זאת, יש סטנדרט דה-פקטו שנקרא WASI לקלט/פלט של WebAssembly במסוף ובצד השרת. הוא תוכנן כיעד הידור עבור חושפת כל מיני סוגים של מערכות קבצים ופעולות אחרות בצורה סינכרונית.

מה אם הייתם יכולים למפות אחד לשני? לאחר מכן תוכלו להדר אפליקציה בכל שפת מקור עם כל שרשרת כלים שתומכת ביעד WASI, ולהריץ אותו ב-Sandbox באינטרנט, ומאפשרת לו לפעול על קבצים של משתמשים אמיתיים. אפשר לעשות את זה בעזרת Asyncify.

בהדגמה הזו, קיבצתי מארז coreutils של Rust עם מספר תיקונים קטנים ל-WASI, שהועברו דרך טרנספורמציה אסינכרונית ומיושמת באופן אסינכרוני קישורים מ-WASI ל-File System Access API בצד JavaScript. לאחר השילוב עם רכיב הטרמינל Xterm.js שמספק מעטפת מציאותית שפועלת בדפדפן וביצוע פעולות על קבצים של משתמשים אמיתיים - בדיוק כמו טרמינל.

אפשר לצפות בו בשידור חי בכתובת https://wasi.rreverser.com/.

תרחישים לדוגמה של אסינכרוניים לא מוגבלים רק לטיימרים ולמערכות קבצים. אפשר להמשיך להשתמש ביותר ממשקי API נישתייים באינטרנט.

לדוגמה, בעזרת אסינכרוניזציה, ניתן למפות libusb – כנראה ספריית הנייטיב הכי פופולרית לעבודה איתה התקני USB - לWebUSB API, שמעניק גישה אסינכרונית למכשירים כאלה באינטרנט. אחרי מיפוי והידור, קיבלתי בדיקות ליבוס רגילות ודוגמאות להרצה על האנשים שנבחרו מכשירים בארגז החול של דף אינטרנט.

צילום מסך של Libusb
פלט ניפוי באגים בדף אינטרנט שמציג מידע על מצלמת Canon המחוברת

זה בטח עניין בפוסט אחר בבלוג.

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