コンパイラプラグイン

イントロダクション

rustc はコンパイラプラグイン、ユーザの提供する構文拡張や構文チェックなどのコンパイラの振舞を拡張するライブラリをロード出来ます。

プラグインとは rustc に拡張を登録するための、指定された 登録用 関数を持った動的ライブラリのクレートです。 他のクレートはこれらのプラグインを #![plugin(...)] クレートアトリビュートでロード出来ます。 プラグインの定義、ロードの仕組みについて詳しくはrustc_pluginを参照して下さい。

#![plugin(foo(... args ...))] のように渡された引数があるなら、それらはrustc自身によっては解釈されません。 Registryargs メソッドを通じてプラグインに渡されます。

ほとんどの場合で、プラグインは #![plugin] を通じて のみ 使われるべきで、 extern crate を通じて使われるべきではありません。 プラグインをリンクするとlibsyntaxとlibrustcの全てをクレートの依存に引き込んでしまいます。 これは別のプラグインを作っているのでもない限り一般的には望まぬ挙動です。 plugin_as_library チェッカによってこのガイドラインは検査されます。

普通の慣行ではコンパイラプラグインはそれ専用のクレートに置かれて、 macro_rules! マクロやコンシューマが使うライブラリのコードとは分けられます。

構文拡張

プラグインはRustの構文を様々な方法で拡張出来ます。構文拡張の1つに手続的マクロがあります。 これらは普通のマクロと同じように実行されますが展開は任意の構文木をコンパイル時に操作するRustのコードが行います。

ローマ数字リテラルを実装するroman_numerals.rsを書いてみましょう。

fn main() { #![crate_type="dylib"] #![feature(plugin_registrar, rustc_private)] extern crate syntax; extern crate rustc; extern crate rustc_plugin; use syntax::codemap::Span; use syntax::parse::token; use syntax::ast::TokenTree; use syntax::ext::base::{ExtCtxt, MacResult, DummyResult, MacEager}; // use syntax::ext::build::AstBuilder; // trait for expr_usize use syntax::ext::build::AstBuilder; // expr_usizeのトレイト use rustc_plugin::Registry; fn expand_rn(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'static> { static NUMERALS: &'static [(&'static str, usize)] = &[ ("M", 1000), ("CM", 900), ("D", 500), ("CD", 400), ("C", 100), ("XC", 90), ("L", 50), ("XL", 40), ("X", 10), ("IX", 9), ("V", 5), ("IV", 4), ("I", 1)]; if args.len() != 1 { cx.span_err( sp, &format!("argument should be a single identifier, but got {} arguments", args.len())); return DummyResult::any(sp); } let text = match args[0] { TokenTree::Token(_, token::Ident(s, _)) => s.to_string(), _ => { cx.span_err(sp, "argument should be a single identifier"); return DummyResult::any(sp); } }; let mut text = &*text; let mut total = 0; while !text.is_empty() { match NUMERALS.iter().find(|&&(rn, _)| text.starts_with(rn)) { Some(&(rn, val)) => { total += val; text = &text[rn.len()..]; } None => { cx.span_err(sp, "invalid Roman numeral"); return DummyResult::any(sp); } } } MacEager::expr(cx.expr_usize(sp, total)) } #[plugin_registrar] pub fn plugin_registrar(reg: &mut Registry) { reg.register_macro("rn", expand_rn); } }
#![crate_type="dylib"]
#![feature(plugin_registrar, rustc_private)]

extern crate syntax;
extern crate rustc;
extern crate rustc_plugin;

use syntax::codemap::Span;
use syntax::parse::token;
use syntax::ast::TokenTree;
use syntax::ext::base::{ExtCtxt, MacResult, DummyResult, MacEager};
use syntax::ext::build::AstBuilder;  // expr_usizeのトレイト
use rustc_plugin::Registry;

fn expand_rn(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree])
        -> Box<MacResult + 'static> {

    static NUMERALS: &'static [(&'static str, usize)] = &[
        ("M", 1000), ("CM", 900), ("D", 500), ("CD", 400),
        ("C",  100), ("XC",  90), ("L",  50), ("XL",  40),
        ("X",   10), ("IX",   9), ("V",   5), ("IV",   4),
        ("I",    1)];

    if args.len() != 1 {
        cx.span_err(
            sp,
            &format!("argument should be a single identifier, but got {} arguments", args.len()));
        return DummyResult::any(sp);
    }

    let text = match args[0] {
        TokenTree::Token(_, token::Ident(s, _)) => s.to_string(),
        _ => {
            cx.span_err(sp, "argument should be a single identifier");
            return DummyResult::any(sp);
        }
    };

    let mut text = &*text;
    let mut total = 0;
    while !text.is_empty() {
        match NUMERALS.iter().find(|&&(rn, _)| text.starts_with(rn)) {
            Some(&(rn, val)) => {
                total += val;
                text = &text[rn.len()..];
            }
            None => {
                cx.span_err(sp, "invalid Roman numeral");
                return DummyResult::any(sp);
            }
        }
    }

    MacEager::expr(cx.expr_usize(sp, total))
}

#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
    reg.register_macro("rn", expand_rn);
}

rn!() マクロを他の任意のマクロと同じように使えます。

#![feature(plugin)] #![plugin(roman_numerals)] fn main() { assert_eq!(rn!(MMXV), 2015); }
#![feature(plugin)]
#![plugin(roman_numerals)]

