크레이트와 소스 파일

크레이트와 소스 파일 (Crates and source files)

Rust 프로그램은 어떻게 하나의 실행 파일로 묶일까요? 답은 크레이트(crate) 라는 개념에 있어요. 이 글에서는 크레이트가 무엇이고, 소스 파일이 크레이트 안에서 어떻게 모듈로 연결되는지, 그리고 main 함수와 크레이트 수준 속성들이 어떤 역할을 하는지 살펴볼게요.

출처: Rust Reference

본문

구문 (Syntax)

Crate →
    InnerAttribute*
    Item*

참고: Rust는 다른 언어와 마찬가지로 인터프리터로도 구현될 수 있지만, 현재 존재하는 구현은 컴파일러뿐이고 언어 자체도 컴파일되도록 설계되어 왔어요. 그래서 이 절에서는 컴파일러를 전제로 설명해요.

컴파일 타임과 런타임 (Compile-time vs run-time)

Rust의 의미론은 컴파일 타임(compile-time)런타임(run-time) 사이에 단계 구분(phase distinction)을 두고 있어요. 정적 해석을 가지는 의미 규칙은 컴파일의 성공 여부를 좌우하고, 동적 해석을 가지는 의미 규칙은 프로그램이 런타임에 어떻게 행동하는지를 좌우하죠.

크레이트 (Crates)

컴파일 모델은 크레이트라고 부르는 산출물(artifact)을 중심으로 돌아가요. 각 컴파일은 소스 형태의 단 하나의 크레이트를 처리하고, 성공하면 바이너리 형태의 크레이트 하나 — 실행 파일이든 일종의 라이브러리든 — 를 만들어 내요.

크레이트는 컴파일과 링킹의 단위일 뿐 아니라 버전 관리, 배포, 런타임 로딩의 단위이기도 해요. 크레이트는 중첩된 모듈 스코프의 트리를 담고 있어요. 이 트리의 최상위는 (모듈 안의 경로 관점에서) 익명인 모듈이고, 크레이트 안의 어떤 아이템이든 그 크레이트의 모듈 트리 안에서의 위치를 나타내는 정규 모듈 경로(canonical module path) 를 가져요.

소스 파일 (Source files)

Rust 컴파일러는 항상 단일 소스 파일을 입력으로 받고, 항상 단일 출력 크레이트를 만들어요. 그 소스 파일을 처리하는 과정에서 다른 소스 파일이 모듈로 로드될 수도 있어요. 소스 파일의 확장자는 .rs예요.

Rust 소스 파일은 하나의 모듈을 서술하는데, 그 이름과 위치는 — 현재 크레이트의 모듈 트리 안에서 — 소스 파일 밖에서 정해져요: 참조하는 소스 파일의 명시적 Module 아이템에 의해서든, 아니면 크레이트 자신의 이름에 의해서든 말이죠.

모든 소스 파일은 하나의 모듈이지만, 모든 모듈이 자기만의 소스 파일을 필요로 하지는 않아요. 모듈 정의는 한 파일 안에 중첩될 수 있기 때문이에요.

각 소스 파일은 0개 이상의 Item 정의의 나열을 포함하고, 선택적으로 그 파일을 담고 있는 모듈에 적용되는 속성들로 시작할 수 있어요. 대부분 그 속성들은 컴파일러의 동작에 영향을 줘요.

익명 크레이트 모듈은 크레이트 전체에 적용되는 추가 속성들을 가질 수 있어요.

참고: 파일의 내용은 셔뱅(shebang)이 앞에 올 수 있어요.

#![allow(unused)]
fn main() {
// Specify the crate name.
#![crate_name = "projx"]

// Specify the type of output artifact.
#![crate_type = "lib"]

// Turn on a warning.
// This can be done in any module, not just the anonymous crate module.
#![warn(non_camel_case_types)]
}

main 함수 (Main functions)

main 함수를 포함하는 크레이트는 실행 파일로 컴파일될 수 있어요.

main 함수가 있다면, 그것은 인자를 받지 않아야 하고, 트레이트나 수명 바운드를 선언하지 않아야 하며, where 절도 없어야 하고, 반환 타입은 Termination 트레이트를 구현해야 해요.

fn main() {}
fn main() -> ! {
    std::process::exit(0);
}
fn main() -> impl std::process::Termination {
    std::process::ExitCode::SUCCESS
}

main 함수는 import로 가져온 것일 수도 있어요. 외부 크레이트에서든 현재 크레이트에서든 말이죠.

#![allow(unused)]
fn main() {
mod foo {
    pub fn bar() {
        println!("Hello, world!");
    }
}
use foo::bar as main;
}

참고: 표준 라이브러리에서 Termination을 구현한 타입들로는 (), !, Infallible, ExitCode, 그리고 Result<T, E>(여기서 T: Termination, E: Debug)가 있어요.

잡히지 않은 외부 언와인딩 (Uncaught foreign unwinding)

"외부(foreign)" 언와인딩 — 예를 들어 C++ 코드에서 던져진 예외나, 다른 패닉 핸들러를 쓰는 Rust 코드의 panic! — 이 main 함수를 넘어 전파되면, 프로세스는 안전하게 종료돼요. 이는 abort의 형태를 띨 수 있는데, 그 경우 어떤 Drop 호출이 실행된다는 보장이 없고, 에러 출력도 "네이티브" Rust 패닉으로 런타임이 종료됐을 때보다 정보가 적을 수 있어요.

자세한 내용은 panic 문서를 참고하세요.

no_main 속성 (The no_main attribute)

no_main 속성은 크레이트 수준에서 적용해서 실행 파일 바이너리에 대한 main 심볼 생성을 끌 수 있어요. 링크되는 다른 객체가 main을 정의하는 경우에 유용해요.

crate_name 속성 (The crate_name attribute)

crate_name 속성은 크레이트 수준에서 적용해서 크레이트 이름을 MetaNameValueStr 구문으로 지정할 수 있어요.

#![allow(unused)]
#![crate_name = "mycrate"]
fn main() {
}

크레이트 이름은 비어 있으면 안 되고, 유니코드 영숫자 또는 _(U+005F) 문자만 포함해야 해요.

더 알아보기 (Learn more)