はじめに
Rust のマクロ、何が展開されてるかわからなくて困ったことありませんか?
#[derive(Debug)]
struct User {
name: String,
}
この Debug 、一体何が生成されてるの?
cargo-expand を使えば、マクロの展開結果が見られます。
目次
インストール
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」が使えます。
- マクロ呼び出しにカーソルを置く
-
Ctrl+Shift+P→ "Expand macro recursively" - 展開結果が表示される
IntelliJ / RustRover
同様に「Expand declarative macro」機能があります。
まとめ
cargo-expand の使いどころ
- derive マクロの理解 - 何が生成されてるか見る
- マクロのデバッグ - 展開結果を確認
- 学習 - 標準マクロの仕組みを理解
- 最適化 - 生成コードのサイズ確認
コマンドまとめ
# インストール
cargo install cargo-expand
# 全体を展開
cargo expand
# 特定モジュール
cargo expand module_name
# ライブラリクレート
cargo expand --lib
# カラーなし(コピペ用)
cargo expand --color never
マクロは黒魔術じゃない。cargo-expand で中身を見れば、怖くなくなります!
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!