WebAssembly คืออะไรและมาจากไหน

นับตั้งแต่ที่เว็บกลายเป็นแพลตฟอร์มที่ไม่ได้มีไว้สำหรับเอกสารเท่านั้น แต่ยังมีแอปด้วย แอปที่ล้ำสมัยที่สุดบางแอปได้ทดสอบขีดจำกัดของเว็บเบราว์เซอร์ แนวทางที่จะ "ใกล้ชิดกับเมทัลมากขึ้น" โดยการเผชิญหน้ากับภาษาในระดับต่ำกว่าเพื่อปรับปรุงประสิทธิภาพนั้นพบในหลายๆ ภาษาในระดับที่สูงกว่า ตัวอย่างเช่น Java มี Java Native Interface สำหรับ JavaScript ภาษาระดับล่างนี้คือ WebAssembly บทความนี้จะอธิบายว่าภาษาแอสเซมบลีคืออะไร และเหตุใดจึงมีประโยชน์บนเว็บ จากนั้นจะอธิบายวิธีสร้าง WebAssembly ผ่านโซลูชันชั่วคราวของ asm.js

คุณเคยเขียนโปรแกรมเป็นภาษาแอสเซมบลีไหม ในการเขียนโปรแกรมคอมพิวเตอร์ ภาษา Assembly หรือที่มักเรียกกันสั้นๆ ว่า Assembly และมักเรียกสั้นๆ ว่า ASM หรือ asm คือภาษาโปรแกรมระดับต่ำใดๆ ที่มีการโต้ตอบกันอย่างมากระหว่างคำสั่งในภาษาและคำสั่งโค้ดเครื่องของสถาปัตยกรรม

ตัวอย่างเช่น เมื่อดูที่สถาปัตยกรรม Intel® 64 และ IA-32 (PDF) คำสั่ง MUL (สําหรับการคูณ) จะทําการคูณแบบไม่ลงนามของโอเปอเรนดแรก (โอเปอเรนดปลายทาง) กับโอเปอเรนดที่ 2 (โอเปอเรนดต้นทาง) และจัดเก็บผลลัพธ์ไว้ในโอเปอเรนดปลายทาง พูดให้เข้าใจง่ายคือ ออบเจ็กต์ปลายทางคือออบเจ็กต์โดยนัยที่อยู่ในรีจิสเตอร์ 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

ขั้นตอนแรกในการเขียนโค้ดแอสเซมบลีที่ไม่มีทรัพยากร Dependency ทางสถาปัตยกรรมคือ asm.js ซึ่งเป็น JavaScript ชุดย่อยที่เข้มงวดซึ่งสามารถใช้เป็นภาษาเป้าหมายระดับต่ำและมีประสิทธิภาพสำหรับคอมไพเลอร์ ภาษาย่อยนี้อธิบายอย่างมีประสิทธิภาพเกี่ยวกับเครื่องเสมือนที่ทำแซนด์บ็อกซ์สำหรับภาษาที่ไม่มีหน่วยความจำมาก เช่น C หรือ C++ การผสมผสานการตรวจสอบความถูกต้องแบบคงที่และแบบไดนามิกช่วยให้เครื่องมือ JavaScript ใช้กลยุทธ์การคอมไพล์แบบเพิ่มประสิทธิภาพล่วงหน้าสำหรับโค้ด asm.js ที่ถูกต้องได้ โค้ดที่เขียนด้วยภาษาที่มีการจัดประเภทแบบคงที่ซึ่งมีการจัดการหน่วยความจำด้วยตนเอง (เช่น C) ได้รับการแปลโดยคอมไพเลอร์แบบซอร์สต่อซอร์ส เช่น Emscripten เวอร์ชันแรก (ซึ่งอิงตาม LLVM)

ประสิทธิภาพได้รับการปรับปรุงโดยการจำกัดฟีเจอร์ภาษาไว้เฉพาะฟีเจอร์ที่รองรับ AOT Firefox 22 เป็นเบราว์เซอร์แรกที่รองรับ asm.js ซึ่งเปิดตัวภายใต้ชื่อ OdinMonkey Chrome เพิ่มการรองรับasm.jsในเวอร์ชัน 61 แม้ว่า asm.js จะยังคงทำงานในเบราว์เซอร์ได้ แต่ WebAssembly เข้ามาแทนที่แล้ว เหตุผลที่ควรใช้ asm.js ในตอนนี้คือใช้เป็นทางเลือกสําหรับเบราว์เซอร์ที่ไม่รองรับ WebAssembly

WebAssembly

