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

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

שפת Assembly

האם אי פעם תכנתת בשפת הרכבה? בתכנות מחשבים, שפת ההרכבה (assembly), לעיתים קרובות נקראת פשוט Assembly, ובדרך כלל ראשי התיבות היא ASM או asm, היא כל שפת תכנות ברמה נמוכה עם התאמה משמעותית בין ההוראות בשפה זו לבין הוראות קוד המכונה של הארכיטקטורה.

לדוגמה, כשבוחנים את Intel® 64 ו-IA-32 Architectures (PDF), ההוראה של 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. Chrome הוסיף תמיכה ב-asm.js בגרסה 61. למרות שה-asm.js עדיין פועל בדפדפנים, הוא הוחלף על ידי WebAssembly. הסיבה לשימוש ב-asm.js בשלב הזה היא חלופה לדפדפנים שאין להם תמיכה ב-WebAssembly.

WebAssembly

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

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

קוד WebAssembly (קוד בינארי, כלומר bytecode) מיועד להרצה במכונת סטאק וירטואלית (VM) ניידת. ה-bytecode נועד להיות מהיר יותר לניתוח ולהפעלה של 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 דרש פונקציונליות חדשה שעליה הסכימו כל ספקי הדפדפנים. כפי שהודענו בשנת 2015 והושק לראשונה במרץ 2017, WebAssembly הפך להמלצה של 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++. קיימים מהדרים אחרים לשפות אחרות כמו חלודה והרבה שפות אחרות. כדאי להשתמש בקוד ה-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 ואת קובץ wrapper של HTML hello.html. כששולחים את הקובץ hello.html משרת אינטרנט, הערך "Hello World" מודפס במסוף כלי הפיתוח.

יש גם דרך להדר ל-WebAssembly ללא wrapper של HTML:

$ emcc hello.c -o hello.js

כמו קודם, הפעולה הזו תיצור קובץ hello.wasm, אבל הפעם קובץ hello.js במקום קובץ wrapper של 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.