นับตั้งแต่เว็บกลายเป็นแพลตฟอร์มที่ไม่ใช่แค่สำหรับเอกสาร แต่ยังเป็นแพลตฟอร์มสำหรับแอปด้วย แอปที่ล้ำสมัยที่สุดบางแอปก็ทำให้เว็บเบราว์เซอร์ทำงานจนถึงขีดจำกัด แนวทางในการ "เข้าถึงระดับล่างสุด" โดยการเชื่อมต่อกับภาษาในระดับที่ต่ำกว่าเพื่อปรับปรุงประสิทธิภาพพบได้ในภาษาในระดับที่สูงกว่าหลายภาษา ตัวอย่างเช่น Java มี Java Native Interface สำหรับ JavaScript ภาษาที่ระดับต่ำกว่านี้คือ WebAssembly ในบทความนี้ คุณจะได้ทราบว่าภาษาแอสเซมบลีคืออะไร และเหตุใดจึงมีประโยชน์บนเว็บ จากนั้นจะได้เรียนรู้วิธีสร้าง WebAssembly ผ่านโซลูชันชั่วคราวของ asm.js
ภาษา Assembly
คุณเคยเขียนโปรแกรมในภาษาแอสเซมบลีไหม ในด้านการเขียนโปรแกรมคอมพิวเตอร์ ภาษาแอสเซมบลี (Assembly Language) ซึ่งมักเรียกกันสั้นๆ ว่าแอสเซมบลี (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
ขั้นตอนแรกในการเขียนโค้ดแอสเซมบลีโดยไม่มีการขึ้นต่อกันของสถาปัตยกรรมคือ asm.js ซึ่งเป็นส่วนย่อยที่เข้มงวดของ JavaScript ที่ใช้เป็นภาษาเป้าหมายที่มีประสิทธิภาพระดับต่ำสำหรับคอมไพเลอร์ได้ ภาษาย่อยนี้อธิบายเครื่องเสมือนแบบแซนด์บ็อกซ์สำหรับภาษาที่ไม่ปลอดภัยในหน่วยความจำ เช่น C หรือ C++ ได้อย่างมีประสิทธิภาพ การผสมผสานการตรวจสอบแบบคงที่และแบบไดนามิกช่วยให้เครื่องมือ JavaScript ใช้กลยุทธ์การคอมไพล์การเพิ่มประสิทธิภาพแบบล่วงหน้า (AOT) สำหรับโค้ด asm.js ที่ถูกต้องได้ คอมไพเลอร์แบบ Source-to-Source เช่น Emscripten รุ่นแรกๆ (อิงตาม LLVM) จะแปลโค้ดที่เขียนในภาษาที่พิมพ์แบบคงที่ซึ่งมีการจัดการหน่วยความจำด้วยตนเอง (เช่น C)
เราปรับปรุงประสิทธิภาพโดยจำกัดฟีเจอร์ภาษาให้เฉพาะฟีเจอร์ที่เหมาะกับ 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 bytecode เป็นโค้ดเครื่อง แล้วจึงเรียกใช้โค้ดนั้น วิธีการแบ่งออกเป็น 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 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"
พิมพ์ลงในคอนโซลเครื่องมือสำหรับนักพัฒนาเว็บ
นอกจากนี้ยังมีวิธีคอมไพล์เป็น 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