전처리기
SafeC는 C 전처리 지시문의 안전한 부분집합을 지원하는 전처리기를 포함합니다. 기본적으로 안전 모드로 동작하며, 이는 디버깅하기 어려운 매크로 문제로 이어지는 기능들을 제한합니다. 완전한 C 매크로 지원을 위한 호환 모드도 제공됩니다.
지시문
#include
다른 SafeC 파일이나 C 헤더를 포함합니다:
#include "mymodule.h" // 로컬 인클루드 (따옴표)
#include <stdio.h> // 시스템 인클루드 (꺾쇠 괄호)
시스템 C 헤더는 clang -ast-dump=json으로부터 함수와 typedef 선언을
추출하는 CHeaderImporter를 통해 네이티브로 임포트됩니다. 자세한 내용은
C 상호운용을 참고.
#include는 헤더만 지정합니다. .sc 소스 파일을 포함하는 것은 컴파일 오류입니다: .sc 파일은 그 자체로 하나의 번역 단위이며, 다른 파일에 붙여 넣으면 그 안의 모든 정의가 중복됩니다 — 다중 정의 링크 오류가 나거나, static 상태가 서로 갈라지는 두 벌의 사본이 생깁니다. 각 .sc는 따로 컴파일하고 그 .h를 포함합니다. 모든 사용자가 인스턴스화해야 하는 제네릭 코드는 헤더 자체에 둡니다(표준 라이브러리의 컨테이너가 헤더 전용인 이유가 바로 이것입니다).
포함 검색 경로를 추가하려면 -I <dir>를 사용합니다.
#define (객체형)
객체형 매크로는 단순한 텍스트 치환을 정의합니다:
#define MAX_SIZE 1024
#define VERSION "2.0"
#define DEBUG
안전 모드에서는 함수형 매크로가 거부됩니다:
// 안전 모드에서 에러:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
대신 const 변수와 제네릭 함수를 사용합니다:
const int MAX_SIZE = 1024;
generic<T: Numeric>
T max(T a, T b) {
if (a > b) return a;
return b;
}
#undef
이전에 정의된 매크로를 제거합니다:
#define FEATURE_X
// ... FEATURE_X 사용 ...
#undef FEATURE_X
#pragma once
헤더가 여러 번 포함되는 것을 방지합니다:
#pragma once
// 헤더 내용...
조건부 컴파일
#ifdef / #ifndef
매크로가 정의되어 있는지 검사합니다:
#ifdef DEBUG
printf("debug: x = %d\n", x);
#endif
#ifndef NDEBUG
assert(x > 0);
#endif
#if / #elif / #else / #endif
상수 표현식을 사용한 일반적인 조건부 컴파일:
#if PLATFORM == 1
// 리눅스 전용 코드
#elif PLATFORM == 2
// macOS 전용 코드
#else
// 일반적인 폴백
#endif
중첩
조건부 블록은 중첩될 수 있습니다:
#ifdef FEATURE_A
#ifdef FEATURE_B
// A와 B 모두 활성화됨
#else
// A만 활성화됨
#endif
#endif
#error와 #warning
#error message는 그 메시지와 함께 컴파일을 멈추고, #warning message는 메시지를 보고한 뒤 계속합니다. #if 안의 defined(NAME)과 함께 쓰면 헤더가 지원하지 않는 구성을 막을 수 있습니다.
#if !defined(__linux__) && !defined(__APPLE__) && !defined(_WIN32) && !defined(__SAFEC_FREESTANDING__)
#error "unsupported target"
#endif
#ifndef NDEBUG
#warning "debug build"
#endif
#pragma once 외의 #pragma는 받아들이되 무시합니다.
사전 정의된 매크로
| 매크로 | 값 | 설명 |
|---|---|---|
__FILE__ |
문자열 리터럴 | 현재 소스 파일 이름 |
__LINE__ |
정수 리터럴 | 현재 줄 번호 |
__SIZEOF_POINTER__ |
4 또는 8 |
타깃의 포인터 너비 |
__SAFEC_FREESTANDING__ |
1 |
--freestanding, 또는 베어메탈/WebAssembly --target 별칭 |
__SAFEC_COMPAT_PREPROCESSOR__ |
1 |
--compat-preprocessor가 켜져 있음 |
__linux__, __APPLE__, _WIN32 |
1 |
해당 OS용 호스티드 빌드(베어메탈 타깃에서는 정의되지 않음) |
__x86_64__/__amd64__, __i386__, __aarch64__ |
1 |
타깃 아키텍처 |
__riscv, __riscv_xlen |
1, 32/64 |
RISC-V 타깃 |
__wasm__, __wasm32__/__wasm64__ |
1 |
WebAssembly 타깃 |
__arm__, __ARM_ARCH, __ARM_ARCH_PROFILE, __ARM_ARCH_<name>__, __thumb__, __thumb2__ |
1, 6/7/8, 'A'/'R'/'M' |
32비트 ARM, clang과 같은 철자(__ARM_ARCH_7EM__, __ARM_ARCH_8M_MAIN__, …) |
C와 달리 SafeC는 빌드를 재현 가능하게 유지하려고 의도적으로
__TIME__과 __DATE__를 두지 않습니다: 둘 중 하나라도 쓰면 컴파일 오류입니다("TIME and DATE are disabled in SafeC").
명령줄 플래그
-D <name>[=value]
명령줄에서 매크로를 정의합니다:
./build/safec program.sc -D DEBUG -D MAX_SIZE=2048 --emit-llvm -o program.ll
-I <dir>
포함 검색 디렉터리를 추가합니다:
./build/safec program.sc -I ./include -I ../lib/include --emit-llvm -o program.ll
--no-import-c-headers
네이티브 C 헤더 임포트 메커니즘을 비활성화합니다. 이 플래그를 사용하면
#include <stdio.h>가 AST 추출을 위해 clang을 호출하지 않습니다.
extern 선언을 직접 제공해야 합니다.
--compat-preprocessor
완전한 C 전처리기 기능을 갖춘 호환 모드를 활성화합니다(아래 참고).
안전 모드 대 호환 모드
안전 모드 (기본값)
기본 전처리기 모드는 C에서 흔한 버그의 원인이 되는 기능을 제한합니다:
| 기능 | 안전 모드 | 근거 |
|---|---|---|
객체형 #define |
허용됨 | 단순한 상수 치환 |
| 함수형 매크로 | 거부됨 | 대신 const + 제네릭 사용 |
## (토큰 붙이기) |
거부됨 | 코드를 난독화함 |
# (문자열화) |
거부됨 | 코드를 난독화함 |
#include |
허용됨 | 파일 포함은 필수적 |
#ifdef / #if |
허용됨 | 조건부 컴파일은 필수적 |
#pragma once |
허용됨 | 헤더 가드 |
호환 모드 (--compat-preprocessor)
레거시 C 코드나 완전한 매크로 지원이 필요한 헤더를 다룰 때는 호환 모드를 활성화합니다:
./build/safec legacy.sc --compat-preprocessor --emit-llvm -o legacy.ll
safeguard 패키지는 [build] compat_preprocessor = true로 자기 소스에 이 모드를 켭니다(패키지 매니저 참고).
호환 모드에서는 함수형 매크로, 토큰 붙이기, 문자열화가 모두 허용되며, 전처리기가 __SAFEC_COMPAT_PREPROCESSOR__를 정의합니다. 선택적인 함수형 편의 매크로를 제공하는 헤더는 이 매크로로 그것을 감싸서, 같은 헤더가 두 모드 모두에서 동작하게 합니다 — 표준 라이브러리는 ASSERT_*(std/test/test.h), RING_STATIC, 로깅·패닉 헬퍼 등에서 이렇게 합니다:
#ifdef __SAFEC_COMPAT_PREPROCESSOR__ // function-like macro: compat mode only
#define ASSERT_TRUE(e) test_assert_true((e) != 0, #e, __FILE__, __LINE__)
#endif
함수형 매크로는 자기가 선언하는 지역 변수에 대해 위생적입니다: 매크로 본문에서 선언한 변수는 전개될 때마다 이름이 바뀌므로, 호출한 쪽의 같은 이름 변수를 붙잡거나 가리지 않습니다.
#define SWAP(a, b) { int tmp = a; a = b; b = tmp; }
int tmp = 1, x = 2;
SWAP(tmp, x); // expands to { int __hyg1_tmp = tmp; tmp = x; x = __hyg1_tmp; }
매크로 본문에서 선언한 struct의 필드는 이름을 유지합니다.
컴파일 타임 대안
SafeC는 대부분의 전처리기 사용 사례를 대체하는 일급 언어 기능을 제공합니다:
| C 전처리기 패턴 | SafeC 대안 |
|---|---|
#define PI 3.14159 |
const double PI = 3.14159; |
#define MAX(a,b) ... |
generic<T> T max(T a, T b) { ... } |
#ifdef DEBUG |
if const (DEBUG) { ... } |
#define ARRAY_SIZE 100 |
const int ARRAY_SIZE = 100; |
| 조건부 함수 본문 | consteval 조건과 함께 if const |
매크로 대신 언어 수준의 구성을 사용하면 다음이 제공됩니다:
- 타입 안전성:
const와 제네릭은 타입 검사됨 - 스코핑:
const변수는 어휘적 스코프를 따름 - 디버깅 용이성: 눈에 보이지 않는 텍스트 치환이 없음
- IDE 지원: 정의로 이동, 이름 바꾸기, 호버가 모두 올바르게 동작함
예시
#pragma once
#define VERSION 1
#ifdef DEBUG
#define LOG_LEVEL 3
#else
#define LOG_LEVEL 0
#endif
const int MAX_CONNECTIONS = 128;
// 타입이 있는 상수에는 #define보다 const를 선호
const double TIMEOUT_SEC = 30.0;
// 함수형 매크로보다 제네릭 함수를 선호
generic<T: Numeric>
T clamp(T val, T lo, T hi) {
if (val < lo) return lo;
if (val > hi) return hi;
return val;
}