10 คะแนน โดย GN⁺ 2025-11-19 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • safe_c.h คือไฟล์เฮดเดอร์แบบกำหนดเองขนาด 600 บรรทัดที่เพิ่ม ความปลอดภัยและฟีเจอร์อำนวยความสะดวกแบบ C++ และ Rust ให้กับภาษา C และถูกนำไปใช้สร้าง grep แบบ thread-safe (cgrep) ที่ไม่มี memory leak
  • ใช้ RAII, smart pointer, และแอตทริบิวต์ cleanup อัตโนมัติ เพื่อทำให้การจัดการทรัพยากรเป็นอัตโนมัติโดยไม่ต้องเรียก free() เอง
  • มี vector, view, ประเภท Result, และ contract macro เพื่อจัดการ buffer overflow, การจัดการข้อผิดพลาด, และการตรวจสอบ precondition ได้อย่างปลอดภัย
  • มี การปลดล็อก mutex อัตโนมัติ, macro สำหรับ spawn thread, และการปรับแต่ง branch prediction เพื่อรักษาทั้ง concurrency และประสิทธิภาพพร้อมเพิ่มความปลอดภัย
  • ผลลัพธ์คือพิสูจน์ว่า สามารถเขียน โค้ด C ที่ไม่มี leak และ segfault ได้โดยยังคงประสิทธิภาพระดับเดียวกัน (-O2)

ภาพรวมของ safe_c.h

  • safe_c.h คือไฟล์เฮดเดอร์ที่ ย้ายฟีเจอร์ของ C++ และ Rust มาสู่โค้ด C
    • ให้พฤติกรรม RAII (การ cleanup อัตโนมัติ) แบบเดียวกันได้ แม้บนคอมไพเลอร์ที่ยังไม่รองรับแอตทริบิวต์ [[cleanup]] ของ C23 (เช่น GCC 11, Clang 18)
    • ใช้ macro CLEANUP(func) เพื่อปล่อยทรัพยากรโดยอัตโนมัติเมื่อออกจากฟังก์ชัน
    • ใช้ macro LIKELY() และ UNLIKELY() สำหรับ ปรับ branch prediction ของ hot path

การจัดการหน่วยความจำ: UniquePtr และ SharedPtr

  • UniquePtr คือ smart pointer แบบมีเจ้าของเพียงรายเดียว ซึ่งจะเรียก free() อัตโนมัติเมื่อออกจากสโคป
    • เมื่อประกาศด้วย macro AUTO_UNIQUE_PTR() หน่วยความจำจะถูกปล่อยอัตโนมัติแม้เกิดข้อผิดพลาดหรือมีการ return ก่อนกำหนด
  • SharedPtr คือโครงสร้างที่ทำ reference counting อัตโนมัติ และจะทำลายทรัพยากรเมื่อ reference สุดท้ายถูกปล่อย
    • ใช้ shared_ptr_init() และ shared_ptr_copy() เพื่อจัดการการเพิ่ม-ลด reference โดยอัตโนมัติ
    • ใช้สำหรับจัดการ shared struct ระหว่าง thread อย่างปลอดภัย

ป้องกัน buffer overflow: Vector และ View

  • ใช้ macro DEFINE_VECTOR_TYPE() เพื่อสร้าง vector แบบขยายอัตโนมัติที่ปลอดภัยต่อชนิดข้อมูล
    • จัดการการ reallocate, ความจุ, และการ cleanup โดยอัตโนมัติ
    • เมื่อประกาศด้วย AUTO_TYPED_VECTOR() จะถูกปล่อยอัตโนมัติเมื่อออกจากสโคป
  • StringView และ Span คือ โครงสร้างอ้างอิงแบบไม่เป็นเจ้าของข้อมูล สำหรับจัดการ slice ของสตริงและอาร์เรย์โดยไม่ต้อง malloc เพิ่ม
    • ใช้ DEFINE_SPAN_TYPE() เพื่อกำหนด Span สำหรับแต่ละชนิดข้อมูล
    • มีการตรวจสอบขอบเขตเพื่อรับประกันการเข้าถึงอาร์เรย์อย่างปลอดภัย