fn main() {
    assert_eq!(rn!(MMXV), 2015);
}

単純な fn(&str) -> u32 に対する利点は

手続き的マクロに加えてderiveライクなアトリビュートや他の拡張を書けます。 Registry::register_syntax_extensionSyntaxExtension 列挙型を参照して下さい。 もっと複雑なマクロの例はregex_macrosを参照して下さい。

ヒントと小技

マクロデバッグのヒントのいくつかが使えます。

syntax::parseを使うことでトークン木を式などの高レベルな構文要素に変換出来ます。

fn main() { fn expand_foo(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult+'static> { let mut parser = cx.new_parser_from_tts(args); let expr: P<Expr> = parser.parse_expr(); }
fn expand_foo(cx: &mut ExtCtxt, sp: Span, args: &[TokenTree])
        -> Box<MacResult+'static> {

    let mut parser = cx.new_parser_from_tts(args);

    let expr: P<Expr> = parser.parse_expr();

libsyntax のパーサのコードを見るとパーサの基盤がどのように機能しているかを感られるでしょう。

パースしたもののSpanは良いエラー報告のために保持しておきましょう。 自分で作ったデータ構造に対してもSpannedでラップ出来ます。

ExtCtxt::span_fatalを呼ぶとコンパイルは即座に中断されます。 ExtCtxt::span_errを呼んでDummyResultを返せばコンパイラはさらなるエラーを発見できるのでその方が良いでしょう。

構文の断片を表示するにはspan_notesyntax::print::pprust::*_to_stringを使えば出来ます。

上記の例ではAstBuilder::expr_usizeを使って整数リテラルを作りました。 AstBuilder トレイトの代替として libsyntax準クォート マクロを提供しています。 ドキュメントがない上に荒削りです。しかしながらその実装は改良版の普通のプラグインライブラリのとっかかりにはほど良いでしょう。

構文チェックプラグイン

プラグインによってRustの構文チェック基盤を拡張してコーディングスタイル、安全性などを検査するようにできます。ではlint_plugin_test.rsプラグインを書いてみましょう。 lintme という名前のアイテムについて警告を出すものです。

#![feature(plugin_registrar)] #![feature(box_syntax, rustc_private)] fn main() { extern crate syntax; // Load rustc as a plugin to get macros // macroを使うためにrustcをプラグインとして読み込む #[macro_use] extern crate rustc; extern crate rustc_plugin; use rustc::lint::{EarlyContext, LintContext, LintPass, EarlyLintPass, EarlyLintPassObject, LintArray}; use rustc_plugin::Registry; use syntax::ast; declare_lint!(TEST_LINT, Warn, "Warn about items named 'lintme'"); struct Pass; impl LintPass for Pass { fn get_lints(&self) -> LintArray { lint_array!(TEST_LINT) } } impl EarlyLintPass for Pass { fn check_item(&mut self, cx: &EarlyContext, it: &ast::Item) { if it.ident.name.as_str() == "lintme" { cx.span_lint(TEST_LINT, it.span, "item is named 'lintme'"); } } } #[plugin_registrar] pub fn plugin_registrar(reg: &mut Registry) { reg.register_early_lint_pass(box Pass as EarlyLintPassObject); } }
#![feature(plugin_registrar)]
#![feature(box_syntax, rustc_private)]

extern crate syntax;

// macroを使うためにrustcをプラグインとして読み込む
#[macro_use]
extern crate rustc;
extern crate rustc_plugin;

use rustc::lint::{EarlyContext, LintContext, LintPass, EarlyLintPass,
                  EarlyLintPassObject, LintArray};
use rustc_plugin::Registry;
use syntax::ast;

declare_lint!(TEST_LINT, Warn, "Warn about items named 'lintme'");

struct Pass;

impl LintPass for Pass {
    fn get_lints(&self) -> LintArray {
        lint_array!(TEST_LINT)
    }
}

impl EarlyLintPass for Pass {
    fn check_item(&mut self, cx: &EarlyContext, it: &ast::Item) {
        if it.ident.name.as_str() == "lintme" {
            cx.span_lint(TEST_LINT, it.span, "item is named 'lintme'");
        }
    }
}

#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
    reg.register_early_lint_pass(box Pass as EarlyLintPassObject);
}

そしたらこのようなコードは

fn main() { #![plugin(lint_plugin_test)] fn lintme() { } }
#![plugin(lint_plugin_test)]

fn lintme() { }

コンパイラの警告を発生させます。

foo.rs:4:1: 4:16 warning: item is named 'lintme', #[warn(test_lint)] on by default
foo.rs:4 fn lintme() { }
         ^~~~~~~~~~~~~~~

構文チェックプラグインのコンポーネントは

構文チェックパスは構文巡回ですが、型情報が得られる、コンパイルの終盤で走ります。 rustc組み込み構文チェックは殆どプラグインと同じ基盤を使っており、どうやって型情報にアクセスするかの例になっています。

プラグインによって定義されたLintは普通のアトリビュートとコンパイラフラグ例えば #[allow(test_lint)]-A test-lint によってコントロールされます。 これらの識別子は declare_lint! の第一引数に由来しており、適切な名前に変換されます。

rustc -W help foo.rs を走らせることで rustc の知っている、及び foo.rs 内で定義されたコンパイラ構文チェックをロード出来ます。