C 상위집합 호환성

SafeC는 C의 상위집합입니다: C 자체가 수십 년간의 시스템 프로그래밍에서 물려받은 패턴을 포함해 실제 C 코드는, 근본적으로 메모리 안전하지 않은 구성(원시 포인터 연산, 검사되지 않은 캐스트 등)을 사용하는 경우를 제외하면 수정 없이 컴파일됩니다 — 그런 구성조차 SafeC는 완전히 거부하는 대신 unsafe { }로 감싸도록 요구할 뿐입니다. 이 페이지는 타입, 함수, 메모리와 리전에서 설명한 핵심 언어를 넘어, SafeC가 그대로 받아들이는 C 구성들을 다룹니다.

__attribute__((...))

GCC/Clang의 어트리뷰트 문법은 실제 C 코드가 사용하는 모든 위치 — 접두사, 접미사, 구조체 태그, 구조체 본문 뒤 — 에서 받아들여지며, 인식되는 어트리뷰트는 코드 생성에 영향을 줍니다. 인식되지 않는 어트리뷰트는 거부되지 않고 그대로 용인된다(파싱만 되고 무시됨) — 그래서 GCC/Clang용으로 작성된 헤더를 임포트할 때 수정이 필요 없습니다.

c
__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 수준에 맞게 컴파일할 수 있습니다:

c
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, 전역 변수, 구조체 필드, 함수 매개변수 — 에서 동작합니다.

c
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);
}

비트필드

c
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; 포함) 시프트/마스크 코드가 생성됩니다 — 한 비트필드에 대한 단순 저장이 같은 단위에 함께 묶인 형제 필드를 훼손하는 일은 절대 없습니다.

지정 초기화자

c
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)

c
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가 필요합니다.

익명 구조체/유니온

c
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;   // 중간 멤버 이름이 필요 없음

복합 리터럴

c
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은 대신 단일 인자의 타입에 따라 이미 작성되어 있는 여러 표현식 중 하나를 고릅니다:

c
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 { } 안에서만 가능합니다 — 런타임에 크기가 결정되는 스택 할당은 안전한 영역이 설계상 배제하는, 검사되지 않는 크기 연산의 전형적인 예입니다(안전성 참고).

c
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;
}