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を触ってみました。
Piston触ってみようかな #rustlang
— ぎふ (@gifnksm) 2015, 5月 16
チュートリアル編
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でもテトリスを作られていたので、ちょっとひねりをいれて、普通のテトリスではなく一人称視点のテトリスを作成してみました。
一人称視点テトリス大体できたけど、慣れると普通のテトリスとほとんど同じ間隔で操作できるな。。。 pic.twitter.com/ClwaLWJ4M0
— ぎふ (@gifnksm) 2015, 8月 11
通常のテトリスでは、固定されたステージに対して上からテトリミノ (ブロック) が落ちてきます。一人称テトリスでは、テトリミノは動かず、ステージが動くように見えます。操作方法は以下の通り。お暇があれば、是非上の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から元記事に自動的に飛ぶユーザースクリプト
multirust で今日から始める快適 rust 生活
Rust 1.0 リリースおめでとうございます!!!!
Rust 1.0 がリリースされました!!!!めでたい!!!!!!
このリリースは非常に大きな一歩です。これまではコンパイラのバージョンが上がる度にコードを頑張って修正する必要がありましたが、今後はstable
な rustc
を使って作ったコードについては、将来にわたってもコンパイル可能で動作することが保証されます。
(機能の deprecation などはありますので、絶対的なものではありませんが、破壊的変更の頻度・度合いはこれまでよりもだいぶ控えめになるはずで、バージョンアップへの追従を諦めてしまうこと、いわゆる "Rust 疲れ" 現象は減ることが期待できます)
現時点での Rust を取り巻く環境の問題
言語の安定化により、サードパーティーのライブラリは今後どんどん充実していくと予想されます。Rust の将来はバラ色になるに違いないと筆者は信じていますが、1.0がリリースされたばかりの現時点ではまだまだ理想の状態には到達していないのが現実です。例えば、本日実施された Rust 1.0 Release記念祝賀LT会 では、多くの方が以下について困っていると訴えていました。
- 使いたいライブラリが unstable な機能を使っているため、stable なコンパイラではコンパイルできない
- 使いたいライブラリがしばらく更新されておらず、最新の
rustc
+cargo
ではコンパイルできない
これらを回避するためには、nightly/beta バージョンのコンパイラと stable バージョンのコンパイラを、プロジェクトごとに切り替えて利用する必要があります。もしかしたら、特定の日付の nightly build を利用する必要が出てきてしまうかもしれません。非常にめんどくさい作業ですね。
今すぐに Rust を活用したい、そんなあなたのためのツールが multirust
です。
multirust
のススメ
multirust
は、一言で言ってしまえば ruby
の rvm
、python
の pyenv
のようなもので、複数バージョン (リリースチャンネル) の 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版の rustc
と cargo
がインストールされます (既に 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を見てみると、、、
あなたとRust, 今すぐコンパイル
— ぎふ (@gifnksm) 2014, 12月 20
コンパイルするだけでツイートされている!!!!!!!!!
なお、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"機能を活用した便利ライブラリももちろん開発されています。
- BurntSushi/quickcheck - quickcheck用関数を簡単に掛けるようにする #[quickcheck] を提供
- Manishearth/rust-clippy - コンパイル時警告をいくつか追加する
- huonw/spellck - ドキュメントコメントや変数名などのスペルチェックを行う lint を追加
少し検索しただけでこれだけのライブラリが出てきたので、他にもいろいろあるのでしょう。 外部の静的解析ツールに頼ること無く独自のlintを追加できるのは、compiler pluginの強みだと思います。
かなしいはなし
本記事で紹介してきた compiler plugin 機能ですが、利用するために "#![feature(...)]" が必要な実験的機能となっています。rustcは今後リリースチャンネルモデルに移行するようですが、実験的機能はbetaやreleaseのチャンネルでは無効化されてしまうため、利用するためにはnightlyチャンネルを利用する必要があります。一般のrustユーザまでcompiler pluginが届くのは、まだまだ先になりそうです。。。
謝辞
今回、compiler pluginを作る箇所は簡単に作れたのですが、Twitterに投稿する処理の作成で非常に苦労しました。明日の記事担当のwoxtuさんのdlang - 市民、認証は義務です - Qiitaが大変参考になりました。ありがとうございます。