C และ Emscripten
Emscripten คือ Toolchain คลาสสิกสำหรับนำโค้ด C และ C++ มาใช้บนเว็บ โดยคอมไพล์ไปยัง WebAssembly (หรือ asm.js สำหรับ Fallback รุ่นเก่า), จำลอง POSIX API และสามารถสร้าง JavaScript Loader ที่ช่วยจัดการ Instantiation ให้คุณโดยอัตโนมัติ
ติดตั้ง Emscripten
หัวข้อที่มีชื่อว่า “ติดตั้ง Emscripten”# Clone Emscripten SDKgit clone https://github.com/emscripten-core/emsdk.gitcd emsdk
# ติดตั้งและ Activate Release ล่าสุด./emsdk install latest./emsdk activate latest
# เพิ่ม emcc เข้า PATH (รันนี้ทุกครั้งที่เปิด Shell ใหม่ หรือเพิ่มใน Profile)source ./emsdk_env.sh
# ตรวจสอบemcc --versionเขียนฟังก์ชัน C
หัวข้อที่มีชื่อว่า “เขียนฟังก์ชัน C”สร้างไฟล์ math.c:
#include <math.h>
// คำนวณ Hypotenuse: sqrt(a*a + b*b)double hypotenuse(double a, double b) { return sqrt(a * a + b * b);}
// Fibonacci (แบบ Iterative)int fibonacci(int n) { if (n <= 1) return n; int a = 0, b = 1; for (int i = 2; i <= n; i++) { int tmp = a + b; a = b; b = tmp; } return b;}คอมไพล์ด้วย emcc
หัวข้อที่มีชื่อว่า “คอมไพล์ด้วย emcc”emcc math.c \ -o math.js \ -sEXPORTED_FUNCTIONS='["_hypotenuse","_fibonacci"]' \ -sEXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \ -sASSERTIONS=1 \ -O2คำสั่งนี้ผลิตไฟล์สองไฟล์:
math.wasm— Binary ที่คอมไพล์แล้วmath.js— JS Loader และ Runtime Glue
รายการ EXPORTED_FUNCTIONS ใช้ Underscore นำหน้าเพราะชื่อ Symbol ของ C จะถูกเติม _ ตามธรรมเนียมของ Emscripten
เรียกใช้จาก JavaScript ด้วย ccall / cwrap
หัวข้อที่มีชื่อว่า “เรียกใช้จาก JavaScript ด้วย ccall / cwrap”// ในเบราว์เซอร์: โหลด JS Loader ที่ Emscripten สร้างก่อน// <script src="math.js"></script>
Module.onRuntimeInitialized = function () { // ccall — เรียกครั้งเดียว: (ชื่อ, ประเภทคืนค่า, ประเภทอาร์กิวเมนต์, อาร์กิวเมนต์) const h = Module.ccall( 'hypotenuse', // ชื่อฟังก์ชัน C (ไม่มี _ นำหน้า) 'number', // ประเภทคืนค่า ['number', 'number'], // ประเภทอาร์กิวเมนต์ [3.0, 4.0] // ค่าอาร์กิวเมนต์ ); console.log('hypotenuse(3, 4) =', h); // 5
// cwrap — สร้างฟังก์ชัน JS ที่นำกลับมาใช้ซ้ำได้ const fib = Module.cwrap('fibonacci', 'number', ['number']); console.log('fibonacci(10) =', fib(10)); // 55 console.log('fibonacci(20) =', fib(20)); // 6765};Output หน้า HTML แบบ Standalone
หัวข้อที่มีชื่อว่า “Output หน้า HTML แบบ Standalone”Emscripten สามารถสร้าง HTML Shell ที่ฝัง Loader และ Canvas แบบ Terminal ไว้ด้วยกัน:
emcc math.c \ -o math.html \ -sEXPORTED_FUNCTIONS='["_hypotenuse","_fibonacci"]' \ -sEXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \ -O2เปิด math.html ใน browser และ Terminal Canvas ของ Emscripten จะปรากฏขึ้น ทุกการเรียก printf ใน C จะแสดงผลที่นั่นโดยอัตโนมัติ
การใช้ malloc และ String
หัวข้อที่มีชื่อว่า “การใช้ malloc และ String”เมื่อส่ง String ข้ามขอบเขต Wasm คุณต้อง Allocate และ Free Memory ด้วยตัวเอง:
#include <string.h>#include <stdlib.h>
// คืน Copy ของ Input บน Heap ที่เป็น Upper-casechar* to_upper(const char* s) { int len = strlen(s); char* out = malloc(len + 1); for (int i = 0; i <= len; i++) out[i] = (s[i] >= 'a' && s[i] <= 'z') ? s[i] - 32 : s[i]; return out;}emcc strings.c \ -o strings.js \ -sEXPORTED_FUNCTIONS='["_to_upper","_malloc","_free"]' \ -sEXPORTED_RUNTIME_METHODS='["ccall","cwrap","allocateUTF8","UTF8ToString"]' \ -O2Module.onRuntimeInitialized = function () { const ptr = Module.allocateUTF8('hello wasm'); const resultPtr = Module.ccall('to_upper', 'number', ['number'], [ptr]); console.log(Module.UTF8ToString(resultPtr)); // HELLO WASM Module._free(ptr); Module._free(resultPtr);};ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| C/C++ + Emscripten | Port Codebase C/C++ ที่มีอยู่แล้วได้เกือบทันที เข้าถึง Ecosystem C/C++ มหาศาลที่สะสมมาหลายสิบปี | Glue Code และ POSIX Emulation Layer เพิ่ม Runtime Overhead ทำให้ Binary ใหญ่กว่า Toolchain อื่นอย่างชัดเจน |
| Rust + wasm-bindgen | Binary เล็กกว่ามาก ไม่มี POSIX Emulation ให้แบก | ต้อง Rewrite โค้ดเดิมทั้งหมดเป็น Rust ไม่สามารถ Port ตรงๆ ได้ |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- Ship Build ที่ไม่ Optimise — ไม่ Strip POSIX Emulation Layer และไม่ใช้ Flag อย่าง
-O3หรือ-sMINIMAL_RUNTIMEทำให้ Bundle บวมโดยไม่จำเป็น - เลือก Emscripten เพียงเพราะทีมถนัด C แม้โปรเจกต์เป็น Module เล็กๆ ที่ไม่มี Codebase เดิมให้ Port ทำให้ต้อง Ship Runtime ใหญ่เกินความจำเป็นทั้งที่ Rust หรือ AssemblyScript เหมาะกว่า
- ลืม
Module._free()Pointer ที่ Allocate ด้วยmallocหรือallocateUTF8ทำให้เกิด Memory Leak สะสมในหน้าเว็บที่รันนาน
💡 ตัวอย่างจากของจริง
Figma คอมไพล์ C++ Rendering Engine ของตัวเองด้วย Emscripten เพื่อรันบนเว็บแอป ทำให้ได้ Performance ระดับ Desktop โดยไม่ต้อง Rewrite Engine ทั้งหมดเป็นภาษาอื่น