Rust - 편리한 매크로 소개

출처

Rust의 표준 매크로는 하나의 파일에 모두 정리하고 있다.
문서에서 볼 수 있다.
https://doc.rust-lang.org/src/std/macros.rs.html
일부 매크로는 컴파일러 매직이기도 하고, 보고있는 것만으로도 꽤 재미있다.

Rust의 편리한 매크로 목록

compile_error!

panic!은 런타임 오류를 내지만,이것은 컴파일 시에 에러를 낼 수 있는 매크로이다.
컴파일시 코드에 compile_error!(어떤 문자열);이 포함 되어 있으면 즉시 컴파일이 실패한다.

macro_rules! foo {
    () => {
        compile_error!("式を渡してね!");
    };
    ($e:expr) => {};
}

foo!();

이것을 실행하면 foo!(); 부분이 compile_error!( "식을 건네주세요!");로 대체 되고, 컴파일 에러가 된다.

오류 내용은 이런 느낌

error : 식을 건네주세요!
  -> src \ hoge.rs : 13 : 9
   |
13 | compile_error! ( "식을 건네주세요!");
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
18 | foo! ();
   | ------- in this macro invocation

env!

env!는 환경 변수 등의 값을 취득하기 위한 매크로이다.
런타임이 아니라 컴파일 시에 값이 대체 된다는 것에 주의해야 한다.
형식은 문자열 리터럴이다.

사용법

env!( "PATH" ); 
env!( "SystemRoot" );

Cargo가 정의하는 환경 변수도 얻을 수 있다.
여기에서 어떤 값이 있는지 쓰고 있다.
https://doc.rust-lang.org/cargo/reference/environment-variables.html
예를 들면 cargo.exe의 위치를 알고 싶을 때는 env!("CARGO") 라고 하면
~/cargo.exe 같은 전체 경로를 얻을 수 있다.
Cargo에 정의되어 있는 환경 변수는 자작 라이브러리 buildscript를 쓸 때 편리하다.

option_env!

조금 전의 env!에는 조금 문제가 있는데, env!( "존재하지 않는 환경 변수 이름") 처럼 없는 환경 변수를 지정하면 컴파일 에러가 된다.
만약 환경 변수가 없다면 이쪽의 처리로 분기한다 라는 방법이 없다
그래서 option_env! 이다.
이것은 Option<&'static str> 형식으로 값을 반환한다.
즉, 환경 변수를 찾지 않아도 컴파일 에러가 되지 않고 Option::None을 반환한다.

concat_idents!

이것은 식별자를 결합한다.
식별자는 것이 포인트이다! 문자열을 결합하는 것은 아니다!

공식에서 가져온 예이다.

fn foobar() -> u32 { 23 }

let f = concat_idents!(foo, bar);
println!("{}", f());

concat_idents!(foo, bar)는 컴파일시 foobar로 대체한다.
concat_idents!(foo, bar, baz)처럼 여러게 연결도 가능하다.

concat!

이것은 컴파일 시에 전달된 값을 결합하여 문자열로 대체하는 매크로이다.

// foo10truebar로 대체! 
concat! ( "foo" ,  10 ,  true ,  "bar" );

문자열 이외에도 결합 대상으로 할 수 있다.
리터럴이라면 기본적으로 결합 할 수 있다.

line!

이 매크로는 열으면 매크로가 위치한 줄 번호로 대체된다.
형식은 부호없는 정수 리터럴이다.

fn  main () { 
  let  hoge:u32 = line!();  // hoge = 2

  let  huga:u32 = line!();  // huga = 4 
}

column!

이것은 앞의 line!의 열 번호 버전이다

file!

이것은 이 매크로가 배치 되어 있는 파일의 경로를 문자열 리터럴로 대체한다.
전체 경로가 아닌 프로젝트에서 상대 경로이다.

stringify!

이 매크로는 어떤 무언가를 그대로 문자열로 만들어 버리는 것이다.

stringify!(1+1);     // "1+1"
stringify!(foobar);  // "foobar"
stringify!(file!()); // "file ! (  )"

// "fn main (  ) { println ! ( "{}" , 1 + 2 ) }"
stringify!(fn main(){println!("{}",1+2 )})

include_str!

컴파일 시 파일에 적혀 있는 문자열로 대체한다.
사전에 적당히 파일을 준비한다.

hoge.txt

abcd
efgh
매크로 최고!

그리고 이렇게 된다

const s: &str = include_str!("hoge.txt");

fn main() {
    println!("{}", s);
}

이제 hoge.txt가 출력된다.

include_bytes!

include_str!의 bytes 버전이다.

module_path!

현재 위치하고 있는 모듈까지의 모듈 경로를 컴파일 시에 문자열로 전개한다.

mod foo{
    pub mod bar{
        pub fn baz(){
            println!(module_path!());    
        }
    }
}

fn main() {
    foo::bar::baz();
}

/*  project_name::foo::bar 가 표시된다*/

include!

include_str!는 파일의 내용을 문자열 리터럴로 전개하지만, 이것은 이대로 전개다. C 언어 include와 비슷하다.

hello.rs

println!( "hello!" )
fn  main() { 
  include!( "hello.rs" ); 
}

컴파일 하면 include!( "hello.rs")println!( "hello!") 로 대체되고, 실행하면 hello!가 출력된다.

try!

일단 튜토리얼의 URL을 보자.
http://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/book/error-handling.html
try!는 Result의 내용을 추출을 unwrap 보다 안전하게, 패턴 매치를 쓰지 않고 간결하게 할 수 있다.

예를 보인 것이 빠를 것이다.

fn foo() -> Result<i32, ()> {
    Result::Ok(777)
}

fn bar() -> Result<i32, ()> {
    let num = try!{foo()};
    println!("{}", num);
    Ok(num)
}

fn main() {
    bar();
}

/* 777가 표시된다 */

이렇게 하여 try!를 사용하면 내용물을 쉽게 꺼낼 수 있다.
unwrap와 다른 점은 값이 Err 라고해도 panic!이 되지 않고 return 되는 것이다.

아마 bar 함수의 try! 부분은 이렇게 바뀐다

fn bar() -> Result<i32, ()> {
    let num = match foo() {
        Ok(x) => x,
        Err(x) => return Err(x),
    };
    println!("{}", num);
    Ok(num)
}

? 연산자

바로 앞에 try! 매크로를 소개했지만 이것을 단순화한 연산자가 ? 연산자이다.
이 연산자는 try!의 신텍스슈거 구문으로 식을 괄호로 감싸는 번거로움을 없앨 수 있다.

try!에서 보여준 bar 함수를 ? 연산자로 고쳐 쓴 예

fn bar() -> Result<i32, ()> {
    let num = foo()?;
    println!("{}", num);
    Ok(num)
}

이 글은 2020-04-02에 작성되었습니다.