4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

Rust のマクロ、何が展開されてるかわからなくて困ったことありませんか?

#[derive(Debug)]
struct User {
    name: String,
}

この Debug 、一体何が生成されてるの?

cargo-expand を使えば、マクロの展開結果が見られます。

目次

  1. インストール
  2. 基本的な使い方
  3. 実例:derive マクロ
  4. 実例:手続き型マクロ
  5. デバッグに活用
  6. まとめ

インストール

cargo install cargo-expand

Nightly toolchain も必要です:

rustup install nightly

基本的な使い方

プロジェクト全体を展開

cargo expand

特定のモジュールだけ

cargo expand module_name

特定のアイテムだけ

cargo expand --item ItemName

ライブラリクレートを展開

cargo expand --lib

バイナリを展開

cargo expand --bin binary_name

実例:derive マクロ

Debug

// src/main.rs
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 1, y: 2 };
    println!("{:?}", p);
}
cargo expand

展開結果:

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;

struct Point {
    x: i32,
    y: i32,
}

#[automatically_derived]
impl ::core::fmt::Debug for Point {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(
            f,
            "Point",
            "x",
            &self.x,
            "y",
            &&self.y,
        )
    }
}

fn main() {
    let p = Point { x: 1, y: 2 };
    {
        ::std::io::_print(format_args!("{0:?}\n", p));
    };
}

見えた! Debug トレイトが実装されてる。

Clone と Copy

#[derive(Clone, Copy)]
struct Color(u8, u8, u8);

展開結果:

struct Color(u8, u8, u8);

#[automatically_derived]
impl ::core::clone::Clone for Color {
    #[inline]
    fn clone(&self) -> Color {
        let _: ::core::clone::AssertParamIsClone<u8>;
        *self
    }
}

#[automatically_derived]
impl ::core::marker::Copy for Color {}

Copy はマーカートレイトなので中身が空、Clone*self で自身をコピーしてる。

PartialEq と Eq

#[derive(PartialEq, Eq)]
struct Id(u64);

展開結果:

struct Id(u64);

#[automatically_derived]
impl ::core::cmp::PartialEq for Id {
    #[inline]
    fn eq(&self, other: &Id) -> bool {
        self.0 == other.0
    }
}

#[automatically_derived]
impl ::core::cmp::Eq for Id {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_receiver_is_total_eq(&self) -> () {
        let _: ::core::cmp::AssertParamIsEq<u64>;
    }
}

フィールドを1つずつ比較するコードが生成されてる!

Default

#[derive(Default)]
struct Config {
    timeout: u64,
    retries: u8,
    enabled: bool,
}

展開結果:

struct Config {
    timeout: u64,
    retries: u8,
    enabled: bool,
}

#[automatically_derived]
impl ::core::default::Default for Config {
    #[inline]
    fn default() -> Config {
        Config {
            timeout: ::core::default::Default::default(),
            retries: ::core::default::Default::default(),
            enabled: ::core::default::Default::default(),
        }
    }
}

各フィールドの Default::default() を呼んでる。

実例:宣言マクロ

vec! マクロ

fn main() {
    let v = vec![1, 2, 3];
}

展開結果:

fn main() {
    let v = <[_]>::into_vec(
        #[rustc_box]
        ::alloc::boxed::Box::new([1, 2, 3]),
    );
}

配列を Box で包んで Vec に変換してる!

println! マクロ

fn main() {
    let name = "Alice";
    println!("Hello, {}!", name);
}

展開結果:

fn main() {
    let name = "Alice";
    {
        ::std::io::_print(format_args!("Hello, {0}!\n", name));
    };
}

format_args! が使われてる。これはコンパイル時に処理されるので、さらに展開はされない。

自作マクロ

macro_rules! say_hello {
    ($name:expr) => {
        println!("Hello, {}!", $name);
    };
}

fn main() {
    say_hello!("World");
}

展開結果:

fn main() {
    {
        ::std::io::_print(format_args!("Hello, {0}!\n", "World"));
    };
}

マクロが完全に展開されてる!

