ข้ามไปยังเนื้อหา

C และ Emscripten

Emscripten คือ Toolchain คลาสสิกสำหรับนำโค้ด C และ C++ มาใช้บนเว็บ โดยคอมไพล์ไปยัง WebAssembly (หรือ asm.js สำหรับ Fallback รุ่นเก่า), จำลอง POSIX API และสามารถสร้าง JavaScript Loader ที่ช่วยจัดการ Instantiation ให้คุณโดยอัตโนมัติ

Terminal window
# Clone Emscripten SDK
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
# ติดตั้งและ Activate Release ล่าสุด
./emsdk install latest
./emsdk activate latest
# เพิ่ม emcc เข้า PATH (รันนี้ทุกครั้งที่เปิด Shell ใหม่ หรือเพิ่มใน Profile)
source ./emsdk_env.sh
# ตรวจสอบ
emcc --version

สร้างไฟล์ 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;
}
Terminal window
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

// ในเบราว์เซอร์: โหลด 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
};

Emscripten สามารถสร้าง HTML Shell ที่ฝัง Loader และ Canvas แบบ Terminal ไว้ด้วยกัน:

Terminal window
emcc math.c \
-o math.html \
-sEXPORTED_FUNCTIONS='["_hypotenuse","_fibonacci"]' \
-sEXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \
-O2

เปิด math.html ใน browser และ Terminal Canvas ของ Emscripten จะปรากฏขึ้น ทุกการเรียก printf ใน C จะแสดงผลที่นั่นโดยอัตโนมัติ

เมื่อส่ง String ข้ามขอบเขต Wasm คุณต้อง Allocate และ Free Memory ด้วยตัวเอง:

#include <string.h>
#include <stdlib.h>
// คืน Copy ของ Input บน Heap ที่เป็น Upper-case
char* 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;
}
Terminal window
emcc strings.c \
-o strings.js \
-sEXPORTED_FUNCTIONS='["_to_upper","_malloc","_free"]' \
-sEXPORTED_RUNTIME_METHODS='["ccall","cwrap","allocateUTF8","UTF8ToString"]' \
-O2
Module.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);
};
ตัวเลือกBenefitCost
C/C++ + EmscriptenPort Codebase C/C++ ที่มีอยู่แล้วได้เกือบทันที เข้าถึง Ecosystem C/C++ มหาศาลที่สะสมมาหลายสิบปีGlue Code และ POSIX Emulation Layer เพิ่ม Runtime Overhead ทำให้ Binary ใหญ่กว่า Toolchain อื่นอย่างชัดเจน
Rust + wasm-bindgenBinary เล็กกว่ามาก ไม่มี 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 ทั้งหมดเป็นภาษาอื่น

ทำไม Entry ใน EXPORTED_FUNCTIONS จึงต้องขึ้นต้นด้วย Underscore (เช่น '_hypotenuse')?
Module.cwrap คืนค่าอะไร?
Flag ของ emcc ใดที่ระบุฟังก์ชันที่ควรเข้าถึงได้จาก JavaScript?
ต้องทำอะไรหลังจาก Allocate String Pointer ด้วย allocateUTF8?