이식성의 착각: C 확장과 컴파일러 듀오폴리
C를 많이 작성해 본 사람이라면 ISO C 표준이 실용적인 현실이라기보다 이론적인 이상에 가깝다고 느끼는 경우가 많습니다. 실제 세계에서는 C 코드가 완전히 표준을 따르는 경우는 드물고, 대신 비표준 동작, 컴파일러별 확장, 그리고 서로 다른 툴체인 사이의 차이를 메우기 위해 고안된 얇은 전처리기 가드들의 조각으로 이루어져 있습니다.
이러한 긴장은 새로운 C 컴파일러를 만들 때 가장 두드러집니다. 유용하려면 컴파일러는 시스템 헤더—특히 Linux의 glibc—를 파싱할 수 있어야 하는데, 이 헤더들은 종종 소수의 컴파일러만 만족시킬 수 있는 가정들로 가득합니다. 코드베이스가 특정 GNU 확장을 사용해 ABI를 유지한다면, 그 확장을 무시하는 "표준" 컴파일러는 단순히 비준수인 것이 아니라 완전히 깨진 것입니다.
시스템 헤더의 난관
어떤 C 컴파일러를 꿈꾸는 이에게도 첫 번째 큰 장애물은 시스템의 C 라이브러리 헤더입니다. GNU/Linux에서 glibc는 비‑GCC 컴파일러와의 호환성을 유지하려 하지만, 이러한 시도는 종종 불완전하거나 결함이 있습니다.
__attribute__ 함정
흔히 발생하는 실패 지점은 __attribute__((packed))의 사용입니다. sys/epoll.h에서 struct epoll_event는 패킹된 구조체입니다. 패킹은 64비트 시스템에서 구조체의 메모리 레이아웃을 바꾸므로, 이 속성을 무시하면 ABI가 깨집니다. 그러나 glibc는 sys/cdefs.h에서 전처리기 검사를 통해 GCC, Clang, TCC가 아닌 컴파일러에 대해 __attribute__ 매크로를 사실상 비활성화합니다:
#if !(defined __GNUC__ || defined __clang__ || defined __TINYC__)
# define __attribute__(xyz) /* Ignore */
#endif
새 컴파일러를 개발한다면, __attribute__((packed))를 구현하더라도 헤더는 "승인된" 컴파일러 중 하나인 척하지 않는 한 이 매크로를 단순히 없애 버립니다.
limits.h 복잡성
다른 예는 <limits.h>입니다. 이 헤더는 종종 컴파일러 내부 정의와 플랫폼 전용 상수의 조합을 필요로 합니다. glibc 구현은 #include_next 확장을 이용해 컴파일러 자체의 limits.h를 먼저 포함한 뒤 POSIX 전용 상수를 추가합니다. 이는 컴파일러가 해당 확장을 지원할 뿐만 아니라, 특정 디렉터리 구조에서 기대되는 내부 헤더들을 제공해야 함을 의미하는 의존성 체인을 만들게 됩니다.
glibc를 넘어: SDL, OpenBSD, 그리고 Bionic
이식성을 위한 투쟁은 주요 C 라이브러리를 넘어 널리 사용되는 라이브러리와 다른 운영체제로 확장됩니다.
- SDL:
SDL_endian.h헤더는 바이트 스와핑을 위한 단계적 탐지 시스템을 사용합니다. 컴파일러가 GCC나 Clang이 아니면, ISA 매크로(예:__x86_64__)에 기반한 확장 인라인 어셈블리로 대체될 수 있는데, 이는 컴파일러가 필요한 내장 함수를 제공하더라도 마찬가지입니다. 이는 x86_64를 타깃으로 하는 모든 컴파일러가 GCC 스타일 인라인 어셈블리를 지원해야 한다는 가정을 담고 있습니다. - OpenBSD: 일부 OpenBSD 헤더는
__only_inline이라는 매크로를 사용해 최적화된 함수 정의를 제공합니다. 이는 GCC의 특정 인라인 의미론에 의존합니다. 비‑GNU 컴파일러에서는 이 매크로가 종종static으로 정의되어 링크 충돌 오류를 일으킬 수 있습니다._ANSI_LIBRARY매크로를 사용해 이를 비활성화할 수는 있지만, 최적화 이점을 포기해야 합니다. - Bionic (Android): Linux와는 반대로, Android의
bioniclibc 헤더는 Clang 사용을 강하게 전제합니다. 여기서는_Nonnull·_Null_unspecified와 같은 Clang 전용 널 가능성 확장을 활용합니다.
컴파일러 개발자의 딜레마
독립적인 컴파일러를 만드는 사람들에게 호환성으로 가는 길은 난관이 많습니다. 커뮤니티 토론에서 slimcc 개발자가 언급했듯이, 문제는 끝이 없습니다: math.h의 NAN 출력 차이부터 GCC나 PCC로 식별되지 않으면 #error를 발생시키는 NetBSD 헤더까지.
이러한 비호환성에 직면했을 때, 컴파일러 제작자는 일반적으로 네 가지 선택지를 가집니다:
- 업스트림 패치: OS나 라이브러리 소스에서 헤더를 고치려 시도합니다. 프로젝트 규모가 방대해 대부분은 실패합니다.
- 인기 확보: 라이브러리 작성자가 새로운 컴파일러에 대한
#ifdef검사를 명시적으로 추가하도록 만들 정도로 인기를 얻습니다. - 다운스트림 패치: 사용자가 컴파일하려는 라이브러리에 적용할 패치를 배포합니다.
- 위장:
__GNUC__를 정의하고 해당 확장을 구현합니다.
위장은 가장 현실적인 넓은 호환성 경로입니다. 예를 들어 Clang은 __GNUC__=4를 정의해 GCC 4.2.1과의 호환성을 주장합니다. 이렇게 하면 방대한 레거시 코드베이스와도 문제없이 동작하지만, 선언한 버전 번호에 기반해 사용자가 기대할 수 있는 모든 확장을 구현해야 하는 "쫓아잡기" 게임이 시작됩니다.
더 나은 표준을 향해
현재 C 이식성 상황은 GCC/Clang이라는 사실상 듀오폴리의 결과입니다. 일부는 임베디드 시스템이나 비‑PC 플랫폼에서는 C99·C11 표준만으로 충분하다고 주장하지만, *NIX 개발의 "현실"은 GNU 확장과 깊이 얽혀 있습니다.
이상적으로는 업계가 __has_builtin, __has_attribute, __has_feature와 같은 기능 테스트 매크로로 이동해 컴파일러가 자신의 정체성에 관계없이 능력을 알릴 수 있게 해야 합니다. 그때까지 이식성 부담은 전처리기 해킹과 전략적 위장의 복잡한 춤으로 남아 있습니다.
SUMMARY: ISO C 표준 준수가 실제로 거의 달성되지 않는 이유와 glibc 및 기타 시스템 헤더를 다룰 때 독립 컴파일러 개발자들이 직면하는 고된 도전 과제에 대한 탐구.
TITLE: 이식성의 착각: C 확장과 컴파일러 듀오폴리