การจัดการข้อผิดพลาด: ประเภท Result และ RAII

  • โครงสร้าง Result เป็น ประเภทค่าที่แยกผลสำเร็จ/ล้มเหลว คล้าย Result<T, E> ของ Rust
    • ใช้ DEFINE_RESULT_TYPE() เพื่อสร้างโครงสร้างผลลัพธ์สำหรับแต่ละชนิดข้อมูล
    • ใช้ RESULT_IS_OK() และ RESULT_UNWRAP_ERROR() เพื่อจัดการข้อผิดพลาดได้อย่างชัดเจน
  • เมื่อนำมารวมกับแอตทริบิวต์ CLEANUP ก็จะปล่อยทรัพยากรโดยอัตโนมัติเมื่อฟังก์ชันจบการทำงาน
    • ใช้ macro AUTO_MEMORY() เพื่อ cleanup หน่วยความจำที่ได้จาก malloc โดยอัตโนมัติ

Contract และสตริงแบบปลอดภัย

  • ใช้ macro requires() / ensures() เพื่อระบุ เงื่อนไขก่อนและหลังของฟังก์ชัน
    • หากล้มเหลวจะพิมพ์ข้อความผิดพลาดที่ชัดเจน
  • safe_strcpy() คือ ฟังก์ชันคัดลอกที่มีการตรวจสอบขนาดบัฟเฟอร์ เพื่อป้องกัน overflow
    • หากล้มเหลวจะคืนค่า false เพื่อให้จัดการข้อผิดพลาดได้อย่างปลอดภัย

Concurrency: การปลดล็อกอัตโนมัติและ macro สำหรับ thread

  • ใช้ฟังก์ชัน ปลดล็อก mutex อัตโนมัติ ที่อิงกับ CLEANUP เพื่อป้องกัน deadlock
    • เมื่อออกจากสโคปจะเรียก pthread_mutex_unlock() โดยอัตโนมัติ
  • ใช้ macro SPAWN_THREAD() และ JOIN_THREAD() เพื่อ ทำให้การสร้างและ join thread ง่ายขึ้น
    • ถูกนำไปใช้ใน thread pool สำหรับประมวลผลไฟล์ของ cgrep

การปรับแต่งประสิทธิภาพ

  • ใช้ macro LIKELY() / UNLIKELY() เพื่อ ทำ branch prediction สำหรับ hot path
    • ช่วยให้ได้ผลของการปรับแต่งระดับใกล้เคียง PGO แม้ในบิลด์ -O2
  • แม้จะเพิ่มฟีเจอร์ด้านความปลอดภัย ก็ยัง ไม่มีประสิทธิภาพลดลง

บทสรุป

  • cgrep ที่ใช้ safe_c.h เป็น โค้ด C ขนาด 2,300 บรรทัด และตัดการเรียก free() แบบเขียนเองออกไปได้มากกว่า 50 จุด
  • สามารถสร้าง โค้ด C ที่ปลอดจาก memory leak และ segfault ได้ โดยยังคง assembly และความเร็วในการทำงานเท่าเดิม
  • เป็นตัวอย่างของการผสาน ความปลอดภัยสมัยใหม่ เข้ากับความเรียบง่ายและอิสระของ C
  • ผู้เขียนระบุว่าจะอธิบายในบทความถัดไปว่าเหตุใด cgrep จึง เร็วกว่า ripgrep มากกว่า 2 เท่า และใช้หน่วยความจำน้อยกว่า 20 เท่า
  • safe_c.h ถูกมองว่า เหมาะกับโปรเจกต์ใหม่ แต่ก็มีการกล่าวถึงความเป็นไปได้ที่ macro-based design จะทำให้การดีบักยากขึ้น
  • มีการตรวจสอบ ความถูกต้องและความปลอดภัย ด้วย static analyzer หลายชนิด (เช่น GCC analyzer, ASAN, UBSAN, Clang-tidy เป็นต้น)

