조건부 컴파일
조건부 컴파일
특정 조건에서만 컴파일되는 코드가 필요할 때가 있어요. 예를 들어 운영체제별로 다른 코드를 넣거나, 특정 아키텍처에서만 동작하는 함수를 만들고 싶을 때죠. 이걸 가능하게 하는 것이 조건부 컴파일이에요. 쉽게 말해, 설정된 조건이 참일 때만 소스 코드가 컴파일에 포함되는 거예요.
이 조건은 컴파일 대상 아키텍처, 컴파일러에 넘긴 임의의 값 등에 따라 달라질 수 있어요. Rust에서는 cfg 속성과 cfg_attr 속성, 그리고 내장 매크로 cfg!와 cfg_select!를 써서 코드를 조건부로 컴파일할 수 있어요.
출처: Rust Reference
구성 조건(Predicate)
조건부 컴파일의 각 형태는 참(true) 또는 거짓(false)으로 평가되는 **구성 조건(configuration predicate)**을 받아요. 문법은 다음과 같아요.
ConfigurationPredicate → ConfigurationOption
| ConfigurationAll
| ConfigurationAny
| ConfigurationNot
| true
| false
ConfigurationOption → IDENTIFIER ( = ( STRING_LITERAL | RAW_STRING_LITERAL ) )?
ConfigurationAll → all(ConfigurationPredicateList?)
ConfigurationAny → any(ConfigurationPredicateList?)
ConfigurationNot → not(ConfigurationPredicate)
ConfigurationPredicateList → ConfigurationPredicate ( , ConfigurationPredicate )* ,?
조건은 다음 중 하나예요.
- 구성 옵션(ConfigurationOption) — 옵션이 설정되어 있으면 참, 설정되어 있지 않으면 거짓이에요.
all()— 쉼표로 구분한 구성 조건 목록을 받아요. 목록의 조건이 모두 참이거나 목록이 비어 있으면 참이에요.any()— 구성 조건 목록을 받아요. 주어진 조건 중 하나라도 참이면 참이고, 조건이 하나도 없으면 거짓이에요.not()— 구성 조건 하나를 받아요. 그 조건이 거짓이면 참, 참이면 거짓이 돼요.true/false— 각각 항상 참, 항상 거짓인 리터럴이에요.
구성 옵션은 이름 또는 키-값 쌍이에요. 이름은 unix처럼 단일 식별자로 쓰고, 키-값 쌍은 target_arch = "x86_64"처럼 식별자, = , 문자열 순서로 써요. = 주변의 공백은 무시되므로 foo="bar"와 foo = "bar"는 같아요. 또 키는 유일할 필요가 없어서, feature = "std"와 feature = "serde"를 동시에 설정할 수 있어요.
설정된 구성 옵션
어떤 구성 옵션이 설정되는지는 크레이트 컴파일 동안 정적으로 결정돼요. 일부 옵션은 컴파일 대상에 대한 데이터를 바탕으로 컴파일러가 설정하고, 다른 옵션은 코드 밖에서 컴파일러에 넘겨진 입력으로 임의 설정돼요. 컴파일 중인 크레이트의 소스 코드 안에서는 구성 옵션을 설정할 수 없어요.
참고로 rustc에서는 임의 설정 옵션을 --cfg 플래그로 지정해요. 지정한 대상의 구성을 보려면 rustc --print cfg --target $TARGET로 확인할 수 있어요. 또 feature 키를 가진 구성 옵션은 Cargo가 컴파일 시점 옵션과 선택적 의존성을 지정하는 데 쓰는 관례예요.
target_arch
대상의 CPU 아키텍처로 한 번 설정되는 키-값 옵션이에요. 값은 대상 트리플의 첫 번째 요소와 비슷하지만 완전히 같진 않아요.
예시 값: "x86", "x86_64", "mips", "powerpc", "powerpc64", "arm", "aarch64"
target_feature
현재 컴파일 대상에서 사용 가능한 각 플랫폼 기능에 대해 설정되는 키-값 옵션이에요.
예시 값: "avx", "avx2", "crt-static", "rdrand", "sse", "sse2", "sse4.1"
target_feature에는 정적 C 런타임을 사용할 수 있음을 나타내는 crt-static이라는 추가 기능도 있어요. 사용 가능한 기능에 대한 자세한 내용은 target_feature 속성을 참고하세요.
target_os
대상의 운영체제로 한 번 설정되는 키-값 옵션이에요. 이 값은 대상 트리플의 두 번째와 세 번째 요소와 비슷해요.
예시 값: "windows", "macos", "ios", "linux", "android", "freebsd", "dragonfly", "openbsd", "netbsd", "none"(임베디드 대상에서 흔함)
target_family
대상에 대한 더 일반적인 설명을 제공하는 키-값 옵션이에요. 대상이 일반적으로 속하는 운영체제나 아키텍처의 계열을 나타내요. target_family 키-값 쌍은 몇 개든 설정할 수 있어요.
예시 값: "unix", "windows", "wasm", 또는 "unix"와 "wasm" 둘 다
target_family = "unix"가 설정되면 unix가, target_family = "windows"가 설정되면 windows가 설정돼요.
target_env
ABI 또는 libc에 대한 정보로 대상 플랫폼을 더 구분하는 키-값 옵션이에요. 역사적인 이유로, 이 값은 실제로 구분이 필요할 때만 빈 문자열이 아닌 값으로 정의돼요. 그래서 많은 GNU 플랫폼에서는 값이 비어 있어요. 이 값은 대상 트리플의 네 번째 요소와 비슷해요. 한 가지 차이는 gnueabihf 같은 임베디드 ABI는 target_env를 그냥 "gnu"로 정의한다는 점이에요.
예시 값: "", "gnu", "msvc", "musl", "sgx", "sim", "macabi"
target_abi
대상 ABI에 대한 정보로 대상을 더 구분하는 키-값 옵션이에요. target_env와 마찬가지로, 역사적인 이유로 실제로 구분이 필요할 때만 빈 문자열이 아닌 값으로 정의돼요. 그래서 많은 GNU 플랫폼에서는 값이 비어 있어요.
예시 값: "", "llvm", "eabihf", "abi64"
target_endian
대상 CPU의 엔디언(endianness)에 따라 "little" 또는 "big" 값으로 한 번 설정되는 키-값 옵션이에요.
target_pointer_width
대상의 포인터 너비를 비트 단위로 한 번 설정하는 키-값 옵션이에요.
예시 값: "16", "32", "64"
target_vendor
대상의 벤더로 한 번 설정되는 키-값 옵션이에요.
예시 값: "apple", "fortanix", "pc", "unknown"
target_has_atomic
대상이 원자적 로드, 저장, 비교-교환(compare-and-swap) 연산을 지원하는 각 비트 너비에 대해 설정되는 키-값 옵션이에요. 이 cfg가 있으면 해당 원자적 너비에 대한 안정적인 core::sync::atomic API를 모두 쓸 수 있어요.
가능한 값: "8", "16", "32", "64", "128", "ptr"
target_has_atomic_primitive_alignment
원자적 타입이 대응하는 정수 타입과 같은 정렬을 갖는 각 비트 너비에 대해 설정되는 키-값 옵션이에요.
정렬은 보통 주어진 비트 너비에서 같아요. 하지만 32비트 x86 같은 일부 대상에서는 AtomicI64 같은 64비트 원자적 타입의 정렬이 8바이트인데 i64는 4바이트로만 정렬돼요. 이런 상황에서는 target_has_atomic_primitive_alignment = "64"가 설정되지 않아요.
가능한 값: "8", "16", "32", "64", "128", "ptr"
test
테스트 하네스를 컴파일할 때 설정돼요. rustc에서는 --test 플래그로 수행돼요.
debug_assertions
최적화 없이 컴파일할 때 기본으로 활성화돼요. 개발 중에 추가 디버깅 코드를 켜되 프로덕션에서는 끄는 데 쓸 수 있어요. 예를 들어 표준 라이브러리의 debug_assert! 매크로 동작을 제어해요.
proc_macro
컴파일 중인 크레이트가 proc_macro 크레이트 타입으로 컴파일될 때 설정돼요.
panic
패닉 전략(panic strategy)에 따라 설정되는 키-값 옵션이에요. 앞으로 값이 더 추가될 수 있어요.
예시 값: "abort", "unwind"
조건부 컴파일의 형태
cfg 속성
cfg 속성은 구성 조건에 따라 속성이 붙은 형태(form)를 조건부로 포함해요.
#![allow(unused)]
fn main() {
// The function is only included in the build when compiling for macOS
#[cfg(target_os = "macos")]
fn macos_only() {
// ...
}
// This function is only included when either foo or bar is defined
#[cfg(any(foo, bar))]
fn needs_foo_or_bar() {
// ...
}
// This function is only included when compiling for a unixish OS with a 32-bit
// architecture
#[cfg(all(unix, target_pointer_width = "32"))]
fn on_32bit_unix() {
// ...
}
// This function is only included when foo is not defined
#[cfg(not(foo))]
fn needs_not_foo() {
// ...
}
// This function is only included when the panic strategy is set to unwind
#[cfg(panic = "unwind")]
fn when_unwinding() {
// ...
}
}
cfg 속성의 문법은 다음과 같아요.
CfgAttribute → cfg ( ConfigurationPredicate )
cfg 속성은 속성이 허용되는 곳이라면 어디든 쓸 수 있고, 한 형태에 여러 번 쓸 수도 있어요. 속성이 붙은 형태는 (크레이트 레벨 속성에서 설명하는 예외를 제외하면) 조건 중 하나라도 거짓이면 포함되지 않아요. 조건이 모두 참이면 형태가 cfg 속성 없이 다시 쓰이고, 하나라도 거짓이면 그 형태는 소스 코드에서 제거돼요.
크레이트 레벨 cfg가 거짓 조건을 가지면, 크레이트 자체는 여전히 존재해요. cfg 앞에 있던 크레이트 속성은 유지되고, 뒤에 있던 속성은 그 뒤의 모든 크레이트 내용과 함께 제거돼요. 앞의 속성을 제거하지 않는 덕분에, #![cfg(...)]가 크레이트 내용을 제거했어도 #![no_std]를 넣어 std 링킹을 피하는 일이 가능해요.
// This `no_std` attribute is kept even though the crate-level `cfg`
// attribute is false.
#![no_std]
#![cfg(false)]
// This function is not included.
pub fn example() {}
cfg_attr 속성
cfg_attr 속성은 구성 조건에 따라 다른 속성을 조건부로 포함해요.
#[cfg_attr(target_os = "linux", path = "linux.rs")]
#[cfg_attr(windows, path = "windows.rs")]
mod os;
위 예시에서 mod os;는 대상에 따라 linux.rs 또는 windows.rs에서 찾아져요. cfg_attr의 문법은 다음과 같아요.
CfgAttrAttribute → cfg_attr ( ConfigurationPredicate , CfgAttrs? )
CfgAttrs → Attr ( , Attr )* ,?
cfg_attr은 속성이 허용되는 곳이라면 어디든, 몇 번이든 쓸 수 있어요. 단 crate_type와 crate_name 속성은 cfg_attr과 함께 쓸 수 없어요. 구성 조건이 참이면 cfg_attr은 조건 뒤에 나열된 속성들로 확장돼요. 속성은 0개, 1개, 그 이상 나열할 수 있고, 여러 개면 각각 별도의 속성으로 확장돼요.
#[cfg_attr(feature = "magic", sparkles, crackles)]
fn bewitched() {}
// When the `magic` feature flag is enabled, the above will expand to:
#[sparkles]
#[crackles]
fn bewitched() {}
참고로 cfg_attr은 다른 cfg_attr로 확장될 수 있어요. #[cfg_attr(target_os = "linux", cfg_attr(feature = "multithreaded", some_other_attribute))]는 유효하고, #[cfg_attr(all(target_os = "linux", feature = "multithreaded"), some_other_attribute)]와 똑같아요.
cfg 매크로
내장 cfg 매크로는 구성 조건 하나를 받아서, 조건이 참이면 true 리터럴로, 거짓이면 false 리터럴로 평가돼요.
#![allow(unused)]
fn main() {
let machine_kind = if cfg!(unix) {
"unix"
} else if cfg!(windows) {
"windows"
} else {
"unknown"
};
println!("I'm running on a {} machine!", machine_kind);
}
cfg_select 매크로
내장 cfg_select! 매크로는 여러 구성 조건에 따라 컴파일 시점에 코드를 선택할 때 써요.
#![allow(unused)]
fn main() {
cfg_select! {
unix => {
fn foo() { /* unix specific functionality */ }
}
target_pointer_width = "32" => {
fn foo() { /* non-unix, 32-bit functionality */ }
}
_ => {
fn foo() { /* fallback implementation */ }
}
}
let is_unix_str = cfg_select! {
unix => "unix",
_ => "not unix",
};
}
cfg_select!의 문법은 다음과 같아요.
CfgSelect → CfgSelectArms?
CfgSelectArms → CfgSelectConfigurationPredicate => ( {^ TokenTree },? CfgSelectArms?
| ExpressionWithBlockNoAttrs ,? CfgSelectArms?
| ExpressionWithoutBlockNoAttrs ( , CfgSelectArms? )? )
CfgSelectConfigurationPredicate → ConfigurationPredicate | _
cfg_select는 구성 조건이 참으로 평가되는 첫 번째 팔(arm)의 페이로드로 확장돼요. 전체 페이로드가 중괄호로 감싸여 있으면 그 중괄호는 확장 중에 제거돼요. 구성 조건 _는 항상 참으로 평가되고, 조건이 하나도 참이 아니면 컴파일 오류가 돼요. 각 오른쪽은 매크로가 호출된 위치에서 문법적으로 유효한 확장이어야 해요.