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

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

שפת Assembly

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

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

WebAssembly

WebAssembly היא שפה דמוית-הרכבה ברמה נמוכה, עם פורמט בינארי קומפקטי שפועלת עם ביצועים כמעט מקומיים, ומספקת שפות כמו C/C++ ו-Rust, ועוד שפות רבות עם יעד הידור (compilation) כדי שהן יפעלו באינטרנט. אנחנו עדיין עובדים על תמיכה בשפות שמנוהלות על ידי זיכרון, כמו 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 (לפורמט 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 וקובץ ה-wrapper של ה-HTML hello.html. כשמגישים את הקובץ hello.html משרת אינטרנט, "Hello World" מודפסת במסוף כלי הפיתוח.

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

אישורים

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