1 ความคิดเห็น

 
GN⁺ 2025-11-19
ความเห็นจาก Hacker News
  • บทความนี้แสดงให้เห็นปัญหาเรื่องต้นทุนที่เกิดขึ้นเมื่อทำ safe abstraction ในภาษา C
    การ implement shared pointer ใช้ POSIX mutex ทำให้ (1) ไม่เป็นอิสระจากแพลตฟอร์ม และ (2) ต้องจ่าย overhead ของ mutex แม้ในกรณี single-thread
    กล่าวคือ มันไม่ใช่ ‘zero-cost abstraction’
    shared_ptr ของ C++ ก็มีปัญหาแบบเดียวกัน แต่ Rust แก้โดยแยกเป็นสองชนิดคือ Rc และ Arc

    • shared_ptr ของ C++ ไม่ได้ใช้ mutex แต่ใช้ atomic operation
      คล้ายกับ Arc ของ Rust และ implementation ในบล็อกนี้ก็แค่ไม่มีประสิทธิภาพเฉยๆ
      อย่างไรก็ตาม C++ ไม่มีชนิดที่เทียบเท่ากับ Rc ดังนั้นถ้าต้องการ pointer แบบ reference counting ธรรมดา ก็ยังคงมีต้นทุนอยู่
    • ในสภาพแวดล้อม glibc และ libstdc++ ถ้าไม่ลิงก์ pthreads แล้ว shared_ptr จะ ไม่ thread-safe
      มันจะค้นหา pthread symbol ตอนรันไทม์เพื่อเลือกเส้นทาง atomic หรือ non-atomic
      ฉันคิดว่าการใช้ atomic ตลอดไปน่าจะดีกว่า
    • ฉันรู้สึกว่าการทำให้โค้ด ไม่ crash สำคัญกว่ามาก
      ความเป็น cross-platform ส่วนใหญ่เป็นแค่ของดีถ้ามีก็เท่านั้น
      overhead ของ mutex น่ารำคาญก็จริง แต่บน CPU สมัยใหม่ก็ยังพอรับได้
      รู้ว่า Rust ยอดเยี่ยม แต่ ecosystem ของ C ใหญ่มากจนยากจะมาแทนที่ทั้งหมด
    • เราอาจ implement reference count ด้วย C11 atomic operation แทน mutex ก็ได้
      ในกรณีนี้ฉันไม่ค่อยเข้าใจว่าข้อดีของ mutex คืออะไร
    • POSIX mutex มี implementation อยู่แล้วบนหลายแพลตฟอร์ม ดังนั้นอาจมองได้ว่าเป็น API ที่ใช้งานได้ทั่วไปกว่า
  • มีโปรเจกต์ทำให้ C ปลอดภัยด้านหน่วยความจำด้วย garbage collector ชื่อ FUGC ที่สร้างโดย Fil (aka pizlonator)
    สามารถนำไปใช้กับโค้ดเดิมได้แทบไม่ต้องแก้อะไร และเปลี่ยน C/C++ ให้เป็น ภาษา memory-safe
    ดู โพสต์ HN ที่เกี่ยวข้อง และ เว็บไซต์ทางการ

    • ขอบคุณมากที่ทำให้เพิ่งรู้จักโปรเจกต์นี้ ฉันคิดว่าเป็นความพยายามที่เจ๋งมาก
    • แต่ฉันไม่อยากยอมรับ ประสิทธิภาพที่ลดลง จาก garbage collector
  • ดูเหมือนว่าบทความนี้จะอธิบายแก่นของ memory safety คลาดเคลื่อนไปเล็กน้อย
    การคืนหน่วยความจำอัตโนมัติของตัวแปรภายในฟังก์ชันหรือการตรวจขอบเขตเพียงอย่างเดียวไม่เพียงพอ
    ปัญหาจริงคือ การจัดการอายุการใช้งานของหน่วยความจำ ทั้งโปรแกรม
    เช่น ตอนคืนค่า UniquePtr หรือคัดลอก SharedPtr จะลืมเพิ่ม reference count หรือไม่ ใครเป็นคนจัดการอายุของสมาชิกใน intrusive list เป็นต้น
    สุดท้ายแล้วแนวทางในบทความนี้ก็ให้ความรู้สึกว่าไม่ได้ต่างจากแพตเทิร์น #define xfree(p) ในอดีตมากนัก

    • UniquePtr ทำได้เพราะสามารถคืนค่า struct แบบ by value ได้
      แต่การคัดลอก SharedPtr ไม่ได้จัดการการเพิ่ม reference count ให้อัตโนมัติ
    • ฉันสงสัยว่าทำไมแพตเทิร์น #define xfree(p) ถึงถือว่าไม่ดี
  • แม้จะบอกว่า C23 เพิ่มแอตทริบิวต์ [[cleanup]] เข้ามา แต่ในความเป็นจริงมันคือ ส่วนขยายของ GCC และต้องเขียนเป็น [[gnu::cleanup()]]
    ดู โค้ดตัวอย่าง

    • หาข้อมูลเรื่องนี้ยากมาก แต่สุดท้ายดูเหมือนว่าจะเปลี่ยนแค่ไวยากรณ์ ส่วนความสามารถเองก็ยังคงเป็น extension อยู่ดี
  • มีมุกว่า “C++: ดูสิว่าภาษาอื่นต้องลำบากแค่ไหนเพื่อเลียนแบบพลังของฉันเพียงบางส่วน”
    ฉันสงสัยว่าทำไมถึงต้องใช้แมโครมาเลียนแบบ C++ แต่ยังไงก็เป็นความพยายามที่น่าสนใจ

    • กระบวนการสร้าง C ที่ปลอดภัยกว่า โดยไม่ใส่ทุกฟีเจอร์ของ C++ เข้ามานั้นน่าสนใจ
      แต่สุดท้ายพอเห็นว่าไปเลียนแบบถึงฟีเจอร์ระดับ C++17 ก็อดคิดไม่ได้ว่าใช้ C++ ไปเลยน่าจะดีกว่าไหม
    • ฉันต้องการ ภาษาที่ parse ได้ง่าย
      C ยังจัดการได้ง่ายอยู่ แต่ C++ ซับซ้อนเกินไปจนยากจะเข้าถึงได้โดยไม่มี frontend
    • C เรียบง่ายจึงเป็น ภาษาที่เหมาะกับการแฮ็ก
      พอไป C++ ก็ซับซ้อนขึ้นจาก build chain, name mangling, การพึ่งพา libstdc++ เป็นต้น
    • โปรเจกต์นี้สามารถอนุญาตเฉพาะบางฟีเจอร์ของ C++ เพื่อบังคับให้ใช้ ไวยากรณ์แบบจำกัด ได้
      ในทางกลับกัน ถ้าใช้ C++ แบบเขียนสไตล์ C ก็ไม่มีข้อจำกัดแบบนั้น
    • การที่ผู้ผลิต embedded CPU หลายรายไม่ให้ C++ compiler มาก็เป็นข้อจำกัดในโลกจริง
  • มันไม่เข้ากันกับ exception handling ที่อิง setjmp/longjmp
    แต่สามารถผสานด้วย คู่แมโคร cleanup ที่ได้แรงบันดาลใจจาก pthread_cleanup_push ของ POSIX แทน
    ใช้ cleanup_push(fn, type, ptr, init) และ cleanup_pop(ptr) เพื่อทำ cleanup routine แบบ stack-based
    วิธีนี้มี ข้อดีที่จับข้อผิดพลาดเรื่องการจับคู่ไม่สมดุลได้ตั้งแต่คอมไพล์ไทม์

  • ไม่ควรสับสนกับ safec.h ตัวจริงของ safeclib
    ดู เฮดเดอร์ของ safeclib

    • ฉันสงสัยว่าทำไมยังต้องการบำรุงรักษา implementation ของ Annex K อยู่
      มันถูกมองว่าเป็น ความล้มเหลวด้านการออกแบบ เพราะ global constraint handler และ toolchain ส่วนใหญ่ก็ไม่รองรับ
      ดู เอกสารที่เกี่ยวข้อง
  • ถ้าใช้ภาษา Nim ก็จะได้ทุกอย่างที่ safe_c.h มีให้
    Nim คอมไพล์ไปเป็น C และให้ทั้ง ความปลอดภัยและประสิทธิภาพ พร้อมกัน
    มีทั้ง automatic reference counting แบบ ARC, defer, Option[T], bounds-checking, likely/unlikely และฟีเจอร์อีกหลายอย่างมาให้ในตัว
    ดู เว็บไซต์ทางการ, แนะนำ ARC, view types, เอกสาร Option, เทมเพลต likely

  • ถ้าแนวทางนี้ตั้งเป้าเรื่อง portability ในทางปฏิบัติก็ควรหยุดที่ C99 จะปลอดภัยกว่า
    C compiler ของ MSVC จุกจิกก็จริง แต่แทบจะขาดไม่ได้ถ้าต้องการ cross-platform
    ฉันก็เคยทำเฮดเดอร์คล้ายกัน แต่ไม่ได้ใส่ utility เรื่อง cleanup เพราะปัญหาด้าน portability

    • ถ้าให้แมโครสร้างโค้ด C++ (แบบอาศัย destructor) ก็ทำได้โดยไม่ต้องมีแอตทริบิวต์ cleanup
      ถ้าโค้ด C นั้นคอมไพล์เป็น C++ ได้ด้วย มันก็จะทำงานได้ดี
    • บน Windows ก็พัฒนาได้สบายด้วย MSYS2 + GCC
      มี package manager มาให้พร้อม
    • เผื่อไว้เป็นข้อมูล ตอนนี้ MSVC รองรับ C17 แล้ว
  • ในบทความมีการพูดถึงโค้ดของ cgrep หลายครั้ง แต่ไม่มีลิงก์
    บน GitHub มีโปรเจกต์ชื่อเดียวกันหลายตัว แต่ส่วนใหญ่เขียนด้วยภาษาอื่น

    • ฉันก็ไม่รู้เหมือนกันว่าหมายถึง cgrep ตัวไหน และอยากลองใช้ด้วยตัวเอง