🪗

cargo-expand を使ってみよう

に公開

前回は thiserror crate の基本的な使い方について書きました 。今回は cargo-expand の使い方を確認しつつ、 thiserror crate の提供する derive macro の展開結果を見ていきます。

cargo-expand とは

cargo-expand は macro の展開結果を出力するコマンドです。

https://github.com/dtolnay/cargo-expand

インストール方法

cargo install cargo-expand

README によると rustfmt もあれば使うらしいです。 cargo が入っていて rustfmt が入っていない環境はそんなにないはず…… (知らない) 。

README の例

README の例をそのまま実行してみます。

ファイルを用意して。

#[derive(Debug)]
struct S;

fn main() {
    println!("{:?}", S);
}

cargo expand

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
struct S;
#[automatically_derived]
impl ::core::fmt::Debug for S {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::write_str(f, "S")
    }
}
fn main() {
    {
        ::std::io::_print(format_args!("{0:?}\n", S));
    };
}

ちょっと README とは違う結果ですけど、エディションの違いなどによるものでしょう。

大切なのは #[derive(Debug)]println! の展開された結果が表示されていることが分かります。

その他のオプション

cargo expand --help してください。省略。

前回の記事の例

さて、本題。前回の thiserror の記事の例に対して cargo expand して展開した結果を見てみましょう。

前回の記事の例 1

まずはこれ。

#[derive(Debug, thiserror::Error)]
#[error("unit struct error")]
struct UnitStructError;

cargo expand すると……

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
#[error("unit struct error")]
struct UnitStructError;
#[automatically_derived]
impl ::core::fmt::Debug for UnitStructError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::write_str(f, "UnitStructError")
    }
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::thiserror::__private::Error for UnitStructError {}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::core::fmt::Display for UnitStructError {
    #[allow(clippy::used_underscore_binding)]
    fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        #[allow(unused_variables, deprecated)]
        let Self {} = self;
        __formatter.write_str("unit struct error")
    }
}

#[derive(...)] はなくなるものの #[error("...")] は消えないみたいですね。

#[derive(...)] の展開結果は #[automatically_derived] が付くんですかね。

まずは Debug trait の実装。これは割愛します。また Debug trait のこともそのうち書くと思うので、そのときにでも。

次は impl ::thiserror::__private::Error for TupleStructError ですね。 ::thiserror::__private::Error ってなんだよって話ですけど、 https://github.com/dtolnay/thiserror/blob/7a5fbc65c03e39eed9a3fd2cd48e4313178734ed/src/lib.rs#L299 という thiserror のソースコードを見れば std::error::Error を re-export したものだと分かります。中身は空でデフォルト実装を使うだけのものですね。

そして Display trait の実装。 formatterwrite_str#[error("...")] で指定した内容を書いているだけですね。

期待通りの出力ですね。

前回の記事の例 2

次はどうでしょう。

#[derive(Debug, thiserror::Error)]
#[error("tuple struct error: {0}")]
struct TupleStructError(#[from] std::io::Error);

cargo expand すると……

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
#[error("tuple struct error: {0}")]
struct TupleStructError(#[from] std::io::Error);
#[automatically_derived]
impl ::core::fmt::Debug for TupleStructError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_tuple_field1_finish(
            f,
            "TupleStructError",
            &&self.0,
        )
    }
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::thiserror::__private::Error for TupleStructError {
    fn source(
        &self,
    ) -> ::core::option::Option<&(dyn ::thiserror::__private::Error + 'static)> {
        use ::thiserror::__private::AsDynError as _;
        ::core::option::Option::Some(self.0.as_dyn_error())
    }
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::core::fmt::Display for TupleStructError {
    #[allow(clippy::used_underscore_binding)]
    fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        use ::thiserror::__private::AsDisplay as _;
        #[allow(unused_variables, deprecated)]
        let Self(_0) = self;
        match (_0.as_display(),) {
            (__display0,) => {
                __formatter
                    .write_fmt(format_args!("tuple struct error: {0}", __display0))
            }
        }
    }
}
#[allow(
    deprecated,
    unused_qualifications,
    clippy::elidable_lifetime_names,
    clippy::needless_lifetimes,
)]
#[automatically_derived]
impl ::core::convert::From<std::io::Error> for TupleStructError {
    fn from(source: std::io::Error) -> Self {
        TupleStructError { 0: source }
    }
}

#[from]#[error("...")] と同様に残っています。

Debug trait の実装は割愛。

Error trait の実装。 #[from]#[source] を兼ねることからきちんと source メソッドを実装していますね。 source の実装では self.0::thiserror::__private::AsDynError として .as_dyn_error() を呼び、それを返していますね。名前や型から dyn Error の参照を得るものですね。

Display trait の実装。 self.0::thiserror::__private::AsDisplay として .as_display() を呼び、 #[error("...")] で指定した文字列の形式に .write_fmt(format_args!(...), ...) していますね。

そして From trait の実装。 #[from] で指定した型から from できるようにしてくれていますね。

いいですね。

前回の記事の例 3

どんどん行きましょう。次はこれです。

