מה זה WebAssembly ומאיפה הוא הגיע?

מאז שהאינטרנט הפך לפלטפורמה לא רק למסמכים אלא גם לאפליקציות, חלק מהאפליקציות המתקדמות ביותר דחפו את דפדפני האינטרנט למגבלות שלהם. הגישה של 'התקרבות למטאל' באמצעות ממשק עם שפות ברמה נמוכה יותר כדי לשפר את הביצועים קיימת בשפות רבות ברמה גבוהה יותר. לדוגמה, ל-Java יש את Java Native Interface. בשפת JavaScript, השפה ברמה הנמוכה יותר היא WebAssembly. במאמר הזה נסביר מהי שפת האסמבלי ולמה היא יכולה להיות שימושית באינטרנט. לאחר מכן נסביר איך נוצר WebAssembly באמצעות הפתרון הזמני asm.js.

שפת Assembly

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

לדוגמה, אם בוחנים את ארכיטקטורות Intel® 64 ו-IA-32 (PDF), ההוראה MUL (לטיפ: mul) מבצעת הכפלה לא חתומה של האופרנד הראשון (אופרנד יעד) והאופרנד השני (אופרנד מקור), ושומרת את התוצאה באופרנד היעד. בתרחיש פשוט מאוד, אופרנד היעד הוא אופרנד משתמע שנמצא ברישום AX, והאופרנד של המקור ממוקם ברישום לשימוש כללי כמו CX. התוצאה מאוחסנת שוב ברשומה AX. נבחן את הדוגמה הבאה של קוד x86:

mov ax, 5  ; Set the value of register AX to 5.
mov cx, 10 ; Set the value of register CX to 10.
mul cx     ; Multiply the value of register AX (5)
           ; and the value of register CX (10), and
           ; store the result in register AX.

לשם השוואה, אם הייתם צריכים להכפיל 5 ב-10, סביר להניח שתכתבו קוד דומה לקוד הבא ב-JavaScript:

const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;

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

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

asm.js

השלב הראשון בכתיבת קוד אסמבלר ללא יחסי תלות בארכיטקטורה היה asm.js, קבוצת משנה קפדנית של JavaScript שאפשר להשתמש בה כשפת יעד יעילה ברמה נמוכה למהדרנים. שפת המשנה הזו תיארה ביעילות מכונה וירטואלית ב-sandbox לשפות לא בטוחות לזיכרון כמו C או C++‎. שילוב של אימות סטטי ודינמי אפשר למנועי JavaScript להשתמש באסטרטגיית הידור לאופטימיזציה מראש (AOT) לקוד תקין של asm.js. קוד שנכתב בשפות עם סוגים סטטיים עם ניהול ידני של זיכרון (כמו C) תורגם על ידי מהדר ממקור למקור, כמו Emscripten המוקדם (מבוסס על LLVM).

כדי לשפר את הביצועים, הגבלנו את תכונות השפה לתכונות שניתנות ל-AOT. דפדפן Firefox 22 היה הדפדפן הראשון שתמך ב-asm.js, שפורסם בשם OdinMonkey. התמיכה ב-asm.js נוספה ל-Chrome בגרסה 61. אמנם asm.js עדיין פועל בדפדפנים, אבל הוא הוחלף על ידי WebAssembly. הסיבה לשימוש ב-asm.js בשלב הזה היא כחלופה לדפדפנים שאין בהם תמיכה ב-WebAssembly.

WebAssembly

WebAssembly היא שפת אסמבלי ברמה נמוכה עם פורמט בינארי קומפקטי שפועלת עם ביצועים שדומים לביצועים של שפות ילידיות. היא מספקת שפות כמו C/C++‏ ו-Rust, ועוד שפות רבות עם יעד הידור כדי שפעולתן תהיה באינטרנט. אנחנו עובדים על תמיכה בשפות עם ניהול זיכרון, כמו Java ו-Dart, והיא אמורה להיות זמינה בקרוב. במקרים מסוימים, התמיכה כבר זמינה, כמו ב-Kotlin/Wasm. WebAssembly תוכנן לפעול לצד JavaScript, ומאפשר לשניהם לפעול יחד.

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