WebAssembly เป็นภาษาระดับต่ำที่คล้ายกับ Assembly โดยมีรูปแบบไบนารีที่กะทัดรัดซึ่งทำงานได้อย่างมีประสิทธิภาพใกล้เคียงกับแบบเนทีฟ และมีภาษาต่างๆ เช่น C/C++ และ Rust รวมถึงภาษาอื่นๆ อีกมากมายที่มีเป้าหมายการคอมไพล์เพื่อให้ทำงานบนเว็บได้ เรากำลังดำเนินการรองรับภาษาที่มีการจัดการหน่วยความจำ เช่น Java และ Dart และควรพร้อมใช้งานในเร็วๆ นี้ หรือพร้อมใช้งานแล้วในบางกรณี เช่น Kotlin/Wasm WebAssembly ได้รับการออกแบบมาให้ทำงานควบคู่ไปกับ JavaScript ซึ่งช่วยให้ทั้ง 2 อย่างทำงานร่วมกันได้

นอกเหนือจากเบราว์เซอร์แล้ว โปรแกรม WebAssembly ยังทํางานในรันไทม์อื่นๆ ได้ด้วย WASI ซึ่งเป็นอินเทอร์เฟซระบบแบบโมดูลสำหรับ WebAssembly WASI สร้างขึ้นเพื่อให้นำไปใช้กับระบบปฏิบัติการต่างๆ ได้ โดยมีวัตถุประสงค์เพื่อความปลอดภัยและความสามารถในการทำงานในสภาพแวดล้อมที่ใช้แซนด์บ็อกซ์

โค้ด WebAssembly (โค้ดไบนารีซึ่งก็คือไบต์โค้ด) มีจุดประสงค์เพื่อให้เรียกใช้ใน Virtual Stack Machine (VM) แบบพกพา โดยไบต์โค้ดได้รับการออกแบบมาให้แยกวิเคราะห์และเรียกใช้ได้เร็วกว่า JavaScript และมีการนำเสนอโค้ดกะทัดรัด

การดำเนินการตามคำสั่งแบบแนวคิดจะดำเนินการผ่านตัวนับโปรแกรมแบบดั้งเดิมที่เลื่อนผ่านคำสั่ง ในทางปฏิบัติ เครื่องมือ Wasm ส่วนใหญ่จะคอมไพล์ไบต์โค้ด Wasm เป็นรหัสเครื่อง แล้วจึงเรียกใช้รหัสนั้น วิธีการแบ่งออกเป็น 2 หมวดหมู่ ดังนี้

  • คำสั่งการควบคุมที่สร้างตัวควบคุมจะสร้างค่าอาร์กิวเมนต์ขึ้นมาจากสแต็ก อาจเปลี่ยนตัวนับโปรแกรม และพุชค่าผลลัพธ์ไปยังสแต็ก
  • คำสั่งง่ายๆ ที่ดึงค่าอาร์กิวเมนต์ออกจากสแต็ก ใช้โอเปอเรเตอร์กับค่า แล้วดันค่าผลลัพธ์ลงในสแต็ก ตามด้วยการเลื่อนตัวนับโปรแกรมโดยนัย

ย้อนกลับไปที่ตัวอย่างก่อนหน้านี้ โค้ด 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 ทั้งหมดในซอฟต์แวร์ กล่าวคือ โค้ดของ Asm.js สามารถทำงานในเครื่องมือ JavaScript ใดก็ได้ (แม้จะไม่ได้เพิ่มประสิทธิภาพ) แต่ WebAssembly ต้องการฟังก์ชันใหม่ที่ผู้ให้บริการเบราว์เซอร์ทั้งหมดตกลงกันไว้ WebAssembly ประกาศในปี 2015 และเปิดตัวครั้งแรกในเดือนมีนาคม 2017 ได้กลายเป็นคําแนะนําของ W3C ในวันที่ 5 ธันวาคม 2019 W3C รักษามาตรฐานไว้ได้ด้วยการสนับสนุนจากผู้ให้บริการเบราว์เซอร์รายใหญ่ทั้งหมดและผู้ที่มีความสนใจรายอื่นๆ ตั้งแต่ปี 2017 เป็นต้นไป เราได้รองรับเบราว์เซอร์ทุกประเภท

WebAssembly มีการแสดงผล 2 แบบ ได้แก่ ข้อความและไบนารี สิ่งที่คุณเห็นด้านบนคือการแสดงผลเป็นข้อความ

การนําเสนอที่เป็นข้อความ

การแสดงข้อความจะอิงตาม S-expression และมักใช้นามสกุลไฟล์ .wat (สำหรับรูปแบบ TWeb) หากต้องการดำเนินการจริงๆ คุณสามารถเขียนข้อมูลด้วยตนเองได้ เมื่อนำตัวอย่างการคูณจากด้านบนมาปรับให้มีประโยชน์มากขึ้นโดยไม่มีการเขียนปัจจัยแบบฮาร์ดโค้ดอีกต่อไป คุณอาจเข้าใจโค้ดต่อไปนี้

(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" แสดงในคอนโซลเครื่องมือสำหรับนักพัฒนาเว็บ

นอกจากนี้ คุณยังคอมไพล์เป็น 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