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

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

קלט/פלט (I/O) בשפות המערכת

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

#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;
}

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

כדי לקרוא את השם מ-C, נדרשות לפחות שתי קריאות חיוניות של קלט/פלט (I/O): 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 היא לרשום קריאה חוזרת (callback), לסיים את ביצוע הקוד ולהחזיר את המשימות שבהמתנה לדפדפן. בסיום קלט/פלט (I/O), ה-handler שלכם יהפוך לאחת מהמשימות האלה ויבוצע.

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

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() ונרשמת לתוצאה באמצעות קריאה חוזרת נוספת. בסוף התהליך, אחרי שמערכת fetch תאחזר את כל התוכן, תתבצע הפעלה של הקריאה החוזרת (callback) האחרונה, שמדפיס במסוף את הכיתוב "Hello, (username)!".

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

לדוגמה אחרונה, גם ממשקי API פשוטים כמו "sleep", שגורם לאפליקציה להמתין מספר מסוים של שניות, הם גם סוג של פעולת קלט/פלט (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 לחסימה עבור קלט/פלט (I/O), ובמקום זאת, בדוגמה מקבילה לאינטרנט נעשה שימוש בממשק API אסינכרוני. כשמבצעים הידור באינטרנט, צריך לעבור בדרך כלשהי בין שני המודלים לביצוע, ול-WebAssembly אין עדיין יכולת מובנית לעשות זאת.

גישור על הפער בעזרת אסינכרוני

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

תרשים קריאה שמתאר JavaScript -> WebAssembly -> Web API -> הפעלת משימה אסינכרונית, כאשר Asyncify מחבר את התוצאה של המשימה האסינכרונית בחזרה ל-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] \
    ...

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

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

A
B

אפשר גם להחזיר ערכים מפונקציות אסינכרוניות. מה שצריך לעשות הוא להחזיר את התוצאה של handleSleep() ולהעביר את התוצאה לקריאה החוזרת (callback) 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' של JavaScript במקום להשתמש ב-API שמבוסס על קריאה חוזרת. כדי לעשות זאת, במקום Asyncify.handleSleep(), צריך להתקשר אל Asyncify.handleAsync(). לאחר מכן, במקום לתזמן קריאה חוזרת (callback) של 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++. יש בו גם תמיכה ב-Asyncify, כך שאפשר לקרוא ל-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 כדגל הידור, כי הוא כבר נכלל כברירת מחדל.

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

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

נניח שיש לכם קריאה סינכרונית דומה במקום כלשהו בקוד Rust שאתם רוצים למפות ל-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 משרשרת הכלים של Biinaryen, וניתן להפעיל אותה באופן הבא:

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 או ב-npm מתחת לשם asyncify-wasm.

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

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

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

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

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

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

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

עלויות המרה

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

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

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

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

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

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

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

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

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

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

אפשר לצפות בו בזמן אמת בכתובת https://wasi.rreverser.com/.

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

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

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

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

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