FABRKNT
Reth Expert — 本番エンジニアリング
パフォーマンス & システム
レッスン 4 / 25·CONTENT15 分35 XP
コース
Reth Expert — 本番エンジニアリング
レッスンの役割
CONTENT
順序
4 / 25

レッスン3 — 手続きマクロ(sol!address! の中身)

問い

address!("0xabc...") は関数呼び出しに見えるが コンパイル時に走るsol! { contract IERC20 { ... } } も同じ。hex のパースはいつ起きるか?コンパイラのどのツールが担うか?無効な hex を書いたら何が起きるか?

原理(最小モデル)

  • 3 種類のマクロ. 関数風(address!sol!)/ derive(#[derive(Serialize)])/ 属性(#[tokio::main])。すべて crate-type = ["proc-macro"] のクレートに住み、TokenStream → TokenStream の関数として動く。
  • syn + quote の 2 クレート. syn が TokenStream を AST にパース、quote がテンプレートから TokenStream を生成。
  • address! は宣言的マクロ(macro_rules!). 本物の手続きマクロは内側で呼ばれる hex! の方。
  • $d:tt のトリック. マクロ内でマクロを生成するとき内側マクロの $ をハイジーン回避するため。
  • sol! がコンパイル時にやること. Solidity 風構文をパース → セレクタ(keccak256)計算 → ABI エンコード / デコード impl 生成。実行時コストゼロ。
  • cargo expand で展開を見る. 詰まったら必ず使う。

具体例

ツールチェイン:

flowchart LR
    Src["ソースコード<br/>sol! マクロ"] -->|コンパイラがマクロ呼び出し| In[入力 TokenStream]
    In -->|syn::parse| AST[Rust / DSL AST]
    AST -->|あなたのロジック| Tree[生成された AST]
    Tree -->|quote!| Out[出力 TokenStream]
    Out -->|コンパイラ続行| Compiled[コンパイル後のバイナリ]

本物の address! ソース(crates/primitives/src/bits/macros.rs):

macro_rules! fixed_bytes_macros {
    ($d:tt $($(#[$attr:meta])* macro $name:ident($ty:ident $($rest:tt)*);)*) => {$(
        $(#[$attr])*
        #[macro_export]
        macro_rules! $name {
            () => {
                $crate::$ty::ZERO
            };

            ($d ($d t:tt)+) => {
                $crate::$ty::new($crate::hex!($d ($d t)+))
            };
        }
    )*};
}

fixed_bytes_macros! { $
    macro address(Address);
    macro b64(B64);
    macro b128(B128);
    macro b256(B256);
    macro b512(B512);
    macro bloom(Bloom);
    macro fixed_bytes(FixedBytes<0>);
}

sol! の使用例:

sol! {
    interface IERC20 {
        function balanceOf(address owner) external view returns (uint256);
        event Transfer(address indexed from, address indexed to, uint256 value);
    }
}

let balance = IERC20::new(token, &provider).balanceOf(owner).call().await?;

const としての address:

const BURN: Address = address!();  // Address::ZERO、const で評価

cargo expand:

cargo install cargo-expand
cargo expand --bin my_app

失敗例(誤解)

address! は手続きマクロ」— 間違いaddress!macro_rules! 宣言的マクロ。内側で呼ばれる hex! が本物の手続きマクロ(hex パース + バイト数チェック + [u8; N] 配列リテラル生成)。

balanceOf の keccak は初回呼び出し時に計算」— 間違いsol! がコンパイル時にセレクタ(keccak256("balanceOf(address)") の先頭 4 バイト = 0x70a08231)を計算してコードに埋め込む。実行時コストゼロ。

「マクロでマクロを生成するとき内側の $ を直接書ける」— 間違い。外側のメタ変数として食われてシンタックスエラー。$d:tt$ にバインドして $d ($d t:tt)+ を生成コードに書く(マクロハイジーン回避)。

🛑 予測。 address!("0xZZZ") を書いたらどんなエラーが出る?(答え: コンパイルエラー(実行時パニックではない)。hex! 手続きマクロがコンパイル時に hex digit 検証 + バイト数(Address なら 20)チェック。Z は hex digit でないのでコンパイル時に失敗。デプロイ前にバグが捕まる。)

ステップで組み立てる

Step 1: 3 種類を即答

関数風 / derive / 属性。すべて crate-type = ["proc-macro"] クレートに住む。

Step 2: 2 クレートの役割

syn → TokenStream を AST に。quote → AST から TokenStream 生成。

Step 3: sol! がコンパイル時にする 4 つ

  1. Solidity 構文パース
  2. 各関数の セレクタ(keccak256 先頭 4 バイト)計算
  3. ABI エンコード / デコード impl 生成
  4. 型付きラッパ struct 生成

実行時はリフレクションなし、文字列パースなし。

Step 4: いつ proc macro を書くか

  • 何度も同じ定型コードが圧縮できる
  • コンパイル時バリデーション(address のパースなど)
  • DSL 級の使い心地に値する規模

「1 回しか書かない 5 行を浮かせる」ためには書かない。

Step 5: デバッグ

詰まったら cargo expand でマクロ生成コードを目視。

答え合わせ

  • hex パースが起きる場所: コンパイル時、hex! 手続きマクロが syn で文字列リテラルを受け取り検証して [u8; N] を生成。
  • $d:tt トリックの理由: 外側マクロ展開時に内側マクロの $ がメタ変数として食われるのを防ぐ。$d$ にバインドすることで「外側にとってのトークン、内側にとってのメタ変数記号」を区別。
  • マクロを定義するマクロのメリット: 1 行で 7 つの型付きマクロ(address! / b64! / b128! / b256! / b512! / bloom! / fixed_bytes!)を作成、メタパターン 1 回書いて 7 倍の便利マクロを得る。

合格基準

  • 3 種類のマクロを即答できる。
  • synquote の役割を 1 文で言える。
  • sol! がコンパイル時にやる 4 つを即答できる。
  • address! が宣言的 + 内側 hex! が手続き型、と区別できる。
  • cargo expand を実行できる。

まとめ(3行)

  • 手続きマクロ = TokenStream → TokenStream の関数、syn で AST に + quote で生成。コンパイル時に走り、実行時コストゼロ。
  • sol! は Solidity 構文 → セレクタ計算 + ABI エンコード impl 生成、address! は宣言的 + 内側で hex! 手続きマクロ呼び出し。
  • 詰まったら cargo expand で展開コードを目視。proc macro は「コンパイル時バリデーション」「DSL」「定型圧縮」が成立する規模だけ書く。