実例:手続き型マクロ

serde

[dependencies]
serde = { version = "1", features = ["derive"] }
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct User {
    name: String,
    age: u32,
}
cargo expand

展開結果(長いので抜粋):

impl serde::Serialize for User {
    fn serialize<__S>(
        &self,
        __serializer: __S,
    ) -> serde::__private::Result<__S::Ok, __S::Error>
    where
        __S: serde::Serializer,
    {
        let mut __serde_state = serde::Serializer::serialize_struct(
            __serializer,
            "User",
            2usize,
        )?;
        serde::ser::SerializeStruct::serialize_field(
            &mut __serde_state,
            "name",
            &self.name,
        )?;
        serde::ser::SerializeStruct::serialize_field(
            &mut __serde_state,
            "age",
            &self.age,
        )?;
        serde::ser::SerializeStruct::end(__serde_state)
    }
}

serde がどうやってシリアライズしてるか丸見え!

thiserror

use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    
    #[error("Parse error at line {line}")]
    Parse { line: usize },
}

展開結果(抜粋):

impl std::fmt::Display for MyError {
    fn fmt(&self, __formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            MyError::Io(_0) => {
                __formatter.write_fmt(format_args!("IO error: {0}", _0))
            }
            MyError::Parse { line } => {
                __formatter.write_fmt(format_args!("Parse error at line {0}", line))
            }
        }
    }
}

impl std::error::Error for MyError {
    fn source(&self) -> std::option::Option<&(dyn std::error::Error + 'static)> {
        match self {
            MyError::Io(_0) => std::option::Option::Some(_0),
            MyError::Parse { .. } => std::option::Option::None,
        }
    }
}

impl std::convert::From<std::io::Error> for MyError {
    fn from(source: std::io::Error) -> Self {
        MyError::Io(source)
    }
}

#[error]#[from] の意味がわかる!

デバッグに活用

マクロがコンパイルエラーになるとき

macro_rules! weird_macro {
    ($($x:expr),*) => {
        $($x +)* 0  // 末尾の + でエラー
    };
}

fn main() {
    let _ = weird_macro!(1, 2, 3);
}

エラーメッセージがわかりにくい...

cargo expand

展開結果:

fn main() {
    let _ = 1 + 2 + 3 + 0;
}

あれ、実は正しく動いてた!(この例では)

期待通りに展開されてるか確認

macro_rules! create_function {
    ($name:ident) => {
        fn $name() {
            println!("Function {} called", stringify!($name));
        }
    };
}

create_function!(foo);
create_function!(bar);

展開結果:

fn foo() {
    {
        ::std::io::_print(format_args!("Function {0} called\n", "foo"));
    };
}
fn bar() {
    {
        ::std::io::_print(format_args!("Function {0} called\n", "bar"));
    };
}

期待通り2つの関数が生成されてる!

便利なオプション

カラー出力を無効化

cargo expand --color never

テーマを変更

cargo expand --theme=GitHub
cargo expand --theme=Monokai\ Extended

特定の feature を有効に

cargo expand --features my_feature

リリースビルドで展開

cargo expand --release

IDE との連携

VS Code

rust-analyzer を使っていれば、コード上で「Expand macro recursively」が使えます。

  1. マクロ呼び出しにカーソルを置く
  2. Ctrl+Shift+P → "Expand macro recursively"
  3. 展開結果が表示される

IntelliJ / RustRover

同様に「Expand declarative macro」機能があります。

まとめ

cargo-expand の使いどころ

  1. derive マクロの理解 - 何が生成されてるか見る
  2. マクロのデバッグ - 展開結果を確認
  3. 学習 - 標準マクロの仕組みを理解
  4. 最適化 - 生成コードのサイズ確認

コマンドまとめ

# インストール
cargo install cargo-expand

# 全体を展開
cargo expand

# 特定モジュール
cargo expand module_name

# ライブラリクレート
cargo expand --lib

# カラーなし(コピペ用)
cargo expand --color never

マクロは黒魔術じゃない。cargo-expand で中身を見れば、怖くなくなります!

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?