이 글은 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 글과 공식 사이트 참고
덕분에 이 프로젝트를 처음 알게 되었음. 정말 멋진 시도라고 생각함
하지만 가비지 컬렉터의 성능 저하를 감수하고 싶지는 않음
이 글은 메모리 안전성의 핵심을 다소 잘못 표현한 것 같음
지역 변수 자동 해제나 경계 검사만으로는 충분하지 않음
프로그램 전체의 메모리 수명 관리가 진짜 문제임
예를 들어 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)을 사용해 스택 기반 정리 루틴을 구현함
이 방식은 컴파일 타임에 균형 오류를 잡아주는 장점이 있음
Annex K 구현을 유지보수하려는 이유가 궁금함
전역 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++ 코드(소멸자 기반)를 생성하게 하면 cleanup 속성 없이도 가능함
C 코드가 C++로도 컴파일된다면 잘 동작함
Windows에서도 MSYS2 + GCC로 충분히 개발 가능함
패키지 매니저도 함께 제공됨
참고로 MSVC는 이제 C17을 지원함
글에서 여러 번 언급된 cgrep의 코드 링크가 없음
GitHub에 같은 이름의 프로젝트가 많지만 대부분 다른 언어로 작성되어 있음
Hacker News 의견
이 글은 C에서 안전한 추상화(safe abstraction) 를 구현할 때 생기는 비용 문제를 보여줌
공유 포인터 구현이 POSIX mutex를 사용해서 (1) 플랫폼 독립적이지 않고 (2) 단일 스레드에서도 mutex 오버헤드를 지불하게 됨
즉, ‘zero-cost abstraction’이 아님
C++의 shared_ptr도 같은 문제를 가지지만, Rust는 Rc와 Arc 두 가지 타입으로 이를 구분해 해결함
Rust의 Arc와 유사하며, 블로그의 구현은 단순히 비효율적일 뿐임
다만 C++에는 Rc에 해당하는 타입이 없어서, 단순 참조 카운팅 포인터를 원할 때는 여전히 비용이 발생함
런타임에서 pthread 심볼을 찾아 atomic 또는 비-atomic 경로를 선택함
차라리 항상 atomic을 쓰는 편이 낫다고 생각함
크로스플랫폼은 대부분의 경우 ‘있으면 좋은’ 수준임
mutex 오버헤드는 짜증나지만, 현대 CPU에서는 감당 가능한 수준임
Rust가 훌륭하다는 건 알지만, C 생태계가 너무 방대해서 완전히 대체하기는 어려움
이 경우 mutex가 주는 이점이 무엇인지 잘 모르겠음
Fil(aka pizlonator)이 만든 FUGC라는 가비지 컬렉터로 C를 메모리 안전하게 만드는 프로젝트가 있음
기존 코드에 거의 수정 없이 적용 가능하며, C/C++을 메모리 안전 언어로 바꿔줌
관련 HN 글과 공식 사이트 참고
이 글은 메모리 안전성의 핵심을 다소 잘못 표현한 것 같음
지역 변수 자동 해제나 경계 검사만으로는 충분하지 않음
프로그램 전체의 메모리 수명 관리가 진짜 문제임
예를 들어 UniquePtr을 반환하거나 SharedPtr을 복사할 때 참조 카운트를 잊지 않는지, intrusive list의 원소 수명은 누가 관리하는지 등
결국 이 글의 접근은 예전의
#define xfree(p)패턴과 크게 다르지 않다고 느낌하지만 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의 진짜 safec.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에 같은 이름의 프로젝트가 많지만 대부분 다른 언어로 작성되어 있음