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

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

ภาษาแอสเซมบลี

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

ตัวอย่างเช่น เมื่อดูที่สถาปัตยกรรม Intel® 64 และ IA-32 (PDF) คำสั่ง MUL (สําหรับ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

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

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

โค้ด WebAssembly (โค้ดไบนารีหรือที่เรียกว่าไบต์โค้ด) มีไว้เพื่อเรียกใช้บนเครื่องเสมือนสแต็กแบบพกพา (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 (สำหรับรูปแบบข้อความ WebAssembly t) หากต้องการจริงๆ คุณก็เขียนด้วยมือได้ เมื่อนำตัวอย่างการคูณจากด้านบนมาปรับให้มีประโยชน์มากขึ้นโดยไม่กำหนดปัจจัยแบบฮาร์ดโค้ดอีกต่อไป คุณอาจเข้าใจโค้ดต่อไปนี้

(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