קוד WebAssembly (קוד בינארי, כלומר קוד בייט) מיועד להרצה במכונה וירטואלית ניידת עם סטאק (VM). הבייטקוד נועד לנתח ולהפעיל אותו מהר יותר מ-JavaScript, ולהציג ייצוג קוד קומפקטי.

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

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

נחזור לדוגמה הקודמת. קוד WebAssembly הבא יהיה שווה ערך לקוד x86 מתחילת המאמר:

i32.const 5  ; Push the integer value 5 onto the stack.
i32.const 10 ; Push the integer value 10 onto the stack.
i32.mul      ; Pop the two most recent items on the stack,
             ; multiply them, and push the result onto the stack.

אמנם asm.js מוטמע בתוכנה, כלומר, הקוד שלו יכול לפעול בכל מנוע JavaScript (גם אם לא מותאם), אך WebAssembly דרש פונקציונליות חדשה שעליה הסכימו כל ספקי הדפדפנים. WebAssembly הוכרז ב-2015 והושק לראשונה במרץ 2017, והפך להמלצה של W3C ב-5 בדצמבר 2019. התקן מתוחזק על ידי W3C, עם תרומות מכל ספקי הדפדפנים הגדולים ומגורמים אחרים בעלי עניין. מאז 2017, התמיכה בדפדפנים היא אוניברסלית.

ל-WebAssembly יש שתי ייצוגים: טקסטואלי ובינארי. מה שמוצג למעלה הוא הייצוג הטקסטואלי.

ייצוג טקסטואלי

הייצוג הטקסטואלי מבוסס על ביטויים מסוג S, ובדרך כלל נעשה בו שימוש בקובץ עם סיומת .wat (לפורמט WebAssembly text). אם היית רוצה, אפשר לכתוב את זה בכתב יד. אם ניקח את דוגמת החישוב שלמעלה ונשפר אותה על ידי הפסקת קידוד הגורמים באופן קבוע, סביר להניח שתוכלו להבין את הקוד הבא:

(module
  (func $mul (param $factor1 i32) (param $factor2 i32) (result i32)
    local.get $factor1
    local.get $factor2
    i32.mul)
  (export "mul" (func $mul))
)

ייצוג בינארי

הפורמט הבינארי שמשתמש בסיומת הקובץ .wasm לא מיועד לשימוש אנושי, שלא לדבר על יצירה אנושית. באמצעות כלי כמו wat2wasm, אפשר להמיר את הקוד שלמעלה לייצוג הבינארי הבא. (הערות בדרך כלל לא נכללות בייצוג הבינארי, אלא נוספות על ידי הכלי wat2wasm כדי לשפר את הבנת הקוד).

