Rust의 시작점 main 함수
≡ 목차 (Table of Contents)
C 언어의 시작점과 동일하게 Rust도 main
이라는 이름의 함수가 시작점이 된다. 이 main 함수과 관련된 내용을 정리해보자.
main 함수
프로그램이 시작되는 지점은 main 이라는 이름을 가진 함수다.
fn main() { ... }
말 그대로 메인 함수다. 독자 실행형 소프트웨어라면 반드시 이 함수가 시작점에 해당한다. 이 정도면 왠만한 경우에도 별 무리 없는 시작점으로 활용 가능한 함수다.
리턴 타입이 없다?
C 언어에 익숙하다면 main 함수는 리턴 타입을 가질 수 있다는 것을 알고 있을 것이다.
int main() { return 0; // Ok }
참고로 유닉스 혹은 유닉스에서 유래한 OS에서 명령의 실행 결과는 전통적으로 0이 성공을 의미한다.
그런데 특이하게도 Rust 가이드 글들의 main 함수는 하나같이 아래와 같은 모양만 보인다.
fn main() { ... }
리턴 타입도 없으므로 당연히 return
문도 사용하지 않는다.
사실 Rust의 main 함수는 별 다른 오류가 없었다면 성공한 것으로 간주하고 정상(0) 코드를 리턴한다. 그래서 아무 리턴이 없었더라도 셸 스크립트에서도 성공으로 간주하여 동작하는 것을 볼 수 있다.
아래는 &&
을 이용해 위의 리턴이 없는 main 함수를 가진 코드를 빌드한 someproj
라는 실행 파일로 시험한 예이다.
$ target/debug/someproj && echo "Ok" Ok
&&
은 앞의 명령이 성공하면 그 뒤의 명령을 실행시키는 셸 오퍼레이터다. 즉 someproj
명령이 성공(0)을 리턴했다는 말이다.
그렇다면 반대로 뭔가 런타임 에러가 발생되는 케이스는 어떻게 될까? 일부러 아래와 같은 코드를 작성해봤다.
use std::fs::File; fn main() { File::open("unknown_file.txt").expect("File not found"); }
없는 파일을 열려고 하니 에러가 발생할거다. 그래서 expect
가 이걸 붙잡고 오류를 내면서 종료시킬 것이다.
$ target/debug/someproj thread 'main' panicked at 'File not found: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
그리고 당연히 이 결과는 실패한 것으로 간주된다.
$ target/debug/someproj && echo "Ok" thread 'main' panicked at 'File not found: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace $ echo $? 101
Ok
가 표시되지 않았다. 그리고 실제로 리턴된 결과도 101이었다. 참고로 셸에서 위처럼 $?
를 이용해 마지막으로 실행시킨 커맨드의 실행 결과를 알 수 있다.
리턴 타입이 있을 수도 있다
위에서는 없다고 했는데 사실 main 함수는 리턴 타입을 가질 수 있다. 다만 특이하게도 유닉스 스럽지 않게 Result
타입을 리턴하도록 강제된다.
fn main() -> Result<(), i32> { // ... Ok(()) }
위의 코드는 성공으로 간주하고 끝난다. 유닉스 기반의 소프트웨어들은 모두 성공 시 0을 리턴하는 게 관습처럼 정해져있기 때문에 성공시에는 별 다른 값 없이 Ok(())
만을 리턴하면 알아서 0을 리턴하는 것으로 보인다.
$ target/debug/someproj && echo "Ok" Ok
예상대로 문제 없이 동작한다.
자 그렇다면 에러를 리턴해야 하는 상황은 어떨까? Result
타입을 사용하므로 에러일 때는 당연하게도 Err()
을 리턴하면 될 것이다.
fn main() -> Result<(), i32> { // ... Err(1) }
위 코드가 실행되면 Error: 1
이라는 것이 화면에 표시되면서 종료되는 특징이 있다. 어쨌든 이번에도 셸로 명령어 연결을 시도해보자.
$ target/debug/someproj && echo "Ok" Error: 1
이번에는 Ok
가 화면에 찍히지 않는다. 즉 someproj
가 에러를 리턴하고 종료되었다는 말이다. 아마도 제대로 이해한 것 같다.
셸에서 리턴 코드를 확인해보면 동일하게 1이 리턴된 것을 알 수 있다.
$ target/debug/someproj Error: 1 $ echo $? 1
Result
타입은 generic이기 때문에 리턴되는 타입 자체가 유동적이다. 아래와 같이 에러를 리턴하는 방식도 쓸 수 있다.
use std::io::Error; fn main() -> Result<(), std::io::Error> { // ... Err(Error::from_raw_os_error(1)) }
이 외에 모듈에서 정의된 다양한 에러타입도 사용되는 것 같다.
파라미터(Parameter or Arguments)
다시 C 언어와 비교해보자. C 언어 main 함수의 기본 형태는 대체로 아래와 같은 식으로 arguments 목록을 얻기 위한 매개변수를 정의한다.
int main(int argc, char *argv[]) { // ... }
하지만 Rust의 main 함수 예제에선 저런 파라미터 처리를 위한 매개변수를 전달받는 예제를 전혀 볼 수 있다.
이유는 단순하다. 파라미터(arguments) 전달 방식이 다르기 때문이다.
use std::env; fn main() { let args: Vec<String> = env::args().collect(); println!("{:?}", args); }
std::env::args
를 이용해 파라미터(arguments)를 받아올 수 있다. 실행시켜보면 아래와 같은 결과를 얻을 수 있다.
$ target/debug/someproj param1 param2 param3 ["target/debug/someproj", "param1", "param2", "param3"]
첫 파라미터는 자기자신이고, 나머지는 공백을 기준으로 나눠진 파라미터 리스트(Vec)로 얻을 수 있다. 다른 언어에서 다뤄봤다면 매우 익숙하게 느껴질 것이다.
참고로 저렇게 빌드해서 실행하는 게 귀찮다면 Cargo의 run
커맨드에서도 파라미터를 줄 수 있으니 멀리 돌아가지 않아도 된다.
$ cargo run param1 param2 param3 Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/someproj param1 param2 param3` ["target/debug/someproj", "param1", "param2", "param3"]
어쨌거나 파라미터를 벡터 타입으로 받아왔기 때문에 갯수나 각 파라미터를 얻는 과정이 어렵지는 않을 것이다.
use std::env; fn main() { let args: Vec<String> = env::args().collect(); println!("Total {} arguments", &args.len()); for param in &args { println!("{}", param); } }
굳이 실행 결과가 필요할지는 의문이지만 대충 이렇게 된다.
$ target/debug/someproj param1 param2 param3
Total 4 arguments
target/debug/someproj
param1
param2
param3
뭐 아마도 이걸 그대로 쓰기 보다는 별도의 옵션 처리를 도와줄 패키지를 이용하는 편이 더 많겠지만 기본적인 지식은 알아두는 편이 좋을 지도 모르겠다.