# C에 초능력을 부여하기: 사용자 정의 헤더 파일(safe_c.h)

> Clean Markdown view of GeekNews topic #24453. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=24453](https://news.hada.io/topic?id=24453)
- GeekNews Markdown: [https://news.hada.io/topic/24453.md](https://news.hada.io/topic/24453.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2025-11-19T05:33:22+09:00
- Updated: 2025-11-19T05:33:22+09:00
- Original source: [hwisnu.bearblog.dev](https://hwisnu.bearblog.dev/giving-c-a-superpower-custom-header-file-safe_ch/)
- Points: 10
- Comments: 1

## Summary

**safe_c.h**는 오래된 C 코드베이스에 **Rust와 C++의 안전성 개념(RAII, 스마트 포인터, Result 타입)**을 이식한 실험적 헤더 파일입니다. 단 600줄로 **자동 메모리 해제, 스레드 안전, 버퍼 경계 검사**를 구현해, 수십 번의 `free()` 호출 없이도 누수·세그폴트 없는 코드를 유지합니다. 특히 **mutex 자동 해제와 분기 예측 매크로** 덕분에 동시성과 성능을 모두 잡은 점이 인상적입니다. “C는 위험하다”는 통념을 뒤집으며, 레거시 시스템을 다루는 개발자에게 새로운 가능성을 보여주는 흥미로운 시도입니다.

## Topic Body

- **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()** 매크로로 타입 안전한 자동 확장 벡터 생성  
  - 재할당, 용량 관리, 정리(cleanup)를 자동 처리  
  - `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()` 매크로로 **스레드 생성·조인 단순화**  
  - cgrep의 파일 처리 스레드 풀 구현에 사용  

### 성능 최적화
- `LIKELY()` / `UNLIKELY()` 매크로로 **핫패스 분기 예측** 제공  
  - PGO 수준의 최적화 효과를 -O2 빌드에서도 확보  
- 안전 기능이 추가되어도 **성능 저하 없음**  

### 결론
- safe_c.h를 사용한 cgrep은 **2,300줄의 C 코드**로, 50회 이상의 수동 free() 호출을 제거  
- 동일한 어셈블리와 실행 속도를 유지하면서 **메모리 누수와 세그폴트 없는 안전한 C 코드** 구현  
- C의 단순함과 자유로움을 유지하면서 **현대적 안전성**을 결합한 사례  
- 작성자는 이후 글에서 cgrep이 ripgrep보다 **2배 이상 빠르고 메모리 사용량은 20배 적은 이유**를 다룰 예정  
- safe_c.h는 **새 프로젝트에 적합**, 매크로 기반이라 **디버깅 난이도 상승** 가능성 언급  
- 다양한 정적 분석기(GCC analyzer, ASAN, UBSAN, Clang-tidy 등)로 **정확성과 안전성 검증** 수행

## Comments



### Comment 46507

- Author: neo
- Created: 2025-11-19T05:33:23+09:00
- Points: 1

###### [Hacker News 의견](https://news.ycombinator.com/item?id=45952428) 
- 이 글은 C에서 **안전한 추상화(safe abstraction)** 를 구현할 때 생기는 비용 문제를 보여줌  
  공유 포인터 구현이 POSIX mutex를 사용해서 (1) 플랫폼 독립적이지 않고 (2) 단일 스레드에서도 **mutex 오버헤드**를 지불하게 됨  
  즉, ‘zero-cost abstraction’이 아님  
  C++의 shared_ptr도 같은 문제를 가지지만, Rust는 Rc와 Arc 두 가지 타입으로 이를 구분해 해결함
  - 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 글](https://news.ycombinator.com/item?id=45133938)과 [공식 사이트](https://fil-c.org/) 참고
  - 덕분에 이 프로젝트를 처음 알게 되었음. 정말 멋진 시도라고 생각함
  - 하지만 가비지 컬렉터의 **성능 저하**를 감수하고 싶지는 않음

- 이 글은 메모리 안전성의 핵심을 다소 **잘못 표현**한 것 같음  
  지역 변수 자동 해제나 경계 검사만으로는 충분하지 않음  
  프로그램 전체의 **메모리 수명 관리**가 진짜 문제임  
  예를 들어 UniquePtr을 반환하거나 SharedPtr을 복사할 때 참조 카운트를 잊지 않는지, intrusive list의 원소 수명은 누가 관리하는지 등  
  결국 이 글의 접근은 예전의 `#define xfree(p)` 패턴과 크게 다르지 않다고 느낌
  - UniquePtr은 구조체를 값으로 반환할 수 있으니 가능함  
    하지만 SharedPtr 복사는 참조 카운트 증가를 자동으로 처리하지 않음
  - `#define xfree(p)` 패턴이 왜 나쁜지 궁금함

- C23이 [[cleanup]] 속성을 도입했다고 하지만, 실제로는 **GCC 확장 기능**이며 `[[gnu::cleanup()]]`로 써야 함  
  [예시 코드](https://godbolt.org/z/Gsz9hs7TE) 참고
  - 관련 정보를 찾기 어렵던데, 결국 문법만 바뀐 것이고 기능 자체는 여전히 확장 기능인 듯함

- “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의 진짜 safec.h와 혼동하지 말아야 함  
  [safeclib 헤더](https://github.com/rurban/safeclib/tree/master/include) 참고
  - Annex K 구현을 유지보수하려는 이유가 궁금함  
    전역 constraint handler 때문에 **설계 실패**로 평가받고 있으며, 대부분의 툴체인이 지원하지 않음  
    [관련 문서](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1967.htm) 참고

- Nim 언어를 쓰면 safe_c.h가 제공하는 기능을 모두 얻을 수 있음  
  Nim은 C로 컴파일되며, **안전성과 성능**을 동시에 제공함  
  ARC 기반의 자동 참조 카운팅, `defer`, `Option[T]`, bounds-checking, `likely/unlikely` 등 다양한 기능을 기본 제공함  
  [공식 사이트](https://nim-lang.org), [ARC 소개](https://nim-lang.org/blog/2020/10/15/introduction-to-arc-orc-in-nim.html), [view types](https://nim-lang.org/docs/manual_experimental.html#view-types), [Option 문서](https://nim-lang.org/docs/options.htm), [likely 템플릿](https://nim-lang.org/docs/system.html#likely.t%2Cbool) 참고

- 이 접근법이 **이식성**을 목표로 한다면, 현실적으로는 C99에 머무르는 게 안전함  
  MSVC의 C 컴파일러는 까다롭지만, 크로스플랫폼을 위해선 거의 필수임  
  나도 비슷한 헤더를 만들었지만, 이식성 문제 때문에 cleanup 유틸리티는 넣지 않았음  
  - 매크로로 C++ 코드(소멸자 기반)를 생성하게 하면 cleanup 속성 없이도 가능함  
    C 코드가 C++로도 컴파일된다면 잘 동작함
  - Windows에서도 **MSYS2 + GCC**로 충분히 개발 가능함  
    패키지 매니저도 함께 제공됨
  - 참고로 **MSVC는 이제 C17을 지원함**

- 글에서 여러 번 언급된 **cgrep**의 코드 링크가 없음  
  GitHub에 같은 이름의 프로젝트가 많지만 대부분 다른 언어로 작성되어 있음  
  - 나도 어떤 cgrep을 말하는 건지 모르겠고, 직접 써보고 싶음
