C 확장, 이식성, 대체 컴파일러에 관하여
(lemon.rip)- ISO C 표준만 따르는 코드는 드물고, 실제 C 코드베이스는 기능 추가와 컴파일러·라이브러리별 공백 우회를 위해 비표준 확장에 의존함
- 유용한 C 컴파일러는
<stdio.h>같은 시스템 헤더부터 처리해야 하지만, glibc는__attribute__((packed)),#include_next같은 GNU 확장과 가정으로 장벽을 만듦 - SDL의 byteswapping 로직은 ISA 매크로가 있으면 inline assembly를 택할 수 있어, GCC·clang이 아닌 컴파일러도 GCC식 확장을 요구받을 수 있음
- OpenBSD와 Gnulib의
extern inline처리는 C99와 GCC 의미 차이, 플랫폼별 분기,_FORTIFY_SOURCE조건 때문에 inline 의미론 호환을 복잡하게 만듦 - 작은 C 컴파일러는 upstream 패치, downstream 패치, 전용 가드 확보, GCC 호환성 흉내 중 선택해야 하며 기능 테스트 매크로 확대가 더 나은 방향으로 보임
glibc 헤더가 만드는 첫 장벽
- 유용한 C 컴파일러가 되려면 시스템 C 라이브러리 헤더를 전처리하고 파싱할 수 있어야 하며,
<stdio.h>를 처리하지 못하면 hello world도 통과하기 어려움 - GNU/Linux 환경에서는 이 장벽이 glibc로 이어짐
- glibc는 거의 모든 libc 헤더가 간접적으로 포함하는 sys/cdefs.h에서 컴파일러가 미리 정의한 매크로를 검사해 지원되는 확장을 판별함
- 지원되지 않는 확장은 관련 정의를 없애는 식으로 처리하지만, 이 호환성 로직도 실제로 깨질 수 있음
-
struct epoll_event와__attribute__((packed))- Linux의
sys/epoll.h에 있는struct epoll_event는 GNU__attribute__((packed))를 쓰는 packed struct임 - 이 속성은 64비트에서 구조체 레이아웃을 바꾸므로, 무시하면 ABI가 깨짐
- 컴파일러가
__attribute__((packed))를 구현해도 충분하지 않음 sys/cdefs.h에는 GCC, clang, tcc가 아니면__attribute__(xyz)를 빈 매크로로 정의하는 코드가 있음- 그 결과 다른 컴파일러는 packed 속성을 지원하더라도 glibc 헤더에서 해당 속성이 제거될 수 있음
epoll헤더는 Linux 전용이므로 C 표준 이식성 기준을 그대로 적용하기 어렵다는 반론도 가능함
- Linux의
-
limits.h와#include_nextstddef.h,stdint.h,limits.h,float.h같은 일부 C 헤더는 freestanding 구현에도 필요해 컴파일러가 제공해야 함- POSIX는 표준 C 상수 외에도 POSIX 전용 상수를
limits.h에 정의하도록 요구하므로, 컴파일러의limits.h위에 플랫폼별limits.h가 필요함 - glibc의
<limits.h>는 GNU C가 아니면 ANSIlimits.h값을 직접 정의하고, GCC 환경에서는#include_next <limits.h>로 컴파일러 헤더를 가져옴 - 이 구조는 GCC 전용 builtin
limits.h가 특정 매크로를 정의한다고 가정하며,#include_next확장에도 의존함 - clang도 이 구조를 우회 처리해야 함
SDL의 기능 감지와 inline assembly 문제
SDL_endian.h의 byteswapping 함수는 가능한 경우 컴파일러 builtin이나 inline assembly를 쓰고, 마지막 수단으로 일반 비트 연산 구현으로 대체함- 감지 로직은 대략 다음 순서로 동작함
- GCC 또는 clang이고
__has_builtin(__builtin_bswapX)가 있으면 builtin 사용 - MSVC 8.0 이상이면 MSVC intrinsic
#pragma사용 __x86_64__같은 ISA별 매크로가 정의되어 있으면 inline assembly 사용- 그 외에는 일반 비트 연산 구현 사용
- GCC 또는 clang이고
- GCC나 clang이 아닌 컴파일러가 합리적인 이유로 ISA별 predefined macro를 정의하면 이 순서가 문제가 됨
- 해당 컴파일러가 bswap builtin과
__has_builtin특수 연산자를 제공해도, 로직상 GCC식 inline assembly를 쓰려고 할 수 있음 - 결과적으로 알 수 없는 컴파일러도 GCC 스타일 inline assembly를 지원한다고 기대하는 구조가 됨
OpenBSD libc와 extern inline의 혼란
- OpenBSD의 일부 헤더는 최적화 시 컴파일러가 선택적으로 사용할 inline 함수 정의를 포함함
- 이 함수들은
__only_inline매크로로 정의되며, 컴파일러가 실제로 inline하지 않으면 외부 심볼로 대체해야 함 - 즉, extern linkage를 가진 inline 함수가 필요함
-
C99 inline과 GCC inline 의미 차이
inline은 C99에 명시되어 있지만, 표준 동작은 C99 이전의 비표준 GCC 동작과 충돌함- 헤더 안 inline 정의는 함수 본문과 함께
extern inline을 써야 하며, 이 경우 실제 exported function을 emit하지 않음 - translation unit에서는 함수 정의를 export하기 위해
inline만 붙여 선언해야 함 inline의 의미는 C++과 C에서도 다름- 이 차이는 Youtao Guo의 글에서 자세히 다뤄짐
-
OpenBSD의
__only_inline- OpenBSD는 GCC inline semantics에 의존함
- GCC 버전 차이를 덮기 위해 sys/cdefs.h의
__only_inline매크로는 최신 GCC에서 명시적__attribute__로 예전 gnu89 inline semantics를 지정함 - 비-GNU 컴파일러에서는
__only_inline이staticlinkage로 정의됨 - 그 결과 함수가 서로 충돌하는 linkage로 선언·정의되어 깨질 수 있음
-
_ANSI_LIBRARY우회- OpenBSD는
_ANSI_LIBRARY매크로를 존중함 - 이 매크로를 정의하면
signal.h같은 표준 헤더에서 깨지는__only_inline정의 사용을 완전히 생략함 - 최적화된 버전은 얻지 못하지만, 최소한 빌드는 동작함
- OpenBSD는
-
Gnulib의
extern inline호환성 코드- Gnulib의
extern inline호환성 코드는 Guile과 nano를 빌드할 때도 등장함 - extern-inline.m4는 이 C corner case의 깨지고 이상한 구현들을 처리하기 위해 복잡한 조건 분기를 포함함
- 조건에는 Apple, DragonFly, FreeBSD, GCC, clang, PCC, HP cc, PGI, SunPro C,
_FORTIFY_SOURCE,__GNUC_STDC_INLINE__,__GNUC_GNU_INLINE__같은 환경 차이가 반영됨
- Gnulib의
Android bionic의 clang 가정
- bionic은 Android의 libc이며, 헤더가 GCC보다 clang을 강하게 가정함
- bionic 헤더는 nullability checks를 위해
_Nonnull,_Null_unspecified같은 clang 전용 확장을 많이 사용함 - 이런 매크로는 명령줄 플래그로
#define해 없애기 어렵지 않음 - Termux를 통해 Android 휴대폰을 네이티브 aarch64 개발 환경으로 사용할 때 bionic 헤더에서 이 문제가 드러남
_Null_unspecified는__BIONIC_COMPLICATED_NULLNESS로도 불리며, 관련 정의는 bionic의 sys/cdefs.h에 있음
작은 C 컴파일러가 마주하는 선택지
- ISO C 표준만 따르는 코드는 현실에서 드물고, 많은 C 코드베이스가 비표준 동작과 언어 확장에 의존함
- 이런 의존은 추가 기능뿐 아니라 컴파일러와 라이브러리마다 다른 버그·공백을 우회하는 과정에서도 생김
- 여러 환경을 지원하려는 코드베이스는 전처리기 검사와 가드에 의존하지만, 이 방식은 쉽게 깨지고 다루기 까다로움
- antcc 같은 C 컴파일러를 만들 때 이런 호환성 문제가 반복해서 드러남
- 많은 오픈소스 프로젝트가 필수적이지 않은 일에도 컴파일러별 비표준 확장과 동작에 의존하면, 대체 컴파일러의 대응 부담이 커짐
- 동시에 모든 개발자가 작고 덜 알려진 컴파일러까지 포함해 여러 컴파일러에서 C 코드를 테스트해야 한다고 요구하기도 어려움
- C 이식성은 그 자체로 충분히 어려움
- 컴파일러 작성자 입장에서 가능한 선택지는 네 가지임
- upstream 패치는 이기기 어려운 싸움처럼 보이고, downstream 패치가 가장 쉬운 방법임
- 많은 코드베이스를 사용자와 개발자에게 최소한의 혼란으로 지원하려면 GCC 호환성 흉내가 현실적이지만 구현 부담이 큼
- clang은 GCC 4.2.1 호환을 주장하기 위해
__GNUC__=4,__GNUC_MINOR__=2,__GNUC_PATCHLEVEL__=1을 정의함 - clang은 지금은 별도 지원 대상에 가깝지만, Linux kernel을 clang으로 컴파일하게 만들기 위해 두 프로젝트 모두에 패치가 필요했을 정도로 큰 노력이 들어감
GCC 매크로와 따라잡기 문제
- GCC인 척하는 방식에도 문제가 있음
- 많은 코드베이스가
#ifdef __GNUC__만 검사하고 버전 확인 없이 최신 GCC 확장을 사용할 수 있음 - 이 경우 대체 컴파일러는 계속 따라잡기를 해야 함
- clang이 4.2.1보다 최신 GNU 확장을 지원하면서도
__GNUC__매크로 값을 올리지 않는 이유 중 하나가 여기에 있음 - 관련 배경은 LLVM의
__GNUC__minor 버전 상향 논의에 있음
더 나은 방향과 현재 상태
댓글과 토론
Lobste.rs 의견들
-
(kefir 컴파일러 작성자)
<sys/cdefs.h>의__attribute__문제는 경험상 가장 골치 아픈 축에 듦. epoll과 일반적인 packed 구조, 생성자, 심볼 가시성을 깨뜨려서 kefir에 다음 몽키패치 헤더를 함께 넣게 됐음
이상적이진 않지만 아마 가장 현실적인 방법이고, 실제로 외부 테스트 스위트에서 커스텀 패치 대부분을 제거할 수 있게 해줬음
또 다른 실패 유형은 버그 있는 대체 코드임. 일부 프로젝트는 컴파일러를 감지해 맞춰 동작하려 하지만, 대체 컴파일러에서 테스트가 부족해 fallback 코드가 버그투성이거나 제대로 유지되지 않는 경우가 있음. 컴파일러 작성자 입장에선 “지원하지 않는 컴파일러”로 바로 실패하는 것보다 훨씬 짜증남. 예를 들어 프로그램과 미리 컴파일된 라이브러리 사이의 정수 typedef 폭 불일치 같은 이상한 오컴파일을 직접 디버깅해야 하기 때문임- 터미널에서도 비슷한 일이 생김.
$TERM을xterm-256color로 설정해서 xterm인 척하지 않으면 온갖 것이 망가짐
어떻게 풀어야 할지 정말 모르겠음. 결국 우리 프로젝트가 충분히 널리 퍼지고 유명해져야 하는 걸까 싶음. 쉽네! - 몽키패치 헤더 방식은 slimcc도 쓰는 방식으로 보이고, 꽤 괜찮은 타협처럼 보임
컴파일러 감지 fallback이 제대로 관리되지 않아 생기는 이상한 오컴파일도 몇 번 겪어본 것 같고, 정말 성가심
- 터미널에서도 비슷한 일이 생김.
-
주로 linux-musl에서 cproc을 개발해서 glibc가 다른 컴파일러에서
__attribute__를 비활성화한다는 건 몰랐는데, 실제로 꽤 나쁜 상황임. 주석에는 attribute 사용은 무시돼도 괜찮다고 되어 있지만, 대부분의 애플리케이션 코드가sys/cdefs.h를 간접적으로 포함하고, 무시하면 안 되는 attribute를 쓸 수 있다는 점을 고려하지 않음
packed외에도aligned와constructor가 흔히 쓰임
이게 어디 이슈 트래커에 보고돼 있는지 궁금함. cdefs.h 안의 attribute 사용 대부분은 이미__glibc_has_attribute로 보호되는 듯해서, 일괄적인__attribute__비활성화가 실제로 뭘 달성하는지, 제거 가능한지도 궁금함
libc 헤더가 쓰는 기능 중 컴파일러가 지원 여부를 잘 표시할 방법이 없는 경우도 문제임.__has_attribute나__has_builtin같은 방식으로 드러나지 않는 기능인데, 떠오르는 예는__asm__라벨임. NetBSD는 이를 심볼 이름 변경에 쓰고,__GNUC__나__PCC__가 없으면#error를 냄. 다만 지원하지 않으면 그냥 시도했다가 실패하게 두는 것 말고 뭘 제안해야 할지는 모르겠음
__builtin_va_list관련 문제도 겪었음. libc가__GNUC__없이 defineva_listtovoid *로 정의하거나, 심지어 충돌하는 정의를 두는 경우가 있음. 이것도__has_builtin으로 테스트할 수 없음.__has_builtin(__builtin_va_arg)가 충분히 좋은 테스트일 수는 있겠지만, macOS에서 이걸 어떻게 고치게 만들 수 있을지는 잘 모르겠음/usr/include/sys와/usr/include/bits에서__attribute__사용을 빠르게 찾아보니, 보호되지 않은 사용이 많았음. 주로__format__,__aligned__,__noreturn__라서 이것들도 고쳐야 할 것임
glibc는 전반적으로 GCC가 아닌 컴파일러와의 호환성을 우선순위로 두지 않는 것 같아서, 그런 패치를 받아줄 가능성은 잘 모르겠음. 올해 초 시스템 업그레이드 후 glibc가 Linux 헤더에 보호 없는__SIZE_TYPE__사용을 추가하면서 내 컴파일러가 일부 프로젝트를 컴파일하지 못하게 됐음. 보고했지만 아직 고쳐지지 않았고, 결국 GCC에 맞추기 위해__X_TYPE__스타일의 미리 정의된 매크로를 추가했음
__asm__라벨 문제는 좋은 해법이 잘 떠오르지 않음. 다만 asm 이름 변경이 애초에 동작에 100% 필요하다면, 컴파일러 체크를 하는 것보다 그냥 시도하고 실패하게 두는 편이 나을 수 있음
__builtin_va_list는 꽤 심각함.__has_builtin(__builtin_va_list)가 동작할 거라고 예상했는데, apparently 그렇지 않음