
2026/06/01 4:28
ゼロから Rust の手続き型マクロ (Procedural Macro) を構築する
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
この章では、struct に対して bit フラグ用メソッドを自動的に生成する Rust プロシージャルマクロ
bitfields の実装方法を説明します。マクロは実行時変数ではなくソースコード上でコンパイル時に動作し、TokenStream 入力を TokenStream 出力にマッピングする関数として定義されます。これらは Cargo.toml に proc-macro = true とマークされた独立したクレート内に配置する必要があります。Rust ではプロシージャルマクロとして #[proc_macro]、#[proc_macro_derive]、および #[proc_macro_attribute] の 3 種類が用意されています。ソースコードを操作するには、入力文法解析器(AST, Abstract Syntax Tree)に変換する syn クレートと、その AST から新しい Rust トークンを生成する quote! マクロを使用します。属性は Meta::List や Meta::NameValue などの構造体として解析されます。マクロは通常の関数では実現できないコンパイル時の挙動を実行できます(例:break 文の挿入)。文法解析にはカスタムキーワード(例:dont_shift)を syn::custom_keyword! を用いて、または「フォーク」などのエラー回復技術を用いて行うことができます。フラグ用のビットマスク生成は、(1 << n) - 1 や u8::MAX >> (u8::BITS - n) のようなビット演算に依存します。生成されたマクロは struct のフィールドに基づいて、ゲッター(get_*/is_*)、セッター(set_*)、クリア関数、コンストラクタメソッドを提供します。マクロ展開時のエラー処理では compile_error! を用いて実行時パニックを回避することが推奨されます。このアプローチは論理を実行時変数からコンパイル時コード操作へシフトさせ、フラグ管理のための boilerplate を削減するとともに、プロジェクト間で標準化された一貫した安全性チェックを実現します。本文
LearnixOS 書籍:Bitflags マクロの実装ガイド
キーボードショートカット
| キー | 動作 |
|---|---|
| ← / → | 章間の移動 |
| S / / | 書籍の検索 |
| ? | ヘルプ表示 |
| Esc | ヘルプを隠す |
Bitflags マクロの導入
プロシージャルマクロとは?
- 定義: 「入力から出力へどのように置換するか」を指定する規則またはパターン。
- 関数同様、入力を出力にマップしますが、ソースコードそのものに作用します。
- Rust の proc-macro 特徴:
- 初期のコード(
)を操作可能にする。TokenStream - ソースコードをトークン化し、
ノードのシーケンスとして扱う。TokenTree
- 初期のコード(
基本的な実装例
#[proc_macro] pub fn custom_proc_macro(input: TokenStream) -> TokenStream { eprintln!("{:?}", input); input // 同じものを返す(多くの場合は異なるコードを生成する) }
トークンストリーム (TokenStream
) とは?
TokenStream- 文字列ではなくトークン単位で処理することで、初期のコードレベルでの操作が可能。
ノードのシーケンスを含みます。TokenTree
: ブレースなどのデリミター付きストリームGroup
: 識別子(例:変数名)Ident
: 句読点文字(例:Punct
,+
),
: リテラル文字、文字列、数値Literal
デバッグ出力の例
TokenStream [ Ident { ident: "struct", ... }, Ident { ident: "Example", ... }, Group { delimiter: Brace, stream: TokenStream [ Ident { ident: "a", ... }, Punct { ch: ':', ... }, Ident { ident: "i32", ... }, Comma, ], }, ]
マクロの動作原理(展開)
- コンパイル時に評価される関数。
- コンパイラ視点:ターゲット言語を別の言語へマップするのではなく、同じ言語内で置換。
例:宣言的マクロの展開
// 定義 macro_rules! square { ($num:expr) => { $num * $num }; } // 使用 fn foo() -> u32 { let x: u32 = 42; square!(x) // 実質的に:x * x }
関数ではできないこと:コンテキスト依存の制御流
- 宣言的マクロが実現できる抽象化(例:ループからの抜出)。
macro_rules! unwrap_or_break { ($e:expr) => { match $e { Some(v) => v, None => break, // コンパイル時に入れ子された文脈 (`break`) を理解可能 } }; } // 関数実装だとエラーになる:None -> break は外部のスコープ参照が必要 fn unwrap_or_break<T>(e: Option<T>) -> T { match e { Some(v) => v, None => panic!("Error"), // ここでは break を使用できない } }
マクロの種類
| 種類 | デコレータ | 特徴 | 対象 |
|---|---|---|---|
| 宣言的マクロ | なし | 規則セットで展開、文法拡張に用いる | 全スコープ |
| 関数型マクロ | | を受け取って返す(通常の関数同様) | グローバルスコープ |
| 誘導マクロ | | 項目に対して自動生成コード ()。トレイト実装などに使用。 | 構造体、列挙型など |
| 属性マクロ | | 項目の動作をカスタマイズ(設定パラメータを受け取る) | 構造体、関数など |
Syn と Quote の導入
背景と必要性
- プロシージャルマクロでは直接
を解析するのは複雑。TokenStream - Syn: Rust の構文ツリーを**AST(抽象構文木)**に変換する。
- Quote: データ (
) をコードのように見える形式に変換する。TokenStream
AST とは?
- プログラムの構文構造を表す木構造。
- 操作しやすいデータ構造にするために、簡略化された Python の例:
current = 0 for item in items: if item > current: current = item
Syn で利用可能な主要 AST タイプ
: 完全な Rust ソースファイル。syn::File
: 属性(例:syn::Attribute
)。#[derive]
: モジュール内にある項目(定数、列挙体、関数、構造体など)。syn::Item
: 式(割り当て文、スライスリテラルなど)。syn::Expr
重要: AST のフィールド順序はソースコードの順序と一致する。これにより解析後の再構築が容易になる。
Quote と ToTokens
- Quoting: コードのように見せるが裏ではデータ (
) として扱われる。TokenStream - ToTraits:
で使用可能なためのトレイト。quote!
// クオートされた式(実際には TokenStream に変換される) quote! { struct Foo { bar: () } } // 変数をトークンに変換する例 let mut item_fn = syn::parse_macro_input!(input as syn::ItemFn); item_fn.sig.ident = Ident::new("with_change_{}", ...); quote! { #item_fn }.into() // `#` で変数を挿入
マクロの設計:Bitfields
要件と仕様
数値をフラグとして表現する(例:
u8, u16)。各フラグに対し、以下を実装。
- Getter: フラグ値の取得 (
)。get_* - Setter: フラグ値の設定 (
)。set_*, new() - Clearer: フラグのリセット (`clear_*)。
機能拡張機能(Attribute)
生成される関数を制御するメタデータ:
- Permission:
(Read),r
(Write),w
(Clear value)。c - FlagType: ユーザー定義の列挙体などを指定 (
)。flag_type = MyEnum- デフォルトは幅に合わせた基本型(例:6 ビット ->
)。u8
- デフォルトは幅に合わせた基本型(例:6 ビット ->
- DontShift: 絶対値で扱いたい場合の設定 (
)。dont_shift - Helper Attributes: 生成されるコードのカスタマイズ。
#[bitfields] struct MyFlags { #[flag(r)] // リードオンリー、オフセット 0-2 a: B2, #[flag(rwc(10))] // レッド・ライト・クリア(値 10)、オフセット 3-7 b: B5, }
マクロの実装プロセス
設計思想:スモールステップで進める
実装を開始する前に、単純な例から機能を手作業で作成し、それを一般化すること。
1. 初期のビット演算ロジック
- マスキング: フラグ以外のビットをゼロにする。
- シフト:
- リード時:フラグ位置へ右シフト (
)。>> - ライト時:フラグ位置へ左シフト (
)。<<
- リード時:フラグ位置へ右シフト (
// マスク生成(全 1 から必要ない分を除去) let mask = (u8::MAX >> (u8::BITS - width as u32)) << offset; // リード値の取得 let val = (raw_value & mask) >> offset;
2. データ構造と定義
:FlagAttribute
,r
,w
,c
,flag_type
などを格納。dont_shift
: ビット幅 (FlagMeta
) と表現型 (width
) を格納。repr_ty: u8/u16/...
: 単一のフラグフィールドのメタ情報。BitField
3. 属性解析(Parse)
のsyn::Attribute
パートから情報を抽出。Meta- フォーク (
) を用いて、成功/失敗を問わずストリームの正確な位置に戻せるようにする。input.fork()
// 簡略化されたパーサーの概念実装 fn try_parse<T>(input: ParseStream, ..) -> Option<syn::Result<T>> { let fork = input.fork(); match fork.parse::<T>() { Ok(parsed) => { // 成功:元の位置に戻し、解析されたデータを返す Some(Ok(parsed)) }, Err(_) => { // 失敗:エラーカウンター増やし、何もしない(フォークを放棄) None } } }
4. 構造体の解析と変換
を解析し、フィールドごとに情報を抽出。syn::ItemStruct- コメント属性(
)と Flag 属性を分離。#[doc] - フィールドのオフセットを動的に計算し続ける。
impl<'a> BitField<'a> { pub fn new(field: &Field, offset: usize) -> syn::Result<Self> { // ... 属性解析、メタデータ抽出 ... Ok(BitField { attr, vis, name, meta, offset, .. }) } }
5. コード生成(Quote)
- チェック関数: リリースビルドでは無効だが、デバッグ用で値の範囲を確認。
- アクセサ生成:
,get_
,set_
を自動生成。clear_
型の場合、bool
に名前を変更する。is_*
- 安全な参照: 完全修飾名(例:
)を使用し、コンパイル時エラーを回避。::core::ptr::read_volatile
// 簡略化された生成ロジック impl<'a> BitFields<'a> { fn fn_read(&self, field: &BitField) -> TokenStream2 { // ... チェック処理、シフト処理 ... quote! { #[inline] pub fn get_#field_name(&self) -> #ty { unsafe { let val = self.0 as #repr_ty; let mask = ...; ((val & mask) >> offset) as #ty } } } } }
6. 補助機能の実装
Trait: 構造体と内部型 (From
など) の相互変換。u8
Trait: フラグ状態を可視化するためのフォーマッター。Debug- Builder Pattern:
のようなチェーン式設定の実装。flag1().flag2()
7. マクロ本体の統合
- 入力を受け取り、解析(またはエラー生成)を行う。
- 成功時はコードを返す。
#[proc_macro_attribute] pub fn bitfields(_attr: TokenStream, item: TokenStream) -> TokenStream { let struct_def = parse_quote!(struct MyStruct { fields: ... }); // BitFields::try_from(&struct_def).map(|...| quote!{#bitfields}) // .unwrap_or_else(|e| e.into_compile_error()) }
まとめ
- プロシージャルマクロは、コンパイル時にコードを生成・置換する強力な機能。
とsyn
を併用することで、複雑な構文ツリーの解析と再生成が容易になる。quote- ビット操作とメタデータ駆動のデザインパターンによって、柔軟かつ安全なマクロを実装できる。