- safe_c.h は、C言語に C++ と Rust の安全性と利便機能 を追加する600行のカスタムヘッダーファイルで、メモリリークのない スレッドセーフな grep(cgrep) の実装に使われている
- RAII、スマートポインタ、自動クリーンアップ(cleanup)属性 により、手動の
free() 呼び出しなしでリソース管理を自動化
- ベクタ、ビュー、Result 型、契約マクロ などにより、バッファオーバーフロー防止、エラー処理、事前条件の検証を安全に実行
- ミューテックスの自動解放、スレッド生成マクロ、分岐予測最適化 などで、並行性と性能を維持しながら安全性を確保
- 結果として、同等の性能(
-O2 水準)で リークやセグフォルトのない C コードを書ける可能性 を実証
safe_c.h 概要
safe_c.h は C++ と Rust の機能を C コードへ移植 するヘッダーファイル
- C23 の
[[cleanup]] 属性をサポートしないコンパイラ(GCC 11、Clang 18 など)でも同じ RAII(自動クリーンアップ) の動作を提供
CLEANUP(func) マクロで、関数終了時にリソースを自動解放
LIKELY() と UNLIKELY() マクロで ホットパスの分岐予測を最適化
メモリ管理: UniquePtr と SharedPtr
- UniquePtr は単一所有のスマートポインタで、スコープ終了時に自動で
free() を呼び出す
AUTO_UNIQUE_PTR() マクロで宣言すると、エラー発生時や早期リターン時にもメモリを自動解放
- SharedPtr は自動参照カウント構造で、最後の参照が解放されたときにリソースを自動破棄
shared_ptr_init() と shared_ptr_copy() により参照の増減を自動処理
- スレッド間で安全に共有する構造体の管理に使用
バッファオーバーフロー防止: Vector と View
DEFINE_VECTOR_TYPE() マクロで、型安全な自動拡張ベクタを生成
- 再割り当て、容量管理、クリーンアップを自動処理
AUTO_TYPED_VECTOR() で宣言すると、スコープ終了時に自動解放
- StringView と Span は 非所有参照構造体 で、別途
malloc せずに文字列や配列のスライスを処理
DEFINE_SPAN_TYPE() で型ごとの Span を定義
- 境界チェックを含み、安全な配列アクセスを保証
エラー処理: Result 型と RAII
- Result 構造体 は Rust の
Result<T, E> に似た 成功/失敗を区別する返り値型
DEFINE_RESULT_TYPE() で型ごとの結果構造を生成
RESULT_IS_OK() と RESULT_UNWRAP_ERROR() で明確なエラー処理を実現
CLEANUP 属性と組み合わせることで、関数終了時にリソースを自動解放
AUTO_MEMORY() マクロで malloc したメモリを自動クリーンアップ
契約と安全な文字列処理
requires() / ensures() マクロで、関数の 事前条件・事後条件を明示
safe_strcpy() は バッファサイズ検査付きのコピー関数 で、オーバーフローを防止
- 失敗時は
false を返し、安全にエラー処理できる
並行性: 自動アンロックとスレッドマクロ
CLEANUP ベースの mutex 自動解放関数 によりデッドロックを防止
- スコープ終了時に
pthread_mutex_unlock() を自動呼び出し
SPAWN_THREAD() と JOIN_THREAD() マクロで スレッド生成・join を簡略化
- cgrep のファイル処理スレッドプール実装に使用
パフォーマンス最適化
LIKELY() / UNLIKELY() マクロで ホットパスの分岐予測 を提供
- PGO レベルの最適化効果を
-O2 ビルドでも確保
- 安全機能が追加されても 性能低下はない
結論
safe_c.h を使った cgrep は 2,300行の C コード で、50回以上の手動 free() 呼び出しを削減
- 同一のアセンブリと実行速度を維持しつつ、メモリリークやセグフォルトのない安全な C コード を実現
- C の単純さと自由度を保ちながら、現代的な安全性 を組み合わせた事例
- 著者は次回の記事で、cgrep が ripgrep より 2倍以上高速で、メモリ使用量が20分の1である理由 を扱う予定
safe_c.h は 新規プロジェクト向き であり、マクロベースのため デバッグ難度が上がる可能性 にも言及
- さまざまな静的解析ツール(GCC analyzer、ASAN、UBSAN、Clang-tidy など)で 正確性と安全性を検証
1件のコメント
Hacker Newsのコメント
この記事は、Cで安全な抽象化(safe abstraction)を実装する際に生じるコストの問題を示している
共有ポインタの実装がPOSIX mutexを使っているため、(1) プラットフォーム非依存ではなく、(2) シングルスレッド環境でもmutexのオーバーヘッドを負担することになる
つまり、‘zero-cost abstraction’ではない
C++の
shared_ptrにも同じ問題があるが、RustはRcとArcの2つの型に分けることでこれを解決しているshared_ptrはmutexではなくatomic演算を使うRustの
Arcに近く、ブログの実装は単に非効率なだけだただしC++には
Rcに相当する型がないため、単純な参照カウントポインタが欲しい場合でも依然としてコストが発生するshared_ptrはスレッドセーフではないランタイムでpthreadシンボルを探し、atomicまたは非atomicの経路を選択する
むしろ常にatomicを使うほうがよいと思う
クロスプラットフォーム性はほとんどの場合「あればよい」程度だ
mutexのオーバーヘッドは煩わしいが、現代のCPUでは許容可能なレベルだ
Rustが素晴らしいのは分かるが、Cのエコシステムはあまりにも巨大で、完全に置き換えるのは難しい
この場合、mutexがもたらす利点が何なのかよく分からない
Fil(aka pizlonator)が作ったFUGCという、Cをメモリセーフにするガベージコレクタのプロジェクトがある
既存コードにもほとんど修正なしで適用でき、C/C++をメモリセーフな言語に変えてくれる
関連HN投稿と公式サイトを参照
この記事はメモリ安全性の核心をやや誤って表現しているように思う
ローカル変数の自動解放や境界チェックだけでは十分ではない
プログラム全体のメモリ寿命管理こそが本当の問題だ
たとえば
UniquePtrを返したりSharedPtrをコピーしたりする際に参照カウントの更新を忘れないか、intrusive listの要素寿命を誰が管理するのか、などだ結局のところ、このアプローチは昔の
#define xfree(p)パターンと大差ないように感じるUniquePtrは構造体を値として返せるので可能だしかし
SharedPtrのコピーでは参照カウントの増加が自動処理されない#define xfree(p)パターンがなぜ悪いのか気になるC23が
[[cleanup]]属性を導入したとはいうものの、実際にはGCC拡張機能であり、[[gnu::cleanup()]]と書く必要があるサンプルコードを参照
「C++:他の言語が私の力の一部でも真似しようとすると、どれだけ苦労するか見よ」というジョークがあった
なぜマクロでC++を真似するのかは気になるが、とにかく興味深い試みだ
ただ、結局C++17の機能まで真似しているのを見ると、もうC++を使ったほうがよいのではないかと思ってしまう
Cは今でも扱いやすいが、C++は複雑すぎてフロントエンドなしでは近づきにくい
C++に移ると、ビルドチェーン、名前マングリング、libstdc++依存などで複雑になる
一方、C++をCスタイルで使う場合にはそうした制約がない
setjmp/longjmpベースの例外処理とは互換性がない
その代わり、POSIXの
pthread_cleanup_pushに着想を得たcleanupマクロのペアとして統合できるcleanup_push(fn, type, ptr, init)とcleanup_pop(ptr)を使って、スタックベースのクリーンアップルーチンを実装するこの方式にはコンパイル時に対応関係の誤りを検出できる利点がある
safeclibの本物の
safe_c.hと混同してはいけないsafeclibヘッダを参照
グローバルなconstraint handlerのために設計上の失敗とみなされており、ほとんどのツールチェーンでサポートされていない
関連文書を参照
Nim言語を使えば、safe_c.hが提供する機能はすべて得られる
NimはCにコンパイルされ、安全性と性能を同時に提供する
ARCベースの自動参照カウント、
defer、Option[T]、bounds-checking、likely/unlikelyなど、さまざまな機能を標準で備えている公式サイト、ARC紹介、view types、Optionドキュメント、likelyテンプレートを参照
このアプローチが移植性を目標にするなら、現実的にはC99にとどめておくのが安全だ
MSVCのCコンパイラは癖が強いが、クロスプラットフォームのためにはほぼ必須だ
私も似たようなヘッダを作ったが、移植性の問題からcleanupユーティリティは入れなかった
CコードがC++としてもコンパイルできるなら、うまく動く
パッケージマネージャも一緒に提供される
記事内で何度も言及されているcgrepのコードへのリンクがない
GitHubには同名のプロジェクトが多いが、その大半は別言語で書かれている