C 상위집합 호환성
SafeC는 C의 상위집합입니다: C 자체가 수십 년간의 시스템 프로그래밍에서
물려받은 패턴을 포함해 실제 C 코드는, 근본적으로 메모리 안전하지 않은
구성(원시 포인터 연산, 검사되지 않은 캐스트 등)을 사용하는 경우를
제외하면 수정 없이 컴파일됩니다 — 그런 구성조차 SafeC는 완전히 거부하는
대신 unsafe { }로 감싸도록 요구할 뿐입니다. 이 페이지는 타입,
함수, 메모리와 리전에서
설명한 핵심 언어를 넘어, SafeC가 그대로 받아들이는 C 구성들을 다룹니다.
__attribute__((...))
GCC/Clang의 어트리뷰트 문법은 실제 C 코드가 사용하는 모든 위치 — 접두사, 접미사, 구조체 태그, 구조체 본문 뒤 — 에서 받아들여지며, 인식되는 어트리뷰트는 코드 생성에 영향을 줍니다. 인식되지 않는 어트리뷰트는 거부되지 않고 그대로 용인된다(파싱만 되고 무시됨) — 그래서 GCC/Clang용으로 작성된 헤더를 임포트할 때 수정이 필요 없습니다.
__attribute__((always_inline)) static int add_one(int x) { return x + 1; }
int subtract_one(int x) __attribute__((pure));
struct __attribute__((packed)) PackedPoint {
unsigned char tag;
int value;
};
int g_aligned_val __attribute__((aligned(16))) = 7;
인식되는 어트리뷰트: always_inline, pure, packed, aligned(N)
(단순 aligned는 타깃의 최대 유효 정렬값을 기본값으로 사용),
section("name") (SafeC 네이티브 section(...)과 동일한 효과 —
베어메탈 참고), 그리고:
| 어트리뷰트 | 효과 |
|---|---|
weak |
정의에 붙이면 다른 정의가 덮어쓸 수 있는 약한 정의가 됩니다. extern 선언에 붙이면 그 심볼을 정의한 곳이 없을 때 null이 되는 참조가 되므로, 프로그램이 선택적 라이브러리를 링크했을 때만 그것을 쓸 수 있습니다(if ((void*)f != (void*)0) f(...)). |
target("avx2,fma") |
이 함수 하나만 빌드 자체의 ISA 기능에 더해 이 기능들을 쓸 수 있습니다. CPU 지원에 따라 실행 시점에 고르는 커널용입니다(CPU SIMD 수준 참고). 기능 이름은 LLVM의 것입니다(avx, avx2, fma, avx512f, sve, mve.fp, ...). |
noinline, cold |
이 함수를 인라인하지 않고, 드물게 실행된다고 표시합니다. 컨테이너는 이것을 성장 경로(Vec::push의 재할당, HashMap의 재해시)에 써서, 인라인되는 빠른 경로가 상태를 레지스터에 유지하게 합니다. |
noreturn |
SafeC의 noreturn과 같음: 함수가 반환하지 않음(함수 참고) |
warn_unused_result, nodiscard |
must_use와 같음: 결과를 버리면 경고. 정의에(또는 호출하는 쪽이 보는 유일한 선언인 헤더의 선언에) 붙이십시오 — 그것이 없는 나중의 정의는 이를 유지하지 않습니다 |
const |
pure와 같음 |
export_name("f"), import_module("m"), import_name("f") |
WebAssembly 내보내기와 가져오기(WebAssembly 참고) |
gnu_inline, artificial |
함수를 inline으로 만듦 |
그 밖의 어트리뷰트(visibility, unused, deprecated, ...)는 받아들이되 무시하므로, 이를 쓰는 C 헤더도 파싱됩니다.
함수 어트리뷰트는 선언 앞이나 매개변수 목록 뒤에 올 수 있고, const 메서드의 const 뒤에도 올 수 있습니다(int T::get() const __attribute__((noinline)) { ... }). 제네릭 함수는 모든 인스턴스에서 어트리뷰트를 유지합니다.
always_inline은 실제로 인라인을 강제하므로, 한 번 작성한 커널 본문을 그것을 호출하는 작은 target(...) 래퍼들로 여러 ISA 수준에 맞게 컴파일할 수 있습니다:
static float body(const float* x, unsigned long n) __attribute__((always_inline)) { ... }
static float body_avx2(const float* x, unsigned long n) __attribute__((target("avx2,fma"))) { return body(x, n); }
C 스타일 함수 포인터
SafeC 네이티브 문법인 fn RetType(Params)와 더불어, 고전적인 C
선언자(declarator) 형식도 타입이 등장할 수 있는 모든 곳 — typedef,
전역 변수, 구조체 필드, 함수 매개변수 — 에서 동작합니다.
typedef int (*BinOp)(int, int);
int (*g_op)(int, int) = mul_impl;
struct Handler {
void (*on_event)(int);
};
static int apply(int (*callback)(int, int), int x, int y) {
return callback(x, y);
}
비트필드
struct Flags {
unsigned int a : 4;
unsigned int b : 12;
unsigned int c : 16;
int sc : 4; // 부호 있는 비트필드
int tag; // 비트필드 뒤의 일반 필드, 자신만의 슬롯을 가짐
};
동일한 저장 단위를 공유하는 비트필드들은 하나의 정수 슬롯으로 묶여,
읽고 쓸 때마다 (복합 대입 f.a += 1;, f.a &= 0xF; 포함) 시프트/마스크
코드가 생성됩니다 — 한 비트필드에 대한 단순 저장이 같은 단위에 함께
묶인 형제 필드를 훼손하는 일은 절대 없습니다.
지정 초기화자
struct Point { int x; int y; int z; };
struct Point p = { .y = 5, .x = 1, .z = 9 }; // 순서 무관
int arr[6] = { 1, 2, [4] = 40, 5 }; // 인덱스 3은 0으로 채워짐
int global_arr[5] = { [1] = 10, [3] = 30 }; // 0, 2, 4는 0으로 채워짐
지정자는 위치 기반 초기화자와 섞어 쓸 수 있습니다. 배열 지정자
([N] = value)와 구조체 지정자(.field = value) 모두 일반 위치 기반
초기화에 Sema가 사용하는 것과 동일한 슬롯 매핑을 통해 처리되므로,
부분적으로만 지정된 애그리게이트는 C와 정확히 동일하게 0으로 채워집니다.
가변 길이 배열 멤버(Flexible Array Members)
struct Msg {
int len;
unsigned char data[]; // 반드시 마지막 필드여야 함
};
unsafe {
struct Msg* m = (struct Msg*)malloc(sizeof(struct Msg) + 4UL);
m->data[0] = (unsigned char)10;
}
sizeof(struct Msg)는 C와 마찬가지로 가변 길이 멤버를 제외합니다 —
초과 할당과 구조체의 고정 부분을 지난 인덱싱은 호출자의 책임이며,
그래서 위 접근에는 unsafe가 필요합니다.
익명 구조체/유니온
struct Variant {
int tag;
union { // 익명 -- 멤버가 Variant 자신의 스코프로 승격됨
int as_int;
float as_float;
};
};
struct Nested {
int id;
struct { // 익명 중첩 구조체, 동일하게 승격됨
int x;
int y;
};
};
struct Variant v;
v.as_int = 42; // 중간 멤버 이름이 필요 없음
복합 리터럴
struct Point { int x; int y; };
int total = sum_point((struct Point){3, 4});
struct Point p2 = (struct Point){.x = 10, .y = 20}; // 여기서도 지정자를 쓸 수 있음
_Generic
C11의 타입 제네릭 선택(type-generic selection)은 컴파일 타임에 제어
표현식의 타입에 따라 디스패치한다는 점에서, SafeC 자체의
generic<T>(인스턴스화되는 타입마다 함수
본문을 단형화하는 방식)와는 다릅니다. _Generic은 대신 단일 인자의
타입에 따라 이미 작성되어 있는 여러 표현식 중 하나를 고릅니다:
int describe(int i, double d) {
int r1 = _Generic(i, int: 100, double: 200, default: -1); // 100
int r2 = _Generic(d, int: 100, double: 200, default: -1); // 200
return r1 + r2;
}
가변 길이 배열(Variable-Length Arrays)
VLA는 지원되지만 unsafe { } 안에서만 가능합니다 — 런타임에 크기가
결정되는 스택 할당은 안전한 영역이 설계상 배제하는, 검사되지 않는
크기 연산의 전형적인 예입니다(안전성 참고).
int sum_vla(int n) {
int total = 0;
unsafe {
int arr[n]; // n은 컴파일 타임 상수가 아니어도 됨
for (int i = 0; i < n; i++) arr[i] = i * 2;
for (int i = 0; i < n; i++) total += arr[i];
}
return total;
}