Wasm Module แรกของคุณ
ทุกการเดินทาง Wasm เริ่มต้นด้วย loop เดียวกัน: เขียน module ใน WebAssembly Text format (WAT), คอมไพล์ เป็น bytes, instantiate และ เรียก ฟังก์ชันที่ export บทเรียนนี้พาคุณเดิน loop นั้นตั้งแต่ต้นจนจบด้วยตัวอย่างที่เล็กที่สุดเท่าที่จะเป็นไปได้ ฟังก์ชัน double ที่คูณ argument ด้วยสอง
เมื่อจบบทเรียนนี้ คุณจะได้เขียน WAT จริง, คอมไพล์ live ใน browser และเรียกใช้จาก JavaScript
การเขียน WAT Module
หัวข้อที่มีชื่อว่า “การเขียน WAT Module”WAT คือการแสดงข้อความที่มนุษย์อ่านได้ของ .wasm binary ต่อไปนี้คือ module ที่สมบูรณ์:
(module (func (export "double") (param $n i32) (result i32) local.get $n i32.const 2 i32.mul))มาแยกทุกบรรทัด:
(module ...)— wrapper ระดับบนสุด ทุกไฟล์.wasmมี module เดียว(func (export "double") ...)— ประกาศฟังก์ชันและทำให้ host มองเห็นได้ในชื่อ"double"ทันที ชื่อ export คือสิ่งที่ JavaScript เห็นบนinstance.exports(param $n i32)— ประกาศ parameter หนึ่งตัวชื่อ$nประเภทi32(32-bit signed integer) label$nเป็นความสะดวกของ WAT ที่ระดับ binary เป็นแค่ index(result i32)— ประกาศว่าฟังก์ชันนี้ returni32หนึ่งตัวlocal.get $n— push ค่าปัจจุบันของ$nลงบน operand stacki32.const 2— push ค่าคงที่2ลงบน stacki32.mul— pop ค่าบนสุดสองตัวของ stack, คูณเข้าด้วยกัน และ push ผลลัพธ์ เนื่องจากนี่คือคำสั่งสุดท้ายและ type ตรงกับ(result i32)ผลลัพธ์จึงกลายเป็น return value
การออกแบบ stack-based ของ Wasm หมายความว่าไม่มี return อย่างชัดเจน ค่าที่อยู่บนสุดของ stack เมื่อสิ้นสุด function body คือ return value
คอมไพล์: WAT Bytes ด้วย compileWat
หัวข้อที่มีชื่อว่า “คอมไพล์: WAT Bytes ด้วย compileWat”sandbox เปิดเผยฟังก์ชัน global compileWat(watString) ที่ขับเคลื่อนด้วย WABT library ฟังก์ชันนี้รับ WAT source string ของคุณแล้วคืน Promise<Uint8Array> ซึ่งคือ .wasm bytes ดิบที่ปกติคุณจะได้จากไฟล์บน disk หรือ network fetch
const bytes = await compileWat(wat);Instantiate: WebAssembly.instantiate
หัวข้อที่มีชื่อว่า “Instantiate: WebAssembly.instantiate”WebAssembly.instantiate(bytes, importObject) คือ standard browser API สำหรับสร้าง Wasm instance ที่รันได้ โดยคืน Promise ที่ resolve เป็นคู่ { instance, module }:
instance— object ที่ live และรันได้ property.exportsของตัวเองมีฟังก์ชันและ memory ทั้งหมดที่ module export ออกมาmodule—WebAssembly.Moduleที่คอมไพล์แล้วแต่ไม่มีชีวิต คุณสามารถแชร์และ instantiate ซ้ำได้โดยไม่ต้องคอมไพล์ใหม่
argument ที่สองคือ import object ที่เป็น nested object ของฟังก์ชัน, memory และ global ที่คุณ provide ให้ module จาก JavaScript เนื่องจากฟังก์ชัน double ของเราไม่ import อะไรจาก host เราจึงส่ง {}
const { instance } = await WebAssembly.instantiate(bytes, {});เรียก: instance.exports.double
หัวข้อที่มีชื่อว่า “เรียก: instance.exports.double”เมื่อ instantiate แล้ว ทุกฟังก์ชันที่ export ออกมาจะใช้งานได้เป็น JavaScript function ธรรมดาบน instance.exports:
instance.exports.double(21); // → 42JavaScript number ถูกแปลงอัตโนมัติไปเป็น/มาจาก i32 การเรียกเป็น synchronous ไม่ต้อง await
ลองรัน Live
หัวข้อที่มีชื่อว่า “ลองรัน Live”runner ด้านล่างคอมไพล์ WAT, instantiate และเรียก double ด้วย input ที่แตกต่างกันสามค่า ลองรันแล้วดูผลลัพธ์ใน console
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| เรียกฟังก์ชันผ่าน Wasm module | ความเร็วใกล้ native เมื่อ workload หนัก, ผลลัพธ์ deterministic | ต้องผ่าน loop compile → instantiate → call ทุกครั้งที่โหลด module ใหม่ ซึ่งมี overhead ที่ JS function ธรรมดาไม่มี |
| เรียกฟังก์ชัน JavaScript ตรงๆ | เรียกได้ทันทีไม่ต้อง compile/instantiate, debug ง่ายด้วย devtools | ไม่ได้ความเร็วระดับ native และไม่มี sandbox guarantee แบบ Wasm |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- เขียนฟังก์ชันง่ายๆ อย่าง
doubleเป็น Wasm module แล้วคาดหวังว่าจะเร็วกว่า JS เสมอ ทั้งที่ function เล็กขนาดนี้ JS JIT compile ให้เร็วพอๆ กันอยู่แล้ว ความต่างจะเห็นชัดก็ต่อเมื่อ workload หนักจริง - ลืมว่า
WebAssembly.instantiateต้องรันทุกครั้งก่อนเรียก export ได้ แล้วพยายาม migrate ทั้งแอปไปเป็น Wasm module ย่อยจำนวนมาก ทำให้ต้อง compile/instantiate ซ้ำๆ โดยไม่จำเป็น - ไม่ cache
WebAssembly.Moduleที่ compile ไว้แล้วเมื่อต้อง instantiate module เดิมซ้ำหลายครั้ง ทำให้เสีย cost ของ compile step ไปโดยเปล่าประโยชน์ โดยเฉพาะกับ module เล็กที่ถูกเรียกไม่บ่อย
💡 ตัวอย่างจากของจริง
Figma ใช้ compile-instantiate-call loop แบบเดียวกับในบทนี้ (แต่ในสเกลใหญ่กว่ามาก) เพื่อรัน rendering engine ที่คอมไพล์จาก C++ เป็น Wasm ทำให้โหลดไฟล์ design เร็วขึ้นราว 3 เท่าเทียบกับตอนที่ยังเป็น pure JavaScript