- อธิบายอย่างละเอียดเกี่ยวกับกระบวนการนำ โค้ด C/C++ ไปพอร์ตเป็น WebAssembly ด้วย Emscripten เพื่อสร้างเว็บแอปที่ทำงานในเบราว์เซอร์ โดยอิงจากตัวอย่าง Rubik’s Cube solver จริง
- ครอบคลุม ปัญหาเฉพาะทางและวิธีแก้แบบเป็นขั้นตอน ที่พบในสภาพแวดล้อมเบราว์เซอร์/WebAssembly ตั้งแต่ Hello World ไปจนถึงมัลติเธรด, callback, persistent storage และ modularization
- เน้น การแก้ปัญหาในงานจริง เช่น การเริ่มต้นแบบ asynchronous ใน JavaScript, การ export ฟังก์ชัน, Web Worker และประเด็น Spectre, รวมถึงการบันทึกถาวรผ่าน IndexedDB ด้วย IDBFS
- ย้ำหลายครั้งว่า abstraction ของ Emscripten มัก “รั่ว” (leaky abstractions) ในการใช้งานจริง และชี้ว่าจำเป็นต้องเข้าใจข้อจำกัดกับโครงสร้างภายในของเว็บแพลตฟอร์ม
- เป็น คู่มือจากประสบการณ์จริง ที่ให้ความช่วยเหลือและเคล็ดลับเชิงปฏิบัติสำหรับ นักพัฒนาที่ต้องการย้ายไลบรารี C/C++ เดิมไปสู่เว็บ ผ่านประสบการณ์พอร์ตโค้ดเบส C ที่ซับซ้อนไปยังเว็บ โดยอาศัยเพียง ความรู้ JavaScript/HTML ฝั่งฟรอนต์เอนด์ในระดับพื้นฐาน
บทนำ
- เมื่อไม่นานมานี้ ผู้เขียนได้ทำโปรเจกต์นำ อัลกอริทึมคำตอบที่เหมาะที่สุดของ Rubik’s Cube มาสร้างเป็นเว็บแอป
- บันทึกกระบวนการคอมไพล์ Rubik’s Cube solver สำหรับการหาคำตอบแบบเหมาะที่สุดที่พัฒนาด้วย C ให้ทำงานในเว็บเบราว์เซอร์เป็น WebAssembly ผ่าน Emscripten
- เหตุผลหลักที่ใช้ WebAssembly คือเพื่อให้ได้ ประสิทธิภาพบนเว็บที่ใกล้เคียง native มาก เมื่อเทียบกับ JavaScript
- บทความนี้ไม่ใช่ทิวทอเรียลพัฒนาเว็บแบบดั้งเดิม แต่เป็น ‘เส้นทางแห่งความเจ็บปวด’ สำหรับ นักพัฒนาที่ต้องการย้ายโค้ด C/C++ เดิมไปสู่เว็บ
- แม้จะไม่มีประสบการณ์พัฒนาเว็บมากนัก ก็ยังตามได้หากรู้โครงสร้างพื้นฐานของ HTML, JavaScript และวิธีใช้ developer tools ของเบราว์เซอร์
การตั้งค่าสภาพแวดล้อม
- สามารถดูโค้ดตัวอย่างทั้งหมดได้ใน git repository, github
- จำเป็นต้องติดตั้ง Emscripten (ดูวิธีติดตั้งจากเว็บไซต์ทางการ) และใช้เว็บเซิร์ฟเวอร์อย่าง darkhttpd หรือ Python
http.server
- ตัวอย่างโค้ดในทิวทอเรียลนี้ผ่านการทดสอบบน Linux และตระกูล UNIX ผู้ใช้ Windows แนะนำให้ใช้ WSL(Windows Subsystem for Linux)
Hello World
- เมื่อคอมไพล์โค้ด Hello World ของ C ด้วยคำสั่ง
emcc -o index.html hello.c จะได้ไฟล์ 3 ไฟล์คือ index.html(หน้าเว็บ), index.wasm(WebAssembly bytecode), index.js(JavaScript glue code)
- สามารถทำงานได้ทั้งในเบราว์เซอร์และ Node.js โดยแต่ละสภาพแวดล้อมก็มีวิธีใช้งานที่ต่างกัน
- หากต้องการสร้างเฉพาะ
.wasm ให้ใช้ตัวเลือก -sSTANDALONE_WASM
- แม้ Emscripten จะสร้างเฉพาะ
.wasm ได้ แต่ในกรณีส่วนใหญ่ JavaScript glue code ก็ยังจำเป็น
Intermezzo I: WebAssembly คืออะไร?
- WebAssembly(WASM) คือภาษาระดับต่ำที่ทำงานบน virtual machine ประสิทธิภาพสูง ภายในเว็บเบราว์เซอร์
- WASM รองรับใน เบราว์เซอร์หลักทุกตัวมาตั้งแต่ปี 2017
- เดิมที Emscripten แปลงโค้ด C/C++ ไปเป็นส่วนย่อยของ JavaScript ที่ชื่อ asm.js แต่ภายหลังได้เปลี่ยนมาใช้ WASM เมื่อเทคโนโลยีนี้เกิดขึ้น
- มีรูปแบบการเขียนแบบข้อความด้วย และมีโครงสร้างแบบ stack-based เดิมทีรองรับเฉพาะสถาปัตยกรรม 32 บิต จึงใช้หน่วยความจำเกิน 4GB ไม่ได้ แต่ตอนนี้ WASM64 กำลังค่อย ๆ ถูกนำเข้าสู่เบราว์เซอร์
การบิลด์ไลบรารี
- ยกตัวอย่างพื้นฐานในการบิลด์ ฟังก์ชัน C ชื่อ multiply() เป็น WASM แล้วเรียกใช้จาก JavaScript
- ในการบิลด์แบบปกติ Emscripten จะเติม ขีดล่าง (_) ไว้หน้าชื่อฟังก์ชัน (เช่น
_multiply)
- หากต้องการเปิดให้เรียกใช้จากภายนอก ต้องระบุออปชัน -sEXPORTED_FUNCTIONS
- การโหลดไลบรารีมีขั้นตอนเริ่มต้นแบบ asynchronous ดังนั้นจึงต้องจัดการแบบ async ผ่าน onRuntimeInitialized หรือ
await
- โค้ดตัวอย่างอยู่ในโฟลเดอร์
01_library ของ repository
Intermezzo II: JavaScript และ DOM
- หากต้องการเข้าถึงและแก้ไของค์ประกอบของ HTML จาก JavaScript จำเป็นต้องใช้ Document Object Model(DOM)
- สามารถสร้าง UI แบบไดนามิกได้ด้วย event listener(addEventListener), operator/ฟังก์ชันในตัว และองค์ประกอบอื่น ๆ
- อธิบายโครงสร้างการเชื่อม HTML/JavaScript ขั้นพื้นฐานสำหรับตัวอย่างที่มี input, ปุ่ม และการแสดงผลลัพธ์
- ยังแนะนำวิธีใช้งานจริงและประเด็นที่ควรระวังในการแยก/รวม script (เช่น การใช้
defer, ลำดับการโหลดองค์ประกอบ DOM)
การทำไลบรารีให้เป็นโมดูลและการโหลด
- เพื่อให้สามารถรวม WASM library หลายตัว หรือ นำกลับมาใช้ซ้ำได้ทั้งใน Node.js และบนเว็บ สามารถบิลด์เป็นโมดูลได้ด้วยออปชัน MODULARIZE, EXPORT_NAME
- แนะนำให้ใช้ส่วนขยาย
.mjs (ES6 module) เพื่อความเข้ากันได้กับ Node.js
- สามารถใช้งานเป็นโมดูลได้ทั้งบนเว็บและใน Node ด้วยรูปแบบ import MyLibrary from ...
มัลติเธรด
- ใน WebAssembly สามารถพอร์ตโค้ดแบบมัลติเธรดที่อิง pthreads มาใช้เพื่อ เพิ่มประสิทธิภาพ ได้
- ภายในฟังก์ชันสามารถสร้างหลายเธรดเพื่อรัน งานคำนวณแบบขนาน (เช่น นับจำนวนจำนวนเฉพาะ)
- ตอนบิลด์ต้องใช้ออปชัน -pthread, -sPTHREAD_POOL_SIZE=
- ในเบราว์เซอร์จริง จำเป็นต้องเพิ่ม HTTP header เช่น Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp
- ตัวอย่างทั้งหมดดูได้ในโฟลเดอร์
03_threads ของ repository
Intermezzo III: Web Workers และ Spectre
- มัลติเธรดของ Emscripten ถูกทำผ่าน Web Workers (Web Workers เป็นโปรเซสแยกและสื่อสารกันด้วยโครงสร้างแบบ message-based)
- การใช้ shared memory(SharedArrayBuffer) มี ข้อจำกัดด้านความปลอดภัย
- หลังการเกิดช่องโหว่ Spectre ในปี 2018 จึงมีการบังคับใช้ข้อกำหนด cross-origin isolated และ header ที่เกี่ยวข้อง
ระวังการบล็อก main thread
- หากงานที่ใช้เวลานานไป บล็อก main UI thread ของเบราว์เซอร์ ประสบการณ์ผู้ใช้จะลดลงอย่างมาก
- เพื่อหลีกเลี่ยงปัญหานี้ จึงนำ web worker มาใช้ โดยแยกการจัดการ UI/อินพุตออกจากงานประมวลผลอย่างชัดเจน
- สื่อสารระหว่าง main thread กับ worker แบบ event-driven ด้วย postMessage, onmessage
- ภายใน web worker จะโหลดโมดูล Emscripten-WASM เพื่อรับผิดชอบเฉพาะงานคำนวณแบบ asynchronous
ฟังก์ชัน callback
- เมื่อต้องส่ง function pointer เป็นพารามิเตอร์ของฟังก์ชัน C จะไม่สามารถเชื่อมกับ function object ของ JavaScript ได้โดยอัตโนมัติ
- ต้องใช้ addFunction(), UTF8ToString() ที่ Emscripten มีให้ และตอนบิลด์ต้องเพิ่มออปชัน -sEXPORTED_RUNTIME_METHODS, -sALLOW_TABLE_GROWTH
- callback ต้องถูกเรียกบน main thread เท่านั้น จึงจะทำงานได้อย่างเสถียร (ไม่สามารถเข้าถึงจาก web worker ได้)
พื้นที่จัดเก็บแบบถาวร
- เพื่อเก็บข้อมูลไว้ถาวรในเบราว์เซอร์ของผู้ใช้ บทความนี้ใช้ IDBFS(ระบบไฟล์ที่อิง IndexedDB) ของ Emscripten
- ตอนบิลด์ต้องตั้งค่าเริ่มต้นด้วยแฟลก --lidbfs.js และ **--pre-js ** เป็นต้น
- ในโค้ด C สามารถใช้ฟังก์ชันอ่านเขียนไฟล์ (
fopen, fread, fwrite) ได้ตามเดิม แต่การสะท้อนข้อมูลจริง/ซิงก์ข้อมูลจะต้องทำ การแมปและ sync อย่างชัดเจนจากฝั่ง JavaScript
- ด้วยข้อจำกัดด้าน sandbox/ความปลอดภัยของเบราว์เซอร์ จึงเข้าถึง local file system โดยตรงได้เฉพาะใน Node.js ส่วนบนเบราว์เซอร์ต้องใช้แบ็กเอนด์อย่าง IDBFS เพื่อเก็บข้อมูลถาวรอย่างปลอดภัย
บทสรุป
- ตลอดทั้งทิวทอเรียลนี้ ผู้อ่านจะได้เรียนรู้วิธีเชิงปฏิบัติอย่างละเอียดในการนำ โค้ด native C/C++ ที่ซับซ้อน มารัน บนเบราว์เซอร์ ได้อย่างปลอดภัยและไม่สูญเสียประสิทธิภาพมากนัก โดยอาศัยเพียง JavaScript และ HTML ขั้นพื้นฐาน
- ในสภาพแวดล้อมจริง จะได้พบทั้งอุปสรรคและวิธีแก้ในแกนสำคัญทั้งหมด ไม่ว่าจะเป็น มัลติเธรด, callback, การประมวลผลแบบ asynchronous, การเชื่อมต่อ storage รวมถึงได้เรียนรู้การตั้งค่าที่เกี่ยวข้องและข้อจำกัดล่าสุดของเบราว์เซอร์
- สามารถนำ ตัวอย่างใน Git repository ไปประยุกต์ใช้และต่อยอดกับโปรเจกต์ของตนเองได้
1 ความคิดเห็น
ความคิดเห็นใน Hacker News