Memberi C Kekuatan Super: File Header Kustom Pengguna (`safe_c.h`)
(hwisnu.bearblog.dev)- safe_c.h adalah file header kustom sepanjang 600 baris yang menambahkan fitur keamanan dan kemudahan ala C++ dan Rust ke bahasa C, dan digunakan dalam implementasi grep yang aman terhadap thread tanpa kebocoran memori (cgrep)
- Melalui RAII, smart pointer, dan atribut cleanup otomatis, pengelolaan resource diotomatisasi tanpa perlu pemanggilan
free()manual - Dengan vector, view, tipe Result, dan makro kontrak, buffer overflow, penanganan error, dan verifikasi prasyarat dapat dilakukan dengan aman
- Dengan pelepasan mutex otomatis, makro spawn thread, dan optimasi prediksi percabangan, keamanan tetap terjaga sambil mempertahankan konkurensi dan performa
- Hasilnya, ini membuktikan kemungkinan menulis kode C tanpa kebocoran dan segfault dengan performa yang sama (setara tingkat
-O2)
Ikhtisar safe_c.h
- safe_c.h adalah file header yang membawa fitur C++ dan Rust ke dalam kode C
- Bahkan pada compiler yang tidak mendukung atribut C23
[[cleanup]](seperti GCC 11, Clang 18), ia tetap menyediakan perilaku RAII (pembersihan otomatis) yang sama - Makro
CLEANUP(func)secara otomatis melepaskan resource saat fungsi berakhir - Makro
LIKELY()danUNLIKELY()untuk optimasi prediksi percabangan pada hot path
- Bahkan pada compiler yang tidak mendukung atribut C23
Manajemen memori: UniquePtr dan SharedPtr
- UniquePtr adalah smart pointer kepemilikan tunggal yang otomatis memanggil
free()saat scope berakhir- Jika dideklarasikan dengan makro
AUTO_UNIQUE_PTR(), memori tetap dibebaskan otomatis meski terjadi error atau return lebih awal
- Jika dideklarasikan dengan makro
- SharedPtr adalah struktur dengan reference counting otomatis yang menghancurkan resource secara otomatis saat referensi terakhir dilepas
shared_ptr_init()danshared_ptr_copy()menangani kenaikan/penurunan referensi secara otomatis- Digunakan untuk mengelola struct yang dibagikan secara aman antar-thread
Pencegahan buffer overflow: Vector dan View
- Makro DEFINE_VECTOR_TYPE() membuat vector yang dapat berkembang otomatis dan type-safe
- Realokasi, pengelolaan kapasitas, dan pembersihan ditangani secara otomatis
- Jika dideklarasikan dengan
AUTO_TYPED_VECTOR(), resource dibebaskan otomatis saat scope berakhir
- StringView dan Span adalah struct referensi non-owning untuk menangani slice string dan array tanpa
malloctambahanDEFINE_SPAN_TYPE()mendefinisikan Span per tipe- Dilengkapi pemeriksaan batas untuk menjamin akses array yang aman
Penanganan error: tipe Result dan RAII
- Struct Result adalah tipe nilai balik yang membedakan sukses/gagal mirip
Result<T, E>milik RustDEFINE_RESULT_TYPE()membuat struct hasil per tipeRESULT_IS_OK()danRESULT_UNWRAP_ERROR()memungkinkan penanganan error yang jelas
- Dikombinasikan dengan atribut
CLEANUP, resource dibebaskan otomatis saat fungsi selesai- Makro
AUTO_MEMORY()membersihkan memori hasilmallocsecara otomatis
- Makro
Kontrak dan string aman
- Makro requires() / ensures() menyatakan kondisi sebelum dan sesudah fungsi
- Jika gagal, pesan error yang jelas akan ditampilkan
- safe_strcpy() adalah fungsi copy dengan pemeriksaan ukuran buffer, untuk mencegah overflow
- Jika gagal, fungsi mengembalikan false sehingga penanganan error tetap aman
Konkurensi: unlock otomatis dan makro thread
- Fungsi pelepasan mutex otomatis berbasis
CLEANUPmencegah deadlockpthread_mutex_unlock()dipanggil otomatis saat scope berakhir
- Makro
SPAWN_THREAD()danJOIN_THREAD()menyederhanakan pembuatan dan join thread- Digunakan dalam implementasi thread pool pemrosesan file pada cgrep
Optimasi performa
- Makro
LIKELY()/UNLIKELY()menyediakan prediksi percabangan untuk hot path- Efek optimasi setingkat PGO dapat diperoleh bahkan pada build
-O2
- Efek optimasi setingkat PGO dapat diperoleh bahkan pada build
- Meski fitur keamanan ditambahkan, tidak ada penurunan performa
Kesimpulan
- cgrep yang menggunakan safe_c.h terdiri dari 2.300 baris kode C, dan menghilangkan lebih dari 50 pemanggilan
free()manual - Tetap mempertahankan assembly dan kecepatan eksekusi yang sama, sambil mewujudkan kode C yang aman tanpa kebocoran memori dan segfault
- Ini adalah contoh yang menggabungkan keamanan modern sambil mempertahankan kesederhanaan dan kebebasan C
- Penulis menyebutkan bahwa tulisan berikutnya akan membahas alasan cgrep bisa lebih dari 2 kali lebih cepat dari ripgrep dan menggunakan memori 20 kali lebih sedikit
- safe_c.h disebut cocok untuk proyek baru, meski juga disinggung bahwa pendekatan berbasis makro dapat meningkatkan kesulitan debugging
- Verifikasi akurasi dan keamanan dilakukan dengan berbagai static analyzer (GCC analyzer, ASAN, UBSAN, Clang-tidy, dll.)
1 komentar
Komentar Hacker News
Tulisan ini menunjukkan masalah biaya yang muncul saat mengimplementasikan abstraksi aman (safe abstraction) di C
Implementasi shared pointer menggunakan POSIX mutex sehingga (1) tidak independen terhadap platform dan (2) tetap harus membayar overhead mutex bahkan dalam single-thread
Artinya, ini bukan ‘zero-cost abstraction’
shared_ptrdi C++ juga memiliki masalah yang sama, tetapi Rust menyelesaikannya dengan membedakan dua tipe, Rc dan Arcshared_ptrdi C++ tidak memakai mutex melainkan operasi atomicIni mirip dengan Arc di Rust, dan implementasi di blog itu hanya sekadar tidak efisien
Namun, di C++ tidak ada tipe yang setara dengan Rc, jadi jika menginginkan pointer reference counting sederhana, biayanya tetap ada
shared_ptrtidak thread-safeSaat runtime, ia mencari simbol pthread lalu memilih jalur atomic atau non-atomic
Menurut saya lebih baik selalu memakai atomic
Cross-platform dalam kebanyakan kasus hanya sebatas ‘bagus kalau ada’
Overhead mutex memang menjengkelkan, tetapi pada CPU modern masih berada di tingkat yang bisa ditanggung
Saya tahu Rust itu hebat, tetapi ekosistem C terlalu besar sehingga sulit digantikan sepenuhnya
Dalam kasus ini, saya kurang paham apa keuntungan yang diberikan mutex
Ada proyek untuk membuat C aman terhadap memori dengan garbage collector bernama FUGC buatan Fil (alias pizlonator)
Ini bisa diterapkan ke kode yang sudah ada hampir tanpa perubahan, dan mengubah C/C++ menjadi bahasa yang aman terhadap memori
Lihat postingan HN terkait dan situs resminya
Tulisan ini tampaknya agak salah menggambarkan inti dari keamanan memori
Pelepasan otomatis variabel lokal atau bounds checking saja tidak cukup
Masalah sebenarnya adalah pengelolaan masa hidup memori di seluruh program
Misalnya, apakah saat mengembalikan
UniquePtratau menyalinSharedPtrkita tidak lupa menaikkan reference count, atau siapa yang mengelola masa hidup elemen dalam intrusive listPada akhirnya, pendekatan dalam tulisan ini terasa tidak jauh berbeda dari pola
#define xfree(p)lamaUniquePtrmemungkinkan karena struct bisa dikembalikan sebagai nilaiNamun, penyalinan
SharedPtrtidak secara otomatis menangani kenaikan reference count#define xfree(p)dianggap burukKatanya C23 memperkenalkan atribut [[cleanup]], tetapi pada praktiknya ini adalah ekstensi GCC dan harus ditulis sebagai
[[gnu::cleanup()]]Lihat contoh kode
Ada lelucon, “C++: lihat betapa kerasnya bahasa lain berusaha meniru sebagian dari kekuatanku”
Saya penasaran kenapa orang mencoba meniru C++ dengan makro, tetapi bagaimanapun ini percobaan yang menarik
Namun ketika akhirnya sampai meniru fitur C++17, saya jadi berpikir bukankah lebih baik langsung memakai C++ saja
C masih mudah ditangani, tetapi C++ terlalu rumit sehingga sulit didekati tanpa frontend
Begitu pindah ke C++, semuanya jadi lebih rumit karena build chain, name mangling, dependensi libstdc++, dan sebagainya
Sebaliknya, jika memakai C++ dengan gaya C, pembatasan seperti itu tidak ada
Ini tidak kompatibel dengan exception handling berbasis setjmp/longjmp
Sebagai gantinya, ini bisa diintegrasikan dengan pasangan makro cleanup yang terinspirasi dari
pthread_cleanup_pushmilik POSIXDengan
cleanup_push(fn, type, ptr, init)dancleanup_pop(ptr), rutin pembersihan berbasis stack dapat diimplementasikanPendekatan ini punya kelebihan karena kesalahan keseimbangan bisa ditangkap saat compile time
Jangan sampai tertukar dengan safec.h asli milik safeclib
Lihat header safeclib
Karena global constraint handler, ini dinilai sebagai kegagalan desain, dan sebagian besar toolchain tidak mendukungnya
Lihat dokumen terkait
Jika memakai bahasa Nim, kita bisa mendapatkan semua yang disediakan safe_c.h
Nim dikompilasi ke C, dan memberikan keamanan serta performa sekaligus
Berbagai fitur seperti automatic reference counting berbasis ARC,
defer,Option[T], bounds-checking,likely/unlikely, dan lainnya disediakan secara bawaanLihat situs resmi, pengenalan ARC, view types, dokumentasi Option, dan template likely
Jika pendekatan ini menargetkan portabilitas, secara realistis paling aman tetap bertahan di C99
Compiler C milik MSVC memang merepotkan, tetapi untuk cross-platform hampir wajib
Saya juga pernah membuat header serupa, tetapi tidak memasukkan utilitas cleanup karena masalah portabilitas
Jika kode C juga bisa dikompilasi sebagai C++, ini akan bekerja dengan baik
Package manager juga disediakan
Tidak ada tautan ke kode cgrep yang beberapa kali disebut dalam tulisan
Ada banyak proyek dengan nama yang sama di GitHub, tetapi kebanyakan ditulis dalam bahasa lain