Rust の Index bounds check の性能影響を調べてみた
Rust の配列 (Vec<T>
や [T]
など) では、インデックスアクセス時に境界チェック (index bounds check) が常に行われます。
常に行われる、というのはビルドの種類 (リリースビルド・デバッグビルド) に依存しないという意味です。
とにかく安全性を重視する Rust らしい仕様であると言えます。
他のプログラミング言語に目を向けて見ると、常にチェックが行われるシステムプログラミング言語というのも珍しい気がします。 すべてがプログラム作成者の責任になる C では当然何のチェックも行われませんし、 安全なシステムプログラミング言語という意味で Rust と立ち位置が近い D では、デバッグビルドの場合のみ境界チェックが行われます (リリースビルドでは行われない)。
配列の境界チェックは実行時の処理になるため、普通に考えると Rust の方がチェック処理の分だけ実行性能が落ちてしまうことになります。境界チェックが行われない配列アクセス手段が用意されている とはいえ、配列アクセスが遅いのであれば Rust で書かれる様々なプログラムの実行性能も遅くなってしまわないか心配になってしまいます。
というわけで、Rust の配列アクセス性能を測定してみました。
tl;dr
Index bounds check のコストが支配的になるケースは少なそう。
注意
以下で出てくるベンチマーク結果は、仮想マシン上の Linux で測定したものなので実マシン上とは異なる結果になっている可能性があります。 また、筆者はアセンブリが読めず、CPUの気持ちも分からないため、コンパイル結果に関する見解は多くの誤りが含まれている可能性があります。 もし変な記述があれば、コメント等頂けると大変助かります。
テストに利用したソースコード一式は、以下Gistからどうぞ。
https://gist.github.com/gifnksm/9a792feff8567067b2d272c86258c2d4
利用したコンパイラは、nightly (2016/5/30) です。
$ rustc --version rustc 1.11.0-nightly (a967611d8 2016-05-30)
テスト1: 配列先頭からシーケンシャルアクセスする
まずは、シンプルなケースから。いくつかの関数について性能を比較します。
1. インデックスアクセスその1 (D index)
#[inline(never)] pub fn direct_index(arr: &[u64]) -> u64 { let mut sum = 0; for i in 0..arr.len() { sum += arr[i]; } sum }
libcoreのソース によると、arr[i]
へのアクセスで arr.len()
との比較により index bounds check が行われます (assert!
はリリースモードでも消えずに残ります)
arr.len()
との比較は Range::next()
でも行われている ため、コンパイラの最適化により arr[i]
の index bounds check は除去されるかもしれません
2. インデックスアクセスその2 (D index_len)
#[inline(never)] pub fn direct_index_len(arr: &[u64], len: usize) -> u64 { let mut sum = 0; for i in 0..len { sum += arr[i]; } sum }
i
の範囲を引数で渡すように変更したものです。arr[i]
の index bounds check が除去されずに残ることが期待値です。
3. イテレータでのアクセス (D iter)
#[inline(never)] pub fn direct_iter(arr: &[u64]) -> u64 { let mut sum = 0; for &n in arr { sum += n; } sum }
std::slice::Iter::next の実装によると、イテレータを使った場合はインデックスアクセスの場合と異なり、Cと同等の性能になることが期待されます。
4. C プログラム (D c)
uint64_t direct_c(const uint64_t *arr, size_t len) { uint64_t sum = 0; for (size_t i = 0; i < len; i++) { sum += arr[i]; } return sum; }
テスト1の測定結果
$ cargo bench test tests::D_c ... bench: 26,372 ns/iter (+/- 362) test tests::D_index ... bench: 26,389 ns/iter (+/- 373) test tests::D_index_len ... bench: 59,863 ns/iter (+/- 602) test tests::D_iter ... bench: 26,386 ns/iter (+/- 465)
1, 3, 4 (index, iter, c) はほぼ性能は同じです。objdump -d
の結果を見ると 1, 4 (index, c) はほぼ同じアセンブリが出力されていましたが、 3 (iter) は少し長めのアセンブリでした。
1, 2 の結果を比較すると、1の場合は想定通り、 arr[i]
の index bounds check は除去されているように思えます。
テスト1のまとめ
配列をシーケンシャルに先頭からアクセスしていく場合、 index bounds check のコストは無視して良いと思います。
先頭からアクセスする場合、多くのケースではイテレータを使うことが多いでしょう。また、スライスを渡す場合でも、スライスと len をペアで渡すよりも、&[..len] のように slice を slicing して渡すような書き方をする方が、引き渡す引数の数を減らすことができ、インタフェースがシンプルになるなどのメリットがあるためです。 slicing した場合、1と同じような性能になることが予想されます。
テスト2: 外部から指定された順序でインデックスアクセスする
配列先頭からシーケンシャルアクセスするのではなく、ランダムな順番など関数外から渡される順番に基づいて間接的 (?) にアクセスするケースについて測定します。
1. index bounds check ありの間接アクセス (I iter, R iter)
#[inline(never)] fn indirect_iter(arr: &[u64], idx: &[usize]) -> u64 { let mut sum = 0; for &i in idx { sum += arr[i]; } sum }
2. index bounds check なしの間接アクセス (I iter_nc, R iter_nc)
#[inline(never)] unsafe fn indirect_iter_uc(arr: &[u64], idx: &[usize]) -> u64 { let mut sum = 0; for &i in idx { sum += *arr.get_unchecked(i); } sum }
1 の index bounds check を行わない版です。
3. C プログラム (I c, R c)
uint64_t indirect_c(const uint64_t *arr, const size_t *idx, size_t idx_len) { uint64_t sum = 0; for (size_t i = 0; i < idx_len; i++) { sum += arr[idx[i]]; } return sum; }
テスト1の測定結果
$ cargo bench test tests::I_c ... bench: 60,888 ns/iter (+/- 1,113) test tests::I_iter ... bench: 69,746 ns/iter (+/- 1,124) test tests::I_iter_uc ... bench: 69,355 ns/iter (+/- 633) test tests::R_c ... bench: 162,968 ns/iter (+/- 14,711) test tests::R_iter ... bench: 169,162 ns/iter (+/- 1,470) test tests::R_iter_uc ... bench: 163,944 ns/iter (+/- 1,850)
I_XXX
はシーケンシャルにアクセスする場合、R_XXX
はランダムアクセスする場合です。
どちらのケースでも c, iter_uc, iter の順で高速です。
_iter
と _iter_uc
で実行速度に差はあるのですが、Cと差が生じているのは別の理由によるのかもしれません。
テスト2のまとめ
この例のように配列のインデックスを間接アクセスするケースは、配列の要素をランダムアクセスすることが多いと思いますが、その場合、 index bounds check ではない別の原因で大きく性能が落ちてしまう (キャッシュ関連?) ようです。 ですので、index bounds check だけをことさら取り上げる必要もないのかなぁ、と思います。
まとめ
考察が雑。
Xperia Z5の画面が割れてタッチに反応しなくなった時にやったこと
機種変更して1ヶ月くらいのXperia Z5の画面を割ってしまう事故を起こしてしまった時にやったことを記録がてらメモ。 同じような事故を起こしてしまった人の助けとなれば。。。
前提
電源を切る
携帯電話を目覚まし時計代わりに使っている方は多いと思いますが、タッチ操作が効かなくなってしまった場合、鳴り始めたアラームをとめることが出来ません。 特にスヌーズ機能を有効にしている場合、アラームはいつまでも鳴り続けるようです。 (朝、家を出るときにアラームをとめられず、諦めて家に放置した携帯電話が、夜帰宅して鳴り続けているのを見たときの絶望感ときたら。。。)
アラームをとめるために電源を切ろうとしても、電源ボタンを長押しするだけでは電源オフ、再起動等の選択肢が表示されるだけで、タッチ操作が要求されてしまいます、が、以下のいずれかの方法で、タッチ操作なしで電源を切ることが出来ます。
- 電源ボタンと、ボリューム増ボタンを同時に長押し
- microSDカード差し込み口横の緊急停止ボタンを押す http://csqa.kddi.com/posts/view/qid/k15102833051
電源+ボリューム増ボタン長押しの方が、カバーを開ける必要がなく楽です。
マウスを接続
タッチ操作が効かなくなった携帯の操作方法として、マウスを接続する方法が有名ですが、 Xperia Z5 の場合、電源オン状態でマウスを接続するだけではマウスを使う事が出来ません (通常時はUSBポートに通電されておらず、機器接続のためにいくつか操作する必要があるそうです)。
しかし、マウスを接続したまま端末を再起動すると、なぜか機器が認識され、ロック画面にマウスカーソルが表示されます。 タッチ操作が効かなくなった場合の救済措置でしょうか。
USB type A 接続のマウスはそのままではつなぐことは出来ないため、 USB type A メス <-> マイクロ"B" オス の変換コネクタが必要です。 マイクロUSBにもAとBのタイプがあるので、お間違えなく。 わたしは、間違えて無駄な変換コネクタを買ってしまいました。。。
機種破損時の交換サービスを利用する
わたしは au ユーザで、au 安心携帯サポートというサービスに入っていたため、以下から申し込みをすることで、実費負担2000円でリフレッシュ品の機種と交換することができました。
日付が変わる頃に申し込みをしたのですが、その二日後には交換用端末が届きました。申し込みとほぼ同時に発送してくれるようです。 壊れてしまった機種の交換もポストに投函するだけと非常に簡単でした。 もしこのサービスに加入されているのなら使わない手はないと思います。
なんかauの宣伝みたいですね。
データを移行する
ここまでの作業で新しい端末も手に入りますので、後は壊れてしまった端末からデータを移すだけです。 Xperia Transfer Mobile というアプリを使うと、Xperia間のアプリケーションのデータやホーム画面のアイコン配置までそっくりそのままコピーしてくれました。移行には結構時間がかかったのですが、環境がほとんどそのまま移動されるので便利です (一部アプリは移行できていなかったようですが)。
まとめ
携帯の破損でいろいろと苦労しましたが、最終的には問題解決して、元と同じ環境に戻すことができました。 戻せるとは言え、携帯を壊してしまうと面倒な作業が発生しますし、こんな作業はやらないで済むに越したことはないですね。。。
EmscriptenでRustをJavaScriptへコンパイルする
以前のエントリではうまくいかなかった、RustのJavaScriptへのコンパイルですが、
先日フォーラムに投稿されていたやり方をまねるだけで簡単にできるようになっていました。
Rust本体にEmscripten対応が取り込まれたのですが、動かすためには専用のLLVMが必要などいくつか制限があります。 制限があるとはいえ、以前よりも簡単にEmscriptenでのコンパイルが試せるようになったのは朗報ですね!
追記
リンクしか内容がない記事というのもアレなので、Emscripten を使ったコンパイル方法を簡単に書いておきます。
Emscripten の環境変数を設定する
$ source <path to emsdk>/emsdk_portable/emsdk_env.sh
rustc
を--target
オプションつきで呼び出す$ <path to rust root>/x86_64-unknown-linux-gnu/stage2/bin/rustc --target asmjs-unknown-emscripten ./hello.rs -O
以上。簡単ですね。
以下のプログラムをコンパイルしてみたところ、1MB程度の巨大なJavaScriptのコードが生成されました。ただの Hello world にしてはすごい量ですが、Emscriptenのランタイム (?) のソースコードの量が多いようです。
fn main() { println!("Hello emscripten world!"); }
-O
や、 -C lto
等の最適化オプションをつけることもできます。
最適化あり・なしの結果をGistに貼り付けてみました。
main関数はここから始まるようです。 なにがなんやら、、、といった感じですね。。。。
LTO有効だからといって、劇的にサイズが減るわけでもないようです。
EmscriptenでRustをJSへコンパイルする (コンパイルできてない)
2016/3/8追記
うまくいきました。エントリ書きました。
RustでEmscriptenを使う方法を調べたのでメモ。2015年12月28日時点でなんとか動作させるための手順です。今後オフィシャルにEmscriptenサポートが入るなどすると、もっと簡単なやり方になるかと思います。
Emscripten SDK をインストール
Emscriptenのサイト から Emscripten SDK をダウンロードしてきて、インストールします。
$ wget https://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz $ tar xvf emsdk-portable.tar.gz $ cd emsdk_portable $ ./emsdk update $ ./emsdk install latest $ ./emsdk activate latest
Emscripten 対応の rust をコンパイル
brson/rust の emscripten
ブランチをクローン。
$ git clone git@github.com:brson/rust.git -b emscripten
なお、12/28時点でのコミットのハッシュは c42f32e
です。
当該ブランチの src/llvm
submodule は rust-lang
のものが読み込まれていますが、ハッシュ値は emscripten の LLVM fork (emscripten-fastcomp) のものとなっていますので、以下の変更を加えます
(.gitmodules
のコミット漏れ?)
diff --git a/.gitmodules b/.gitmodules index 83be070..5f92864 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "src/llvm"] path = src/llvm - url = https://github.com/rust-lang/llvm.git + url = https://github.com/kripken/emscripten-fastcomp.git branch = master [submodule "src/compiler-rt"] path = src/compiler-rt
ここまで準備できたらいよいよ configure
& make
です。以下の作業が必要なようです。
- Emscripten SDK の環境変数の有効化
- 最適化の無効化 (releaseモードだとうまく動作しないらしい)
$ source <path to SDK>/emsdk_env.sh $ ./configure --target=asmjs-unknown-emscripten $ make -j4
なお、手元の OSX 環境では configure
に失敗するため、EmscriptenのIssueコメントを参考にし、 CC=gcc CXX=g++
の環境変数を指定して configure
したところ、うまく動作しました (デフォルトコンパイラが clang の環境でEmscripten 側の clang が使われてしまうとだめなのかもしれません)。
configure: error: C preprocessor "/lib/cpp" fails sanity check
hello world! のコンパイル
謎のエラーが出て通らず。。。
$ LD_LIBRARY_PATH=../rust-emscripten-c42f32e/x86_64-unknown-linux-gnu/stage2/lib/ ../rust-emscripten-c42f32e/x86_64-unknown-linux-gnu/stage2/bin/rustc ./main.rs --target asmjs-unknown-emscripten error: linking with `emcc` failed: exit code: 1 note: "emcc" "-L" "/home/nksm/tmp/rust-emscripten-c42f32e/x86_64-unknown-linux-gnu/stage2/lib/rustlib/asmjs-unknown-emscripten/lib" "main.0.o" "-o" "main.js" "-Wl,--gc-sections" "-nodefaultlibs" "-L" "/home/nksm/tmp/rust-emscripten-c42f32e/x86_64-unknown-linux-gnu/stage2/lib/rustlib/asmjs-unknown-emscripten/lib" "-Wl,-Bstatic" "-Wl,-Bdynamic" "/home/nksm/tmp/rust-emscripten-c42f32e/x86_64-unknown-linux-gnu/stage2/lib/rustlib/asmjs-unknown-emscripten/lib/libstd-ca1c970e.rlib" "/home/nksm/tmp/rust-emscripten-c42f32e/x86_64-unknown-linux-gnu/stage2/lib/rustlib/asmjs-unknown-emscripten/lib/libcollections-ca1c970e.rlib" "/home/nksm/tmp/rust-emscripten-c42f32e/x86_64-unknown-linux-gnu/stage2/lib/rustlib/asmjs-unknown-emscripten/lib/librustc_unicode-ca1c970e.rlib" "/home/nksm/tmp/rust-emscripten-c42f32e/x86_64-unknown-linux-gnu/stage2/lib/rustlib/asmjs-unknown-emscripten/lib/librand-ca1c970e.rlib" "/home/nksm/tmp/rust-emscripten-c42f32e/x86_64-unknown-linux-gnu/stage2/lib/rustlib/asmjs-unknown-emscripten/lib/liballoc-ca1c970e.rlib" "/home/nksm/tmp/rust-emscripten-c42f32e/x86_64-unknown-linux-gnu/stage2/lib/rustlib/asmjs-unknown-emscripten/lib/liballoc_system-ca1c970e.rlib" "/home/nksm/tmp/rust-emscripten-c42f32e/x86_64-unknown-linux-gnu/stage2/lib/rustlib/asmjs-unknown-emscripten/lib/liblibc-ca1c970e.rlib" "/home/nksm/tmp/rust-emscripten-c42f32e/x86_64-unknown-linux-gnu/stage2/lib/rustlib/asmjs-unknown-emscripten/lib/libcore-ca1c970e.rlib" "-l" "c" note: error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) error: Unknown attribute kind (45) Traceback (most recent call last): File "/home/nksm/dev/emsdk_portable/emscripten/master/emcc", line 1309, in <module> final = shared.Building.llvm_opt(final, link_opts, DEFAULT_FINAL) File "/home/nksm/dev/emsdk_portable/emscripten/master/tools/shared.py", line 1471, in llvm_opt assert os.path.exists(target), Failed to run llvm optimizations: + output AssertionError: Failed to run llvm optimizations: error: aborting due to previous error
うーん、なんでしょうこれは。
kcovを使ってRustプログラムのカバレッジを測定する
本記事は Rust Advent Calendar 2015 13日目の記事です。
前置き
Rust 1.5リリースおめでとうございます!!!
cargo install
の登場により、Rust製ツール群のインストールが飛躍的に楽になりました。最近のアップデートに追従できていなかったという方も、是非是非ダウンロードしてみてください。
背景
Rustには、 言語組み込みのユニットテスト機能 や、cargo test
のインテグレーションテストサポートなど、テストを実行させる方法は充実しているのですが、gcov相当のカバレッジ採取機能については、 2015年12月時点では残念ながらサポートされていません (RFCリポジトリにIssueはあります)。しかし、Rust にはプログラムのDWARF情報 (デバッグ情報) を生成する機能があるため、 kcov というプログラムを利用することでカバレッジを採取することが可能です。
本記事では、 kcov を利用して Rust プログラムのカバレッジを測定する方法について説明します。
kcov のインストール
各ディストリビューションでバイナリが配付されていない場合、ソースコードからビルドすることになります。 以下に載っているコマンドラインを参考にしてみてください。
- Ubuntuの場合
- CentOSの場合
Arch Linux の場合: AUR に kcov-git パッケージが存在するため、以下コマンドでインストール可能です (yaourt を利用している場合)。
$ yaourt -S kcov-git
kcov を使う
cargo test --no-run
でデバッグシンボルのついたテスト用実行ファイルを作成し、kcov 経由で実行します。
$ cargo test --no-run $ kcov target/cov target/debug/$TEST_EXECUTABLE
上記実行すると、 target/cov
ディレクトリ配下にカバレッジ測定結果を格納したHTMLファイルが生成されます。
デフォルトでは、 ~/.cargo
配下のファイル (Cargo.toml
で依存関係として指定したライブラリ) についてもカバレッジ測定対象となってしまうため、邪魔であれば以下のように --exclude-pattern
オプションを指定することで、カバレッジ測定対象を絞ることが出来ます。
$ kcov --exclude-pattern=/.cargo target/cov target/debug/$TEST_EXECUTABLE # multirust を使っている場合、以下 $ kcov --exclude-pattern=/.multirust target/cov target/debug/$TEST_EXECUTABLE
また、kcov
を複数回実行した場合、カバレッジ情報が蓄積されていきます。引数などの条件を変えて何度もコマンドを実行し、それらの結果を合算することなどが可能です。
$ kcov --exclude-pattern=/.cargo target/cov target/debug/$TEST_EXECUTABLE $ kcov --exclude-pattern=/.cargo target/cov target/debug/$TEST_EXECUTABLE --ignored
Coveralls との連携
Coveralls とは、 GitHub (BitBucketも?) で開発しているソースコードのテストカバレッジを表示してくれるサービスです。 Travis CI と連携して利用します。
kcov には測定したカバレッジを Coveralls へと送信してくれる機能があり、.travis.yml
上で kcov を以下のように呼び出すよう設定するだけで Coveralls を利用できます。
$ kcov --coveralls-id=${TRAVIS_JOB_ID} target/cov target/debug/$TEST_EXECUTABLE
ただし、Travis CI にはデフォルトで kcov はインストールされていないため、 .travis.yml
中で kcov をインストールしてやる必要があります。
language: rust sudo: false addons: apt: packages: - libcurl4-openssl-dev - libelf-dev - libdw-dev before_script: - | if [ "${TRAVIS_OS_NAME}" = "linux" ]; then export KCOV="${TRAVIS_BUILD_DIR}/kcov/build/src/kcov --exclude-pattern=/.cargo --coveralls-id=${TRAVIS_JOB_ID} ${TRAVIS_BUILD_DIR}/coverage" wget https://github.com/SimonKagstrom/kcov/archive/master.zip unzip master.zip mv kcov-master kcov mkdir kcov/build cd kcov/build cmake .. make cd ${TRAVIS_BUILD_DIR} fi script: - ${KCOV} target/debug/$TEST_EXECUTABLE
すこしめんどくさいですね。
travis-cargo を使う
すべての Rust プロジェクトの .travis.yml
に kcov のインストール手順を書くのは面倒だというあなたのために、 travis-cargo という便利ツールが公開されています。
rustdoc
の生成物を github.io
にアップロードするなど、カバレッジ生成採取の便利な機能も搭載されています。
一部、travis-cargo
では実現できないこともあるのですが (cargo test
で生成されるバイナリ以外のカバレッジ採取する場合など)、多くの場合、 travis-cargo
を使うのが便利だと思います。
最後に
筆者が公開している以下リポジトリでは、ビルド時にカバレッジを採取し、バッジとして README に表示しています。.travis.yml
の書き方の参考にどうぞ。
- gifnksm/srither: Rustで書かれたスリザーリンク (パズル) のソルバー。
travis-cargo
は使っていません。 - gifnksm/oauth-client-rs
- gifnksm/generic-matrix-rs
- gifnksm/union-find-rs
- gifnksm/twitter-api-rs
- gifnksm/topological-sort-rs
- gifnksm/ProjectEulerRust
- gifnksm/polynomial-rs
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
が使えたりしてうれしい面の方が多かったかなぁと思います。もう少し複雑なことをやろうとするといろいろと難しいことがあるかもしれませんが、今のところ不満は特にないですね。
なお、一人称テトリスには先行研究があります。こっちの方がグラフィックは凝っていて良い感じです。ここまでできるようになると、更に楽しくなりそうですね。