0000000: 0061 736d                             ; WASM_BINARY_MAGIC
0000004: 0100 0000                             ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                    ; section code
0000009: 00                                    ; section size (guess)
000000a: 01                                    ; num types
; func type 0
000000b: 60                                    ; func
000000c: 02                                    ; num params
000000d: 7f                                    ; i32
000000e: 7f                                    ; i32
000000f: 01                                    ; num results
0000010: 7f                                    ; i32
0000009: 07                                    ; FIXUP section size
; section "Function" (3)
0000011: 03                                    ; section code
0000012: 00                                    ; section size (guess)
0000013: 01                                    ; num functions
0000014: 00                                    ; function 0 signature index
0000012: 02                                    ; FIXUP section size
; section "Export" (7)
0000015: 07                                    ; section code
0000016: 00                                    ; section size (guess)
0000017: 01                                    ; num exports
0000018: 03                                    ; string length
0000019: 6d75 6c                          mul  ; export name
000001c: 00                                    ; export kind
000001d: 00                                    ; export func index
0000016: 07                                    ; FIXUP section size
; section "Code" (10)
000001e: 0a                                    ; section code
000001f: 00                                    ; section size (guess)
0000020: 01                                    ; num functions
; function body 0
0000021: 00                                    ; func body size (guess)
0000022: 00                                    ; local decl count
0000023: 20                                    ; local.get
0000024: 00                                    ; local index
0000025: 20                                    ; local.get
0000026: 01                                    ; local index
0000027: 6c                                    ; i32.mul
0000028: 0b                                    ; end
0000021: 07                                    ; FIXUP func body size
000001f: 09                                    ; FIXUP section size
; section "name"
0000029: 00                                    ; section code
000002a: 00                                    ; section size (guess)
000002b: 04                                    ; string length
000002c: 6e61 6d65                       name  ; custom section name
0000030: 01                                    ; name subsection type
0000031: 00                                    ; subsection size (guess)
0000032: 01                                    ; num names
0000033: 00                                    ; elem index
0000034: 03                                    ; string length
0000035: 6d75 6c                          mul  ; elem name 0
0000031: 06                                    ; FIXUP subsection size
0000038: 02                                    ; local name type
0000039: 00                                    ; subsection size (guess)
000003a: 01                                    ; num functions
000003b: 00                                    ; function index
000003c: 02                                    ; num locals
000003d: 00                                    ; local index
000003e: 07                                    ; string length
000003f: 6661 6374 6f72 31            factor1  ; local name 0
0000046: 01                                    ; local index
0000047: 07                                    ; string length
0000048: 6661 6374 6f72 32            factor2  ; local name 1
0000039: 15                                    ; FIXUP subsection size
000002a: 24                                    ; FIXUP section size

הידור ל-WebAssembly

כפי שאתם רואים, גם .wat וגם .wasm לא נוחים במיוחד לשימוש. כאן נכנסים למשחק מהדרים כמו Emscripten. הוא מאפשר להדר מ-C ו-C++‎. יש גם קומפילרים אחרים לשפות אחרות, כמו Rust ועוד הרבה. נבחן את הקוד הבא ב-C:

#include <stdio.h>

int main() {
  printf("Hello World\n");
  return 0;
}

בדרך כלל, תוכנית ה-C הזו תעבור הידור באמצעות המהדרר gcc.

$ gcc hello.c -o hello

אם Emscripten מותקן, אפשר להדר אותו ל-WebAssembly באמצעות הפקודה emcc וכמעט אותם ארגומנטים:

$ emcc hello.c -o hello.html

הפעולה הזו תיצור קובץ hello.wasm ואת קובץ האריזה של ה-HTML‏ hello.html. כשמציגים את הקובץ hello.html משרת אינטרנט, "Hello World" מודפס במסוף DevTools.

יש גם דרך לבצע הידור ל-WebAssembly בלי מעטפת ה-HTML:

$ emcc hello.c -o hello.js

כמו קודם, הפעולה הזו תיצור קובץ hello.wasm, אבל הפעם קובץ hello.js במקום מעטפת ה-HTML. כדי לבדוק, מריצים את קובץ ה-JavaScript שנוצר, hello.js, באמצעות Node.js, למשל:

$ node hello.js
Hello World

מידע נוסף

המבוא הקצר הזה ל-WebAssembly הוא רק קצה הקרחון. מידע נוסף על WebAssembly זמין במסמכי התיעוד של WebAssembly ב-MDN, וגם במסמכי התיעוד של Emscripten. האמת היא שעבודה עם WebAssembly יכולה להרגיש קצת כמו המימ של איך לצייר ינשוף, במיוחד מכיוון שמפתחי אתרים שמכירים את HTML,‏ CSS ו-JavaScript לא בהכרח מומחים בשפות שצריך לבצע מהן הידור, כמו C. למרבה המזל, יש ערוצים כמו תג webassembly ב-StackOverflow שבהם מומחים בדרך כלל שמחים לעזור אם מבקשים בדרך נעימה.

תודות

הבדיקה של המאמר בוצעה על ידי Jakob Kummerow,‏ Derek Schuff ו-Rachel Andrew.