#[derive(Debug, thiserror::Error)]
#[error("struct error: {cause:?}")]
struct StructError {
    #[source]
    cause: std::io::Error,
}

cargo expand すると……

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
#[error("struct error: {cause:?}")]
struct StructError {
    #[source]
    cause: std::io::Error,
}
#[automatically_derived]
impl ::core::fmt::Debug for StructError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field1_finish(
            f,
            "StructError",
            "cause",
            &&self.cause,
        )
    }
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::thiserror::__private::Error for StructError {
    fn source(
        &self,
    ) -> ::core::option::Option<&(dyn ::thiserror::__private::Error + 'static)> {
        use ::thiserror::__private::AsDynError as _;
        ::core::option::Option::Some(self.cause.as_dyn_error())
    }
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::core::fmt::Display for StructError {
    #[allow(clippy::used_underscore_binding)]
    fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        #[allow(unused_variables, deprecated)]
        let Self { cause } = self;
        match (cause,) {
            (__field_cause,) => {
                __formatter.write_fmt(format_args!("struct error: {0:?}", __field_cause))
            }
        }
    }
}

Debug trait の実装は割愛。

Error trait の実装は例 2 と変わらず。 #[source] がついているから .source() が実装されていますね。

Display trait の実装は format が {0:?} になったくらいですね。フィールドの展開などは thiserror の実装がいろいろやってくれているんでしょうか (きちんとソースコードは追っていないので想像ですが) 。展開後のコードは素朴ですね。

#[from] がないので From trait の実装はなく、ややスッキリしていますね。

前回の記事の例 4

最後の例ですね。

#[derive(Debug, thiserror::Error)]
enum EnumError {
    #[error("io error")]
    Io(#[source] std::io::Error),
    #[error(transparent)]
    From(#[from] Box<dyn std::error::Error + Send + Sync>),
    #[error(transparent)]
    Unknown(Box<dyn std::error::Error + Send + Sync>),
}

cargo expand すると……

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
enum EnumError {
    #[error("io error")]
    Io(#[source] std::io::Error),
    #[error(transparent)]
    From(#[from] Box<dyn std::error::Error + Send + Sync>),
    #[error(transparent)]
    Unknown(Box<dyn std::error::Error + Send + Sync>),
}
#[automatically_derived]
impl ::core::fmt::Debug for EnumError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            EnumError::Io(__self_0) => {
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Io", &__self_0)
            }
            EnumError::From(__self_0) => {
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "From", &__self_0)
            }
            EnumError::Unknown(__self_0) => {
                ::core::fmt::Formatter::debug_tuple_field1_finish(
                    f,
                    "Unknown",
                    &__self_0,
                )
            }
        }
    }
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::thiserror::__private::Error for EnumError {
    fn source(
        &self,
    ) -> ::core::option::Option<&(dyn ::thiserror::__private::Error + 'static)> {
        use ::thiserror::__private::AsDynError as _;
        #[allow(deprecated)]
        match self {
            EnumError::Io { 0: source, .. } => {
                ::core::option::Option::Some(source.as_dyn_error())
            }
            EnumError::From { 0: transparent } => {
                ::thiserror::__private::Error::source(transparent.as_dyn_error())
            }
            EnumError::Unknown { 0: transparent } => {
                ::thiserror::__private::Error::source(transparent.as_dyn_error())
            }
        }
    }
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::core::fmt::Display for EnumError {
    fn fmt(&self, __formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        #[allow(unused_variables, deprecated, clippy::used_underscore_binding)]
        match self {
            EnumError::Io(_0) => __formatter.write_str("io error"),
            EnumError::From(_0) => ::core::fmt::Display::fmt(_0, __formatter),
            EnumError::Unknown(_0) => ::core::fmt::Display::fmt(_0, __formatter),
        }
    }
}
#[allow(
    deprecated,
    unused_qualifications,
    clippy::elidable_lifetime_names,
    clippy::needless_lifetimes,
)]
#[automatically_derived]
impl ::core::convert::From<Box<dyn std::error::Error + Send + Sync>> for EnumError {
    fn from(source: Box<dyn std::error::Error + Send + Sync>) -> Self {
        EnumError::From { 0: source }
    }
}

Debug trait の実装。割愛。

Error trait の実装。 match で variant ごとに分けていますね。 #[transparent] については Some(source.as_dyn_error()) とせず Error::source(transparent.as_dyn_error()) として、中身の Error に丸投げしていますね。確かに透過的です。

Display trait の実装。こちらも match で variant ごとに分けていますね。こちらも #[transparent] について Display::fmt(_0, __formatter) として丸投げしていますね。うんうん。

From trait の実装。 #[from] をつけたのでこれもありますね。 #[from] を付けた EnumError::From variant がきちんと指定されています。

もう見慣れてきましたね。

おわり

cargo-expand の紹介と、それを使って前回の thiserror の記事の例の macro を展開してみました。

macro は何が起きるかパッと見では分かりづらいですが、よく使うものくらいは展開された中身を見てみる・理解してみると面白いかもしれませんね。

GitHubで編集を提案
ドクターメイト

Discussion