מאז שהאינטרנט הפך לפלטפורמה לא רק למסמכים אלא גם לאפליקציות, חלק מהאפליקציות המתקדמות ביותר דוחפות את דפדפני האינטרנט לקצה גבול היכולת שלהם. הגישה של "התקרבות למתכת" על ידי אינטראקציה עם שפות ברמה נמוכה יותר כדי לשפר את הביצועים נפוצה בשפות רבות ברמה גבוהה יותר. לדוגמה, ב-Java יש ממשק Java מקורי. ב-JavaScript, השפה ברמה הנמוכה יותר היא WebAssembly. במאמר הזה נסביר מהי שפת סף, למה היא יכולה להיות שימושית באינטרנט, ואיך נוצר WebAssembly באמצעות הפתרון הזמני asm.js.
שפת Assembly
האם יצא לך לתכנת בשפת אסמבלי? בתכנות מחשבים, שפת אסמבלי (Assembly), שלעתים קרובות נקראת פשוט Assembly ומקוצרת בדרך כלל ל-ASM או ל-asm, היא כל שפת תכנות ברמה נמוכה עם התאמה חזקה מאוד בין ההוראות בשפה לבין הוראות קוד המכונה של הארכיטקטורה.
לדוגמה, אם בודקים את Intel® 64 and IA-32 Architectures (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 שאפשר להשתמש בה כשפת יעד יעילה ברמה נמוכה עבור קומפיילרים. שפת המשנה הזו תיארה למעשה מכונה וירטואלית בסביבת ארגז חול לשפות לא בטוחות מבחינת ניהול זיכרון, כמו C או C++. שילוב של אימות סטטי ודינמי אפשר למנועי JavaScript להשתמש באסטרטגיית קומפילציה אופטימלית מראש (AOT) לקוד asm.js תקין. קוד שנכתב בשפות עם הקלדה סטטית וניהול זיכרון ידני (כמו C) תורגם על ידי קומפיילר מקוד מקור לקוד מקור, כמו גרסה מוקדמת של Emscripten (מבוססת על LLVM).
הביצועים שופרו על ידי הגבלת תכונות השפה לאלה שמתאימות ל-AOT. Firefox 22 היה הדפדפן הראשון שתמך ב-asm.js, והוא הופץ בשם OdinMonkey. בגרסה 61 של Chrome נוספה תמיכה ב-asm.js. למרות ש-asm.js עדיין פועל בדפדפנים, הוא הוחלף ב-WebAssembly. הסיבה לשימוש ב-asm.js בשלב הזה היא כחלופה לדפדפנים שלא תומכים ב-WebAssembly.
WebAssembly
WebAssembly היא שפה ברמה נמוכה שדומה לשפת אסמבלי (Assembly) עם פורמט בינארי קומפקטי שפועלת עם ביצועים כמעט מקוריים, ומספקת שפות כמו C/C++ ו-Rust, ועוד רבות אחרות עם יעד קומפילציה כדי שהן יפעלו באינטרנט. אנחנו עובדים על תמיכה בשפות עם ניהול זיכרון כמו Java ו-Dart, והיא תהיה זמינה בקרוב. לחלופין, היא כבר זמינה כמו במקרה של Kotlin/Wasm. WebAssembly נועד לפעול לצד JavaScript, כך ששניהם יכולים לעבוד יחד.
בנוסף לדפדפן, תוכניות WebAssembly פועלות גם בסביבות זמן ריצה אחרות, הודות ל-WASI, ממשק המערכת של WebAssembly, שהוא ממשק מערכת מודולרי ל-WebAssembly. ממשק WASI נוצר כדי להיות נייד בין מערכות הפעלה, במטרה להיות מאובטח ולפעול בסביבת ארגז חול.
קוד WebAssembly (קוד בינארי, כלומר קוד בייט) מיועד להרצה במכונת מחסנית וירטואלית (VM) ניידת. הבייטקוד נועד להיות מהיר יותר בניתוח ובהרצה בהשוואה ל-JavaScript, וגם לייצג קוד בצורה קומפקטית.
הביצוע הקונספטואלי של ההוראות מתבצע באמצעות מונה תוכניות מסורתי שמתקדם בין ההוראות. בפועל, רוב מנועי ה-Wasm מהדרים את ה-bytecode של 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). אם ממש רוצים, אפשר לכתוב אותו בכתב יד. אם ניקח את דוגמת הכפל שלמעלה ונשפר אותה כך שהגורמים לא יהיו מוצפנים יותר, סביר להניח שתבינו את הקוד הבא:
(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 hello.html
. כשמגישים את הקובץ hello.html
משרת אינטרנט, רואים את "Hello World"
מודפס במסוף של כלי הפיתוח.
יש גם דרך לבצע קומפילציה ל-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.