Power Assert を Rust で作ってみた

素敵な表示をしてくれるassertionライブラリ、Power Assert を Rust でも実現してみました!!!

Assertion failure 時にこんな感じに分かりやすくメッセージ出力してくれます。使い方は GitHub参照ください。

$ cargo run --example test[master]
   Compiling power-assert v0.1.0 (file:///home/nksm/repos/gifnksm/power-assert-rs)
     Running `target/debug/examples/test`
power_assert!(bar.val == bar.foo.val)
              |   |   |  |   |   |
              |   3   |  |   |   2
              |       |  |   Foo { val: 2 }
              |       |  Bar { val: 3, foo: Foo { val: 2 } }
              |       false
              Bar { val: 3, foo: Foo { val: 2 } }
thread '<main>' panicked at 'assertion failed: bar.val == bar.foo.val', examples/test.rs:26
An unknown error occurred

To learn more, run the command again with --verbose.

とりあえず動くところまでいったという段階でバグはいっぱいあると思います。使ってみて気づいた点があれば Issue を立てるか、 PullReq いただけると大変うれしいです。

技術的な話

Rust のコンパイラプラグインを利用して、power_assert!() マクロに与えられた引数の AST を変換して、式中に登場する値を表示できるようにしています。 例えば、

power_assert!(bar.val == bar.foo.val);

という式は、以下のように変換されます。

let mut vals = vec!();
let cond =
    {
        let expr =
            {
                vals.push((0i32, format!("{:?}" , bar)));
                vals.push((4i32, format!("{:?}" , bar.val)));
                bar.val
            } ==
                {
                    vals.push((11i32, format!("{:?}" , bar)));
                    vals.push((15i32, format!("{:?}" , bar.foo)));
                    vals.push((19i32, format!("{:?}" , bar.foo.val)));
                    bar.foo.val
                };
        vals.push((8i32, format!("{:?}" , expr)));
        expr
    };
if !cond {
    use std::io::{self, Write};
    fn width(s: &str) -> i32 { s.len() as i32 }
    fn align<T: Write>(writer: &mut T, cur: &mut i32, col: i32, s: &str) {
        while *cur < col { let _ = write!(writer , " "); *cur += 1; }
        let _ = write!(writer , "{}" , s);
        *cur += width(s);
    }
    {
        vals.sort();
        let mut err = io::stderr();
        let _ =
            writeln!(err , "{}{}{}" , "power_assert!(" ,
                     "bar.val == bar.foo.val" , ")");
        {
            let mut cur = 0;
            for &(c, _) in &vals {
                align(&mut err, &mut cur, 14i32 + c, "|");
            }
            let _ = writeln!(err , "");
        }
        while !vals.is_empty() {
            let mut cur = 0;
            let mut i = 0;
            while i < vals.len() {
                if i == vals.len() - 1 ||
                       vals[i].0 + width(&vals[i].1) < vals[i + 1].0 {
                    align(&mut err, &mut cur, 14i32 + vals[i].0,
                          &vals[i].1);
                    let _ = vals.remove(i);
                } else {
                    align(&mut err, &mut cur, 14i32 + vals[i].0, "|");
                    i += 1;
                }
            }
            let _ = writeln!(err , "");
        }
    }
    panic!(concat ! ( "assertion failed: " , "bar.val == bar.foo.val" ))
}

いろいろと汚いことやっていますが、変換処理自体はツリーを再帰的に見ていくなどしているだけでシンプルです。syntax::fold::Folder のデフォルト実装を利用することで、AST を渡り歩くこと自体は結構簡単にできる感じです。とはいえ、AST 関連は全然ドキュメントが整備されていないので、慣れるまではなかなか思った通りのコードを書けませんでしたが。。。

ランタイムの依存関係が不要になるように、メッセージ表示のための処理が個々の assert に対して一つずつ定義されるようになっており、コード量は結構多くなっています。plugin から crate の root に何かを追加することが可能ならば解決できるのですが、やり方が分かりませんでした。

なお、AST 変換で実現している関係上、Debug が実装されていない値に対して format!("{:?}", ...) を実行してしまいコンパイルが通らなくなってしまう場合があります。これは仕組み上どうしようもないかもしれませんが、なんとか改善したいところではあります。

rustで変なテトリスを書いた

Rust1.0リリースパーティーで、絶対にPistonを使ってみるぞ!!!と決心してから早3ヶ月、やっとPistonを触ってみました。

チュートリアル

RustリリースパーティーでLTをされていた、http://cakecatz.hatenadiary.com/entry/2015/05/17/221004 さんの記事を参考に、Pistonのチュートリアルから触ってみました、が、バージョンを明に指定せずに (=最新版のpiston関連ライブラリを使って) 実行しようとすると、チュートリアルの記法のままでは動かず、またライブラリの構成自体も大きく変わってしまっているようだったため、piston_window というユーティリティ (?) ライブラリを使ってみました。piston_window を使うと、チュートリアルのソースコード とほとんど同じような使い方ができました。以下は■を高速で回転させるプログラムです。実行するとちょっと楽しい。

extern crate piston_window;

use piston_window::{PistonWindow, WindowSettings, UpdateEvent, RenderEvent, Transformed, rectangle};

const GREEN: [f32; 4]  = [0.5, 1.0, 0.5, 1.0];
const PURPLE: [f32; 4] = [0.5, 0.5, 1.0, 1.0];

fn main() {
    let window: PistonWindow = WindowSettings::new("spinning-square", [200, 200])
        .exit_on_esc(true)
        .build()
        .unwrap();

    let mut rot = 0.0;

    for e in window {
        if let Some(r) = e.render_args() {
            let square = rectangle::square(-25.0, -25.0, 50.0);

            e.draw_2d(|c, g| {
                piston_window::clear(GREEN, g);

                let (x, y) = ((r.draw_width / 4) as f64,
                              (r.draw_height / 4) as f64);

                let transform = c.transform
                    .trans(x, y)
                    .rot_rad(rot);

                rectangle(PURPLE, square, transform, g);
            });
        }

        if let Some(u) = e.update_args() {
            rot += 20.0 * u.dt;
        }
    }
}

テトリス

よっしゃ、Rustでグラフィカルなプログラミングいけそうだぞ!!!という気持ちになったので、お手軽な練習プログラムとして割とよくやられがちなテトリスを作ってみました。ただし、先の cakecatz さんのLTでもテトリスを作られていたので、ちょっとひねりをいれて、普通のテトリスではなく一人称視点のテトリスを作成してみました。

通常のテトリスでは、固定されたステージに対して上からテトリミノ (ブロック) が落ちてきます。一人称テトリスでは、テトリミノは動かず、ステージが動くように見えます。操作方法は以下の通り。お暇があれば、是非上のGitHubのリンクからダウンロードしてプレイしてみてください!

  • , w, k: テトリミノから見て前方に移動
  • , s, j: テトリミノから見て後方に移動
  • , d, l: 右方向に旋回
  • , a, h: 左方向に旋回
  • space: 重力方向への落下を加速

一人称テトリスにも piston_window を使っています。pistonだから難しいとかそういうことは特になく、逆にRustのenumが使えたりしてうれしい面の方が多かったかなぁと思います。もう少し複雑なことをやろうとするといろいろと難しいことがあるかもしれませんが、今のところ不満は特にないですね。

なお、一人称テトリスには先行研究があります。こっちの方がグラフィックは凝っていて良い感じです。ここまでできるようになると、更に楽しくなりそうですね。

rustでtelnetチャットを書いた

ソケットプログラミングの練習でよくあるアレをrustで書いてみた。

https://github.com/gifnksm/telnet-chat

クライアントプログラム書くのがダルかったので、telnet を使う仕様にした。 \ から始まるコマンドを打つことで、参加者一覧を出したりできる。 練習のため、通信関係の処理はすべて標準ライブラリのものを利用している。 雑に設計して雑に書いたので無駄なスレッド生成とかいっぱいしているけどそれはご愛敬。 もうちょっと改善したいけど連休も終わるのでひとまず投稿した次第です。

  • Rust だからといって、並列プログラミングが超お手軽に書けるわけではない。異常系処理をきちんと作り込んむためにはスレッド間のやりとりなどはきちんとした設計が必要。
  • とはいえCで書くよりは断然楽。enum とパターンマッチ最高という思いを強くした。型定義だけでプロトコルが定義できるのは分かりやすい。C APIを叩く準備などの細かい処理に意識をとらわれず、本質的な論理に集中できるというのもやはり良い。move semantics や borrow checker のおかげでスレッド間のデータの排他についてそれほど気を遣わなくて良いのも大きい。
  • 一番ハマったのは、socket 全然関係無く docopt の使い方。 let arg: Args = Args::docopt()...: Args を書き忘れたばっかりに、decode 処理で実行時エラーになっていた。どうも型指定省略すると args の型が () になるっぽい。謎。
  • socket とか channel が Rust のライブラリで抽象化されているため、C で書いたときのようにどんな場合にどんなエラーが返ってくるのか man で調べつつ書く、というのができなくてエラー処理をどうすれば良いのか (リカバリ可否の判断とか) が判断できない局面があった。クロスプラットフォーム対応のために難しいのかもしれないけど、もう少しドキュメントに詳細なエラー原因とかがあると良いと思った。

ちゃんとしたサーバアプリケーションを書くならば、mio とか使うのがよさそう。

gunosyから元記事に自動的に飛ぶユーザースクリプト

Gunosyから届くメールのリンクのうちいくつかが元記事ではなくGunosy内に転載された記事に飛ぶようになっていて()腹が立ったので書いたので広報する。

ページ内から「元記事を探す」のリンクを探してきて、location.hrefに設定するだけです。

multirust で今日から始める快適 rust 生活

Rust 1.0 リリースおめでとうございます!!!!

Rust 1.0 がリリースされました!!!!めでたい!!!!!!

このリリースは非常に大きな一歩です。これまではコンパイラのバージョンが上がる度にコードを頑張って修正する必要がありましたが、今後はstablerustc を使って作ったコードについては、将来にわたってもコンパイル可能で動作することが保証されます。 (機能の deprecation などはありますので、絶対的なものではありませんが、破壊的変更の頻度・度合いはこれまでよりもだいぶ控えめになるはずで、バージョンアップへの追従を諦めてしまうこと、いわゆる "Rust 疲れ" 現象は減ることが期待できます)

現時点での Rust を取り巻く環境の問題

言語の安定化により、サードパーティーのライブラリは今後どんどん充実していくと予想されます。Rust の将来はバラ色になるに違いないと筆者は信じていますが、1.0がリリースされたばかりの現時点ではまだまだ理想の状態には到達していないのが現実です。例えば、本日実施された Rust 1.0 Release記念祝賀LT会 では、多くの方が以下について困っていると訴えていました。

  • 使いたいライブラリが unstable な機能を使っているため、stable なコンパイラではコンパイルできない
  • 使いたいライブラリがしばらく更新されておらず、最新の rustc + cargo ではコンパイルできない

これらを回避するためには、nightly/beta バージョンのコンパイラと stable バージョンのコンパイラを、プロジェクトごとに切り替えて利用する必要があります。もしかしたら、特定の日付の nightly build を利用する必要が出てきてしまうかもしれません。非常にめんどくさい作業ですね。

今すぐに Rust を活用したい、そんなあなたのためのツールmultirust です。

multirust のススメ

multirust は、一言で言ってしまえば rubyrvmpythonpyenv のようなもので、複数バージョン (リリースチャンネル) の Rust ツールチェイン (rustc & cargo) を簡単に切り替えて使えるようにするものです。

Rust の開発環境を整える方法としては、公式サイトに記載されている rustup.sh を利用する方法 が有名ですが、multirust を使う方法をオススメします。 (Rust のコアチームの一員の Steve Klabnik さんも 非Windows環境のユーザーについては multirust の利用を推奨しています)

multirust のインストール

以下コマンドをコピペするだけで multirust のインストールは可能です (2015/5/16 現在。実際にインストールする際は README を読んで最新の手順を確認してください)。

curl -sf https://raw.githubusercontent.com/brson/multirust/master/blastoff.sh | sh

上記を実行すると、multirust コマンドと nightly版の rustccargo がインストールされます (既に rustc などがインストールされていると、衝突するかも?)。

multirust の使い方

multirust のインストール直後の状態では、 rustc/cargo コマンドを実行すると、nightly 版の各コマンドが実行されます。

あるプロジェクトについて、stable 版の rustc/cargo を利用したい場合、以下を実行します。

cd <path to project>
multirust override stable

こうすることで、当該ディレクトリのパスとコンパイラバージョンの対応関係が記録され、当該ディレクトリで実行する rustc/cargo については、 stable 版が利用されるようになります (beta 版が使いたい場合は、 multirust override beta とする)。

また、特定の日付の nightly 版を利用することもできます。

cd <path to project>
multirust oberride nightly-2015-05-16

nightly 版のツールチェインについては日々の更新に追従する必要があるかもしれません。デフォルトでは multirust をインストールした時点で最新の nightly が利用されますが、最新の nightly を利用するようにするためには、以下を実行して保存されているツールチェインを更新します。

multirust update nightly

毎日の nightly 更新作業が便利になりますね!

なお、チャンネル名を指定せず multirust update する場合、nightly, beta, release の3チャンネル全てのコンパイラが更新されるそうです。

まとめ

multirust 便利!!!

詳しい話は multirust の README によくまとまっているので読みましょう。

Rust nightly の x86_64, i686 両対応版の PKGBUILD

タイトルの通りのブツです。これを参考にして rust-bin-nightly を改造した感じです。 パッケージのビルド時間が短くなって大変ハッピー。

# Maintainer: NAKASHIMA, Makoto <makoto.nksm@gmail.com>
pkgname=rust-nightly-bin
pkgver=2015.01.25
pkgrel=1
arch=('i686' 'x86_64')
pkgdesc='A safe, concurrent, practical language from Mozilla.'
url='http://www.rust-lang.org/'
provides=('rust' 'cargo')
conflicts=('rust' 'rust-git' 'rust-nightly' 'cargo-nightly-bin')
depends=('shared-mime-info')
license=('MIT' 'Apache')
source=('https://raw.githubusercontent.com/rust-lang/rust/master/src/etc/gedit/share/mime/packages/rust.xml'
        'rust-nightly.conf')
sha256sums=('a2a6609d6a89dd57f1bf874da222316c2b3c8ffcfe5b5ae87f8489096744446d'
            'fc2ed32e9841d2080803b117a773aa6606fc74391fec76fbadd2e952ca1fc256')
install=rust.install
options=(staticlibs !strip)

_target_os=unknown-linux-gnu
_nightly_x86_64="https://static.rust-lang.org/dist/rust-nightly-x86_64-$_target_os.tar.gz"
_nightly_i686="https://static.rust-lang.org/dist/rust-nightly-i686-$_target_os.tar.gz"

pkgver() {
  date --utc -d"$(curl -I $_nightly_x86_64 | sed -n 's/^Last-Modified: *//p')" +"%Y.%m.%d"
}

package() {
  local INSTALL_DIR=/usr/local
  local MAN_DIR=/usr/local/man

  curl -O $_nightly_x86_64
  curl -O $_nightly_i686
  tar xf rust-nightly-x86_64-$_target_os.tar.gz
  tar xf rust-nightly-i686-$_target_os.tar.gz

  cp -r rust-nightly-i686-$_target_os/lib/rustlib/i686-$_target_os \
     rust-nightly-x86_64-$_target_os/lib/rustlib

  cd rust-nightly-x86_64-$_target_os
  find lib/rustlib/i686-$_target_os/lib -type f | sed 's/^/file:/' >> manifest-rustc.in

  # Rust, Cargo and Documentation.
  ./install.sh --prefix=${pkgdir}${INSTALL_DIR} --mandir=${pkgdir}${MAN_DIR}

  # Establish .rs MIME type association.
  mkdir -p "${pkgdir}/usr/share/mime/packages"
  install -m 644 "${srcdir}/rust.xml" "${pkgdir}/usr/share/mime/packages/rust.xml"

  # Dynamic linker configuration (no LD_LIBRARY_PATH required).
  mkdir -p "${pkgdir}/etc/ld.so.conf.d"
  install -m 644 "${srcdir}/rust-nightly.conf" "${pkgdir}/etc/ld.so.conf.d/rust-nightly.conf"

  # Remove manifest file referencing $pkgdir.
  rm -f ${pkgdir}/usr/local/lib/rustlib/{manifest-rustc,manifest-cargo,components,rust-installer-version}
}

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が大変参考になりました。ありがとうございます。