링키지
링키지 (Linkage)
여러 crate를 하나로 묶어 실행 파일이나 라이브러리로 만드는 과정을 링킹(linkage) 이라고 해요. 이 장은 언어 자체보다 컴파일러의 관점에서 설명한다는 점을 먼저 짚어둘게요. crate를 정적·동적으로 연결하는 다양한 방법을 살펴보고, 네이티브 라이브러리에 대한 더 자세한 내용은 Rust Book의 FFI 장에서 다뤄요.
본문
한 번의 컴파일 세션에서, 컴파일러는 커맨드라인 플래그나 crate_type 속성을 통해 여러 산출물(artifact) 을 생성할 수 있어요. 커맨드라인 플래그가 하나 이상 지정되면, 모든 crate_type 속성은 무시되고 커맨드라인이 지정한 산출물만 빌드돼요.
crate 타입들
각 --crate-type이 무엇을 만드는지 하나씩 볼게요.
-
--crate-type=bin/#![crate_type = "bin"]— 실행 가능한 실행 파일을 만들어요. crate에 프로그램 시작 시 실행될main함수가 필요해요. 모든 Rust·네이티브 의존성을 링크해서 배포 가능한 단일 바이너리를 생산해요. 이게 기본 crate 타입이에요. -
--crate-type=lib/#![crate_type = "lib"]— Rust 라이브러리를 만들어요. 라이브러리는 여러 형태로 나타날 수 있어서 정확히 무엇이 만들어지는지는 모호한데, 이 일반lib옵션의 목적은 "컴파일러가 권장하는" 스타일의 라이브러리를 생성하는 거예요. 결과물은 항상 rustc가 쓸 수 있지만, 실제 라이브러리 타입은 때에 따라 바뀔 수 있어요. 나머지 출력 타입들은 모두 라이브러리의 다른 종류들이고,lib은 그중 하나의 별칭으로 볼 수 있어요(실제로는 컴파일러가 정의). -
--crate-type=dylib/#![crate_type = "dylib"]— 동적 Rust 라이브러리를 만들어요.lib출력 타입과 달리 동적 라이브러리 생성을 강제해요. 결과 동적 라이브러리는 다른 라이브러리나 실행 파일의 의존성으로 쓸 수 있어요. Linux에서는*.so, macOS에서는*.dylib, Windows에서는*.dll파일을 만들어요. -
--crate-type=staticlib/#![crate_type = "staticlib"]— 정적 시스템 라이브러리를 만들어요. 다른 라이브러리 출력과 달리 컴파일러는staticlib출력에 링크를 시도하지 않아요. 이 출력 타입의 목적은 로컬 crate의 모든 코드와 모든 상위(upstream) 의존성을 포함하는 정적 라이브러리를 만드는 거예요. Linux, macOS, Windows(MinGW)에서는*.a, Windows(MSVC)에서는*.lib파일을 만들어요. 다른 Rust 코드에 대한 동적 의존성이 없기 때문에, Rust 코드를 기존 non-Rust 애플리케이션에 링크하는 상황에 권장돼요.정적 라이브러리가 가질 수 있는 동적 의존성(시스템 라이브러리나 동적 라이브러리로 컴파일된 Rust 라이브러리에 대한 의존성 같은 것)은 그 staticlib을 다른 데서 링크할 때 수동으로 지정해야 해요.
--print=native-static-libs플래그가 도움이 될 수 있어요.결과 정적 라이브러리는 모든 의존성(표준 라이브러리 포함)의 코드를 담고 그들의 모든 공개 심볼을 export하기 때문에, 그것을 실행 파일이나 공유 라이브러리에 링크할 때 특별히 주의가 필요해요. 공유 라이브러리의 경우 export 심볼 목록을 예컨대 링커/symbol version 스크립트, export 심볼 목록(macOS), 모듈 정의 파일(Windows) 등으로 제한해야 해요. 또 실제로 사용되지 않는 의존성의 코드를 모두 제거하기 위해 사용하지 않는 섹션을 없앨 수도 있어요(예:
--gc-sections, macOS의-dead_strip). -
--crate-type=cdylib/#![crate_type = "cdylib"]— 동적 시스템 라이브러리를 만들어요. 다른 언어에서 로드될 동적 라이브러리를 컴파일할 때 써요. Linux에서는*.so, macOS에서는*.dylib, Windows에서는*.dll파일을 만들어요. -
--crate-type=rlib/#![crate_type = "rlib"]— "Rust 라이브러리" 파일을 만들어요. 중간 산출물로 쓰이며 "정적 Rust 라이브러리"라고 생각할 수 있어요.staticlib파일과 달리rlib파일은 미래의 링킹에서 컴파일러가 해석해요. 즉 rustc가 동적 라이브러리에서 메타데이터를 찾듯이rlib파일에서도 메타데이터를 찾는다는 뜻이에요. 이 출력 형태는 정적으로 링크된 실행 파일과staticlib출력을 만드는 데 쓰여요. -
--crate-type=proc-macro/#![crate_type = "proc-macro"]— 출력은 명시되어 있지 않지만,-L경로가 주어지면 컴파일러가 그 출력 산출물을 매크로로 인식해서 프로그램에서 로드할 수 있어요. 이 crate 타입으로 컴파일된 crate는 절차적 매크로만 export해야 해요. 컴파일러는 자동으로proc_macro설정 옵션을 설정해요. 이 crate는 항상 컴파일러 자신이 빌드된 것과 같은 타겟으로 컴파일돼요. 예를 들어 Linux에서 x86_64 CPU로 컴파일러를 실행한다면, crate가 다른 타겟용으로 빌드되는 다른 crate의 의존성이라도 타겟은x86_64-unknown-linux-gnu가 돼요.
이 출력들은 쌓을 수 있어요(stackable). 여러 개를 지정하면 컴파일러가 재컴파일 없이 각 형태의 출력을 만들어요. 다만 이것은 같은 방법으로 지정된 출력에만 적용돼요.
crate_type속성만 지정하면 그게 모두 빌드되지만,--crate-type커맨드라인 플래그가 하나 이상 있으면 그 출력들만 빌드돼요.
의존성 형식 선택 규칙
crate A가 crate B에 의존한다면, 컴파일러는 시스템에서 B를 다양한 형태로 찾을 수 있어요. 다만 컴파일러가 찾는 형태는 rlib 형식과 동적 라이브러리 형식뿐이에요. 두 가지 선택지가 있을 때 컴파일러는 다음 규칙을 따라요.
- 정적 라이브러리를 만들 때, 모든 상위 의존성은
rlib형식으로 있어야 해요. 동적 라이브러리는 정적 형식으로 변환할 수 없기 때문이에요. 또한 정적 라이브러리에 네이티브 동적 의존성을 링크하는 것은 불가능한데, 이 경우 링크되지 않은 네이티브 동적 의존성에 대한 경고가 출력돼요. rlib파일을 만들 때, 상위 의존성 형식에 제한이 없어요. 모든 상위 의존성이 메타데이터를 읽기 위해 존재하기만 하면 돼요.rlib파일은 상위 의존성을 아예 담고 있지 않기 때문이에요. 모든rlib파일이libstd.rlib의 복사본을 담고 있으면 비효율적이죠.- 실행 파일을 만들 때
-C prefer-dynamic플래그가 없으면, 의존성을 먼저rlib형식으로 찾으려 시도해요. 어떤 의존성이 rlib 형식으로 없으면 동적 링킹을 시도해요. - 동적 라이브러리나 동적으로 링크되는 실행 파일을 만들 때, 컴파일러는 사용 가능한 의존성을 rlib 또는 dylib 형식으로 조정해서 최종 제품을 만들어요.
컴파일러의 주요 목표 중 하나는 어떤 라이브러리도 어떤 산출물에 두 번 이상 나타나지 않게 하는 거예요. 예를 들어 동적 라이브러리 B와 C가 각각 라이브러리 A를 정적으로 링크했다면, crate는 B와 C를 함께 링크할 수 없어요 — A의 복사본이 두 개가 되기 때문이에요. 컴파일러는 rlib과 dylib 형식을 혼합하는 것을 허용하지만, 이 제약은 반드시 지켜져야 해요.
컴파일러는 현재 "이 라이브러리는 어떤 형식으로 링크돼야 한다"고 힌트하는 방법을 구현하고 있지 않아요. 동적 링킹을 할 때 컴파일러는 일부 의존성을 rlib으로 링크하는 것도 허용하면서 동적 의존성을 최대화하려 시도해요. 대부분의 상황에서 동적 링킹을 쓴다면 모든 라이브러리를 dylib으로 두는 게 권장돼요. 다른 상황에서는 컴파일러가 어떤 형식으로 각 라이브러리를 링크할지 결정하지 못할 때 경고를 내보내요.
일반적으로 대부분의 컴파일 요구에는 --crate-type=bin 또는 --crate-type=lib면 충분하고, 나머지 옵션들은 crate의 출력 형식에 더 세밀한 제어가 필요할 때만 쓰면 돼요.
정적·동적 C 런타임
표준 라이브러리는 일반적으로 타겟에 적절하게 정적 링크 C 런타임과 동적 링크 C 런타임을 모두 지원하려 노력해요. 예를 들어 x86_64-pc-windows-msvc와 x86_64-unknown-linux-musl 타겟은 보통 두 런타임을 모두 가지며 사용자가 하나를 선택해요. 컴파일러의 모든 타겟은 C 런타임에 링크하는 기본 모드가 있어요. 보통 타겟은 기본적으로 동적 링크를 해요. 하지만 기본적으로 정적 링크를 하는 예외도 있는데, 그런 게 다음과 같아요.
arm-unknown-linux-musleabiarm-unknown-linux-musleabihfarmv7-unknown-linux-musleabihfi686-unknown-linux-muslx86_64-unknown-linux-musl
C 런타임의 링키지는 crt-static 타겟 피처를 존중하도록 설정돼 있어요. 이 타겟 피처들은 보통 컴파일러 자신에게 플래그로 커맨드라인에서 설정돼요. 예를 들어 정적 런타임을 켜려면 이렇게 실행해요.
rustc -C target-feature=+crt-static foo.rs
동적 런타임으로 링크하려면 이렇게요.
rustc -C target-feature=-crt-static foo.rs
C 런타임 링키지 전환을 지원하지 않는 타겟은 이 플래그를 무시해요. 컴파일이 성공한 뒤 결과 바이너리를 검사해서 예상대로 링크되었는지 확인하는 걸 권장해요.
crate도 C 런타임이 어떻게 링크되고 있는지 알 수 있어요. 예를 들어 MSVC의 코드는 링크되는 런타임에 따라 다르게 컴파일돼야 해요(예: /MT 또는 /MD). 이건 현재 cfg 속성의 target_feature 옵션으로 export돼요.
#[cfg(target_feature = "crt-static")]
fn foo() {
println!("the C runtime should be statically linked");
}
#[cfg(not(target_feature = "crt-static"))]
fn foo() {
println!("the C runtime should be dynamically linked");
}
또 Cargo 빌드 스크립트는 환경 변수를 통해 이 피처를 알 수 있어요. 빌드 스크립트에서 이렇게 감지할 수 있어요.
use std::env;
fn main() {
let linkage = env::var("CARGO_CFG_TARGET_FEATURE").unwrap_or(String::new());
if linkage.contains("crt-static") {
println!("the C runtime will be statically linked");
} else {
println!("the C runtime will be dynamically linked");
}
}
로컬에서 이 피처를 쓰려면 보통 RUSTFLAGS 환경 변수로 Cargo를 통해 컴파일러에 플래그를 지정해요. 예를 들어 MSVC에서 정적으로 링크된 바이너리를 컴파일하려면 이렇게 해요.
RUSTFLAGS='-C target-feature=+crt-static' cargo build --target x86_64-pc-windows-msvc
Rust와 외부 코드의 혼합
Rust를 외부 코드(예: C, C++)와 섞어서 두 종류의 코드를 모두 담은 단일 바이너리를 만들고 싶다면, 최종 바이너리 링크에 두 가지 방법이 있어요.
rustc를 사용 — non-Rust 라이브러리는-L <directory>와-l<library>rustc 인자, 그리고/또는 Rust 코드의#[link]지시문으로 넘겨요..o파일에 링크해야 한다면-Clink-arg=file.o를 쓸 수 있어요.- 외부 링커를 사용 — 이 경우 먼저 Rust
staticlib타겟을 생성해서 외부 링커 호출에 넘겨요. 여러 Rust 하위 시스템을 링크해야 한다면, 여러rlib을 포함하기 위해extern crate문을 많이 써서 단일staticlib을 생성해야 해요. 여러 Ruststaticlib파일은 서로 충돌하기 쉽기 때문이에요.rlib을 직접 외부 링커에 넘기는 것은 현재 지원되지 않아요.
참고: 다른 인스턴스의 Rust 런타임으로 컴파일되거나 링크된 Rust 코드는 이 장의 목적상 "외부 코드"로 취급돼요.
금지된 링키지와 unwind
panic unwind는 바이너리가 다음 규칙에 따라 일관되게 빌드된 경우에만 쓸 수 있어요.
Rust 산출물은 다음 조건 중 하나라도 만족하면 잠재적으로 unwind하는(potentially unwinding) 이라고 불러요.
- 해당 산출물이
unwindpanic 핸들러를 사용한다. - 해당 산출물이
unwindpanic 전략으로 빌드된 crate를 포함하는데, 그 crate가-unwindABI를 사용하는 함수를 호출한다. - 해당 산출물이, 별도의 Rust 런타임 복사본을 가진 다른 Rust 산출물에서 실행되는 코드에
"Rust"ABI 호출을 하는데, 그 다른 산출물이 잠재적으로 unwind한다.
이 정의는 Rust 산출물 안의
"Rust"ABI 호출이 언제든 unwind할 수 있는지를 포착해요.
Rust 산출물이 잠재적으로 unwind한다면, 그 모든 crate는 unwind panic 전략으로 빌드되어야 해요. 그렇지 않으면 unwind가 정의되지 않은 동작을 일으킬 수 있어요.
rustc로 링크한다면 이 규칙은 자동으로 시행돼요.rustc로 링크하지 않는다면 바이너리 전체에서 unwind가 일관되게 처리되도록 주의해야 해요.rustc없이 링크하는 것에는dlopen이나 그와 유사한, 시스템 런타임이rustc없이 링킹하는 기능 사용이 포함돼요. 이는 보통 서로 다른-C panic플래그로 코드를 섞을 때만 일어나므로, 대부분의 사용자는 신경 쓸 필요가 없어요.
링크 시점에 어떤 panic 런타임이 쓰이든 라이브러리가 sound하도록(그리고
rustc로 링크 가능하도록) 보장하려면ffi_unwind_calls린트를 쓸 수 있어요. 이 린트는-unwind외부 함수나 함수 포인터에 대한 모든 호출을 표시해요.