Preprocessor
SafeC includes a preprocessor that supports a safe subset of C preprocessing directives. By default, it operates in safe mode, which restricts features that lead to hard-to-debug macro issues. A compatibility mode is available for full C macro support.
Directives
#include
Include other SafeC files or C headers:
#include "mymodule.h" // local include (quoted)
#include <stdio.h> // system include (angle brackets)
System C headers are imported natively via the CHeaderImporter, which extracts function and typedef declarations from clang -ast-dump=json. See C Interop for details.
#include names headers only. Including a .sc source file is a compile error: a .sc file is a translation unit of its own, and pasting it into another duplicates every definition it holds — multiple-definition link errors, or two diverging copies of its static state. Compile each .sc separately and include its .h; generic code that every user must instantiate lives in the header itself (the standard library's containers are header-only for exactly this reason).
Use -I <dir> to add include search paths.
#define (Object-Like)
Object-like macros define simple text substitutions:
#define MAX_SIZE 1024
#define VERSION "2.0"
#define DEBUG
In safe mode, function-like macros are rejected:
// ERROR in safe mode:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
Use const variables and generic functions instead:
const int MAX_SIZE = 1024;
generic<T: Numeric>
T max(T a, T b) {
if (a > b) return a;
return b;
}
#undef
Remove a previously defined macro:
#define FEATURE_X
// ... use FEATURE_X ...
#undef FEATURE_X
#pragma once
Prevent a header from being included multiple times:
#pragma once
// header contents...
Conditional Compilation
#ifdef / #ifndef
Test whether a macro is defined:
#ifdef DEBUG
printf("debug: x = %d\n", x);
#endif
#ifndef NDEBUG
assert(x > 0);
#endif
#if / #elif / #else / #endif
General conditional compilation with constant expressions:
#if PLATFORM == 1
// Linux-specific code
#elif PLATFORM == 2
// macOS-specific code
#else
// Generic fallback
#endif
Nesting
Conditional blocks can be nested:
#ifdef FEATURE_A
#ifdef FEATURE_B
// both A and B enabled
#else
// only A enabled
#endif
#endif
#error and #warning
#error message stops compilation with that message; #warning message reports it and carries on. Together with defined(NAME) in #if they guard a header against configurations it doesn't support:
#if !defined(__linux__) && !defined(__APPLE__) && !defined(_WIN32) && !defined(__SAFEC_FREESTANDING__)
#error "unsupported target"
#endif
#ifndef NDEBUG
#warning "debug build"
#endif
Any other #pragma than #pragma once is accepted and ignored.
Predefined Macros
| Macro | Value | Description |
|---|---|---|
__FILE__ |
String literal | Current source file name |
__LINE__ |
Integer literal | Current line number |
__SIZEOF_POINTER__ |
4 or 8 |
Pointer width of the target |
__SAFEC_FREESTANDING__ |
1 |
--freestanding, or any bare-metal/WebAssembly --target alias |
__SAFEC_COMPAT_PREPROCESSOR__ |
1 |
--compat-preprocessor is on |
__linux__, __APPLE__, _WIN32 |
1 |
Hosted builds for that OS (never on bare-metal targets) |
__x86_64__/__amd64__, __i386__, __aarch64__ |
1 |
Target architecture |
__riscv, __riscv_xlen |
1, 32/64 |
RISC-V targets |
__wasm__, __wasm32__/__wasm64__ |
1 |
WebAssembly targets |
__arm__, __ARM_ARCH, __ARM_ARCH_PROFILE, __ARM_ARCH_<name>__, __thumb__, __thumb2__ |
1, 6/7/8, 'A'/'R'/'M' |
32-bit ARM, spelled as clang does (__ARM_ARCH_7EM__, __ARM_ARCH_8M_MAIN__, …) |
Unlike C, SafeC deliberately leaves out __TIME__ and __DATE__ to keep builds reproducible: using either is a compile error ("TIME and DATE are disabled in SafeC").
Command-Line Flags
-D <name>[=value]
Define a macro from the command line:
./build/safec program.sc -D DEBUG -D MAX_SIZE=2048 --emit-llvm -o program.ll
-I <dir>
Add an include search directory:
./build/safec program.sc -I ./include -I ../lib/include --emit-llvm -o program.ll
--no-import-c-headers
Disable the native C header import mechanism. With this flag, #include <stdio.h> will not invoke clang for AST extraction. You must provide extern declarations manually.
--compat-preprocessor
Enable compatibility mode with full C preprocessor features (see below).
Safe Mode vs Compatibility Mode
Safe Mode (Default)
The default preprocessor mode restricts features that are common sources of bugs in C:
| Feature | Safe Mode | Rationale |
|---|---|---|
Object-like #define |
Allowed | Simple constant substitution |
| Function-like macros | Rejected | Use const + generics instead |
## (token pasting) |
Rejected | Obfuscates code |
# (stringification) |
Rejected | Obfuscates code |
#include |
Allowed | File inclusion is necessary |
#ifdef / #if |
Allowed | Conditional compilation is necessary |
#pragma once |
Allowed | Header guard |
Compatibility Mode (--compat-preprocessor)
When working with legacy C code or headers that require full macro support, enable compatibility mode:
./build/safec legacy.sc --compat-preprocessor --emit-llvm -o legacy.ll
A safeguard package turns it on for its own sources with [build] compat_preprocessor = true (see Package Manager).
In compatibility mode, function-like macros, token pasting, and stringification are all permitted, and the preprocessor defines __SAFEC_COMPAT_PREPROCESSOR__. Headers that offer optional function-like convenience macros guard them with it, so the same header works in both modes — the standard library does this for ASSERT_* (std/test/test.h), RING_STATIC, the logging and panic helpers, and the rest:
#ifdef __SAFEC_COMPAT_PREPROCESSOR__ // function-like macro: compat mode only
#define ASSERT_TRUE(e) test_assert_true((e) != 0, #e, __FILE__, __LINE__)
#endif
Function-like macros are hygienic for the locals they declare: a variable declared in a macro body is renamed at each expansion, so it can't capture or shadow a caller's variable of the same name:
#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; }
Fields of a struct declared in a macro body keep their names.
Compile-Time Alternatives
SafeC provides first-class language features that replace most preprocessor use cases:
| C Preprocessor Pattern | SafeC Alternative |
|---|---|
#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; |
| Conditional function body | if const with consteval conditions |
Using language-level constructs instead of macros provides:
- Type safety:
constand generics are type-checked - Scoping:
constvariables respect lexical scope - Debuggability: no invisible text substitution
- IDE support: go-to-definition, rename, hover all work correctly
Example
#pragma once
#define VERSION 1
#ifdef DEBUG
#define LOG_LEVEL 3
#else
#define LOG_LEVEL 0
#endif
const int MAX_CONNECTIONS = 128;
// Prefer const over #define for typed constants
const double TIMEOUT_SEC = 30.0;
// Prefer generic functions over function-like macros
generic<T: Numeric>
T clamp(T val, T lo, T hi) {
if (val < lo) return lo;
if (val > hi) return hi;
return val;
}