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

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

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

לדוגמה, לפי הארכיטקטורות Intel® 64 ו-IA-32 (PDF), ההוראה MUL (multiplication) מבצעת כפל ללא סימן של האופרנד הראשון (אופרנד היעד) והאופרנד השני (אופרנד המקור), ושומרת את התוצאה באופרנד היעד. בתרחיש פשוט מאוד, אופרנד היעד הוא אופרנד משתמע שנמצא ברישום 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. ב-5 בדצמבר 2019 הוא הפך להמלצה של W3C. התקן מתוחזק על ידי W3C, עם תרומות מכל ספקי הדפדפנים הגדולים ומגורמים אחרים בעלי עניין. מאז 2017, התמיכה בדפדפנים היא אוניברסלית.

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

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

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

תודות

המאמר הזה נבדק על ידי ג'קוב קומרוו, דרק שאף ורייצ'ל אנדרו.