Rustでコンパイル時処理に挑戦する

この記事は Rust Language Advent Calendar 2014 の21日目の記事です。 20日目はRyuseiさんの「Go, Rust, Haxeによる正規表現エンジン実装の読み比べ - M59のブログ」、22日目はwoxtuさん担当です。

Rustのコンパイル時処理

システムプログラム界隈でコンパイル時処理というと、C++constexpr やDのCTFE (Compile Time Function Execution; コンパイル時関数実行) が有名です。Rustでもこれらと類似の機能をサポートしようという話は何度も持ち上がっていますが、残念ながら現時点ではサポートする予定はないようです (どこかでpost1.0の機能として上がっているのを見た気がしますが、URL失念。)

Rust 1.0 にCTFEはありませんが、コンパイル時処理を行うことができる "compiler plugin" という(実験的)機能が存在します。"compiler plugin" 機能は、C++/Dのコンパイル時処理とはだいぶ異なった使い勝手になっています。詳しくは The Rust Compiler Plugins Guide を参照してください。

デモ

さて、そんなRustのコンパイル時処理ですが、何ができるのでしょうか。簡単なプログラムを作ってみました。

https://github.com/gifnksm/cttw-rs

使い方を説明します。

まず、適当なディレクトリで cargo new を実行します。

$ cargo new --bin hello
$ cd hello

Cargo.tomlに以下を書き足します。

[dependencies.cttw]
git = "https://github.com/gifnksm/cttw-rs"

また、自動的に生成されているsrc/main.rsの先頭に以下を追加します。

#![feature(phase)]
#[phase(plugin)] extern crate cttw;

これで準備は完了です。コンパイルしましょう。

$ cargo build
   Compiling pkg-config v0.1.2
   Compiling gcc v0.1.2
   Compiling url v0.2.6
   Compiling curl-sys v0.1.2
   Compiling libz-sys v0.1.0
   Compiling openssl-sys v0.2.6
   Compiling time v0.1.3
   Compiling curl v0.1.2
   Compiling rust-crypto v0.2.2 (https://github.com/DaGenix/rust-crypto#2a2220b8)
   Compiling cttw v0.0.1 (https://github.com/gifnksm/cttw-rs#41848736)
   Compiling hello v0.0.1 (file:///home/nksm/tmp/tmp/hello)
input your consumer key:

なにやら見慣れないプロンプトが登場します。ここでは、Twitterのアプリケーション向けconsumer key を入力するよう指示されています。Twitterのサイトで適当なアプリケーションを作成し、割り振られたconsumer keyを入力してください。

input your consumer secret:

続けてconsumer secretを要求されるので、入力してください。

open the following url:
        https://api.twitter.com/oauth/authorize?oauth_token=XXXXX
input pin:

ここまで来たら後一歩。表示されたURLにアクセスし、Twitterアカウントでログインおよび認証後、表示されるPINコードを入力してください。 その後、何事も無かったかのようにコンパイルが終わります。Twitterを見てみると、、、

コンパイルするだけでツイートされている!!!!!!!!!

なお、2度目のコンパイルからは認証用情報の入力は不要です (カレントディレクトリに情報を保存します)。

種明かし

compiler plugin は、コンパイル時にユーザが定義した関数を呼び出すことでrustcの機能を拡張します。これらのユーザ定義関数は、動的ライブラリ内で定義され、rustcはコンパイル時にライブラリをリンクします。リンクされる側のライブラリは普通のRustコードですので、任意の処理を書くことができます。

今回作ったライブラリでは、以下のように、プラグインを登録する処理を行う関数内にツイートする処理をぶち込むことで簡単にコンパイル時処理を実現してしまいました。

#[plugin_registrar]
pub fn plugin_registrar(_reg: &mut Registry) {
    let conf = match config::read() {
        Some(c) => c,
        None => {
            let consumer_key    = console_input("input your consumer key:");
            let consumer_secret = console_input("input your consumer secret:");
            let consumer = Token::new(consumer_key, consumer_secret);

            let request = twitter::get_request_token(&consumer);
            println!("open the following url:");
            println!("\t{}", twitter::get_authorize_url(&request));
            let pin = console_input("input pin:");
            let access = twitter::get_access_token(&consumer, &request, pin.as_slice());

            let c = Config {
                consumer_key: consumer.key.to_string(),
                consumer_secret: consumer.secret.to_string(),
                access_key: access.key.to_string(),
                access_secret: access.secret.to_string()
            };
            config::write(&c);
            c
        }
    };

    let consumer = Token::new(conf.consumer_key, conf.consumer_secret);
    let access = Token::new(conf.access_key, conf.access_secret);
    twitter::tweet(&consumer, &access, "あなたとRust, 今すぐコンパイル");
}

まじめなはなし

今回作ったのはしょぼいネタライブラリでしたが、"compiler plugin"機能を活用した便利ライブラリももちろん開発されています。

少し検索しただけでこれだけのライブラリが出てきたので、他にもいろいろあるのでしょう。 外部の静的解析ツールに頼ること無く独自のlintを追加できるのは、compiler pluginの強みだと思います。

かなしいはなし

本記事で紹介してきた compiler plugin 機能ですが、利用するために "#![feature(...)]" が必要な実験的機能となっています。rustcは今後リリースチャンネルモデルに移行するようですが、実験的機能はbetaやreleaseのチャンネルでは無効化されてしまうため、利用するためにはnightlyチャンネルを利用する必要があります。一般のrustユーザまでcompiler pluginが届くのは、まだまだ先になりそうです。。。

謝辞

今回、compiler pluginを作る箇所は簡単に作れたのですが、Twitterに投稿する処理の作成で非常に苦労しました。明日の記事担当のwoxtuさんのdlang - 市民、認証は義務です - Qiitaが大変参考になりました。ありがとうございます。