10 ポイント 投稿者 GN⁺ 2025-11-19 | 1件のコメント | WhatsAppで共有
  • safe_c.h は、C言語に C++ と Rust の安全性と利便機能 を追加する600行のカスタムヘッダーファイルで、メモリリークのない スレッドセーフな grep(cgrep) の実装に使われている
  • RAII、スマートポインタ、自動クリーンアップ(cleanup)属性 により、手動の free() 呼び出しなしでリソース管理を自動化
  • ベクタ、ビュー、Result 型、契約マクロ などにより、バッファオーバーフロー防止、エラー処理、事前条件の検証を安全に実行
  • ミューテックスの自動解放、スレッド生成マクロ、分岐予測最適化 などで、並行性と性能を維持しながら安全性を確保
  • 結果として、同等の性能(-O2 水準)で リークやセグフォルトのない C コードを書ける可能性 を実証

safe_c.h 概要

  • safe_c.hC++ と 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() で宣言すると、スコープ終了時に自動解放
  • StringViewSpan非所有参照構造体 で、別途 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件のコメント

 
GN⁺ 2025-11-19
Hacker Newsのコメント
  • この記事は、Cで安全な抽象化(safe abstraction)を実装する際に生じるコストの問題を示している
    共有ポインタの実装がPOSIX mutexを使っているため、(1) プラットフォーム非依存ではなく、(2) シングルスレッド環境でも
    mutexのオーバーヘッド
    を負担することになる
    つまり、‘zero-cost abstraction’ではない
    C++のshared_ptrにも同じ問題があるが、RustはRcArcの2つの型に分けることでこれを解決している

    • C++のshared_ptrはmutexではなくatomic演算を使う
      RustのArcに近く、ブログの実装は単に非効率なだけだ
      ただしC++にはRcに相当する型がないため、単純な参照カウントポインタが欲しい場合でも依然としてコストが発生する
    • glibcとlibstdc++の環境では、pthreadsをリンクしないとshared_ptrスレッドセーフではない
      ランタイムでpthreadシンボルを探し、atomicまたは非atomicの経路を選択する
      むしろ常にatomicを使うほうがよいと思う
    • 私はコードをクラッシュさせないことのほうがはるかに重要だと感じる
      クロスプラットフォーム性はほとんどの場合「あればよい」程度だ
      mutexのオーバーヘッドは煩わしいが、現代のCPUでは許容可能なレベルだ
      Rustが素晴らしいのは分かるが、Cのエコシステムはあまりにも巨大で、完全に置き換えるのは難しい
    • mutexの代わりにC11 atomic演算で参照カウントを実装することもできる
      この場合、mutexがもたらす利点が何なのかよく分からない
    • POSIX mutexはすでに多くのプラットフォームで実装されており、むしろより汎用的なAPIだと思う
  • 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++のすべての機能を入れなくてもより安全なCを作っていく過程は興味深かった
      ただ、結局C++17の機能まで真似しているのを見ると、もうC++を使ったほうがよいのではないかと思ってしまう
    • 私はパース可能な言語が欲しい
      Cは今でも扱いやすいが、C++は複雑すぎてフロントエンドなしでは近づきにくい
    • Cは単純なのでハックしやすい言語
      C++に移ると、ビルドチェーン、名前マングリング、libstdc++依存などで複雑になる
    • このプロジェクトはC++の一部機能だけを許可して制限された文法を強制できる
      一方、C++をCスタイルで使う場合にはそうした制約がない
    • 組み込みCPUベンダーがC++コンパイラを提供していないことも現実的な制約だ
  • setjmp/longjmpベースの例外処理とは互換性がない
    その代わり、POSIXのpthread_cleanup_pushに着想を得たcleanupマクロのペアとして統合できる
    cleanup_push(fn, type, ptr, init)cleanup_pop(ptr)を使って、スタックベースのクリーンアップルーチンを実装する
    この方式にはコンパイル時に対応関係の誤りを検出できる利点がある

  • safeclibの本物のsafe_c.hと混同してはいけない
    safeclibヘッダを参照

    • Annex K実装を保守しようとする理由が気になる
      グローバルなconstraint handlerのために設計上の失敗とみなされており、ほとんどのツールチェーンでサポートされていない
      関連文書を参照
  • Nim言語を使えば、safe_c.hが提供する機能はすべて得られる
    NimはCにコンパイルされ、安全性と性能を同時に提供する
    ARCベースの自動参照カウント、deferOption[T]、bounds-checking、likely/unlikelyなど、さまざまな機能を標準で備えている
    公式サイトARC紹介view typesOptionドキュメントlikelyテンプレートを参照

  • このアプローチが移植性を目標にするなら、現実的にはC99にとどめておくのが安全だ
    MSVCのCコンパイラは癖が強いが、クロスプラットフォームのためにはほぼ必須だ
    私も似たようなヘッダを作ったが、移植性の問題からcleanupユーティリティは入れなかった

    • マクロでC++コード(デストラクタベース)を生成するようにすれば、cleanup属性がなくても実現できる
      CコードがC++としてもコンパイルできるなら、うまく動く
    • WindowsでもMSYS2 + GCCで十分に開発できる
      パッケージマネージャも一緒に提供される
    • 参考までに、MSVCは現在C17をサポートしている
  • 記事内で何度も言及されているcgrepのコードへのリンクがない
    GitHubには同名のプロジェクトが多いが、その大半は別言語で書かれている

    • 私もどのcgrepのことを言っているのか分からないし、実際に試してみたい