レッスン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 つ
- Solidity 構文パース
- 各関数の セレクタ(keccak256 先頭 4 バイト)計算
- ABI エンコード / デコード impl 生成
- 型付きラッパ 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 種類のマクロを即答できる。
synとquoteの役割を 1 文で言える。sol!がコンパイル時にやる 4 つを即答できる。address!が宣言的 + 内側hex!が手続き型、と区別できる。cargo expandを実行できる。
まとめ(3行)
- 手続きマクロ =
TokenStream → TokenStreamの関数、synで AST に +quoteで生成。コンパイル時に走り、実行時コストゼロ。 sol!は Solidity 構文 → セレクタ計算 + ABI エンコード impl 生成、address!は宣言的 + 内側でhex!手続きマクロ呼び出し。- 詰まったら
cargo expandで展開コードを目視。proc macro は「コンパイル時バリデーション」「DSL」「定型圧縮」が成立する規模だけ書く。