Rustでぷよぷよ風ゲーム
最近遊んでいたおもちゃが形になってきた。
こんなん。ターミナルでプレイするぷよぷよ風ゲームのようなもの。(得点とかないし、まだゲームオーバーすらない)
(だいぶカクカクしているのは agif の変換の仕方の問題が大きい)
Rust でマルチスレッドでプログラミングする方法の理解をもう少し深めようと思って書いていたが、一番大変だったのは ncurses で UI を書くところ。
ただしスレッドを使ったプログラミングについては少し理解が深まったのでそれはまた別途。
Mutex の挙動を勘違いしてデッドロックして悩んだり非常に無駄な時間を使っていた。
自分の場合はそもそもマルチスレッドの理解が浅いのでだいぶ基礎からやる必要があったけど、基本的にはここを読んでおけば大丈夫な気がする
一通り書いたあとでググったのでもっと速くググっておけば良かった。
Rust メモ Windows向けにクロスコンパイル
Windowsでも使いたいコードがあり、Linux上でWindows向けにクロスコンパイルをしてみたメモ。 rustupを使うと簡単にクロスコンパイルができる。
windows向けのtargetを探す
$ rustup target list | grep win i586-pc-windows-msvc i686-apple-darwin i686-pc-windows-gnu i686-pc-windows-msvc x86_64-apple-darwin x86_64-pc-windows-gnu x86_64-pc-windows-msvc
今回は x86_64-pc-windows-gnu
を追加する
rustup target add x86_64-pc-windows-gnu $ rustup target add x86_64-pc-windows-gnu info: downloading component 'rust-std' for 'x86_64-pc-windows-gnu' info: installing component 'rust-std' for 'x86_64-pc-windows-gnu'
これを --target
にしてビルドするが、その前に mingw-w64
をインストール
$ sudo apt install mingw-w64 Unpacking gcc-mingw-w64-base (4.8.2-10ubuntu2+12) ... Selecting previously unselected package gcc-mingw-w64-i686. Preparing to unpack .../gcc-mingw-w64-i686_4.8.2-10ubuntu2+12_amd64.deb ... Unpacking gcc-mingw-w64-i686 (4.8.2-10ubuntu2+12) ... Selecting previously unselected package g++-mingw-w64-i686. Preparing to unpack .../g++-mingw-w64-i686_4.8.2-10ubuntu2+12_amd64.deb ... Unpacking g++-mingw-w64-i686 (4.8.2-10ubuntu2+12) ... Selecting previously unselected package mingw-w64-x86-64-dev. Preparing to unpack .../mingw-w64-x86-64-dev_3.1.0-1_all.deb ... Unpacking mingw-w64-x86-64-dev (3.1.0-1) ... Selecting previously unselected package gcc-mingw-w64-x86-64. Preparing to unpack .../gcc-mingw-w64-x86-64_4.8.2-10ubuntu2+12_amd64.deb ... Unpacking gcc-mingw-w64-x86-64 (4.8.2-10ubuntu2+12) ... Selecting previously unselected package g++-mingw-w64-x86-64. Preparing to unpack .../g++-mingw-w64-x86-64_4.8.2-10ubuntu2+12_amd64.deb ... Unpacking g++-mingw-w64-x86-64 (4.8.2-10ubuntu2+12) ... Selecting previously unselected package g++-mingw-w64. Preparing to unpack .../g++-mingw-w64_4.8.2-10ubuntu2+12_all.deb ... Unpacking g++-mingw-w64 (4.8.2-10ubuntu2+12) ... Selecting previously unselected package gcc-mingw-w64. Preparing to unpack .../gcc-mingw-w64_4.8.2-10ubuntu2+12_all.deb ... Unpacking gcc-mingw-w64 (4.8.2-10ubuntu2+12) ... Selecting previously unselected package gfortran-mingw-w64-i686. Preparing to unpack .../gfortran-mingw-w64-i686_4.8.2-10ubuntu2+12_amd64.deb ... Unpacking gfortran-mingw-w64-i686 (4.8.2-10ubuntu2+12) ... Selecting previously unselected package gfortran-mingw-w64-x86-64. Preparing to unpack .../gfortran-mingw-w64-x86-64_4.8.2-10ubuntu2+12_amd64.deb ... Unpacking gfortran-mingw-w64-x86-64 (4.8.2-10ubuntu2+12) ... Selecting previously unselected package gfortran-mingw-w64. Preparing to unpack .../gfortran-mingw-w64_4.8.2-10ubuntu2+12_all.deb ... Unpacking gfortran-mingw-w64 (4.8.2-10ubuntu2+12) ... Selecting previously unselected package gnat-mingw-w64-base. Preparing to unpack .../gnat-mingw-w64-base_4.8.2-12ubuntu2+12.1_amd64.deb ... Unpacking gnat-mingw-w64-base (4.8.2-12ubuntu2+12.1) ... Selecting previously unselected package gnat-mingw-w64-i686. Preparing to unpack .../gnat-mingw-w64-i686_4.8.2-12ubuntu2+12.1_amd64.deb ... Unpacking gnat-mingw-w64-i686 (4.8.2-12ubuntu2+12.1) ... Selecting previously unselected package gnat-mingw-w64-x86-64. Preparing to unpack .../gnat-mingw-w64-x86-64_4.8.2-12ubuntu2+12.1_amd64.deb ... Unpacking gnat-mingw-w64-x86-64 (4.8.2-12ubuntu2+12.1) ... Selecting previously unselected package gnat-mingw-w64. Preparing to unpack .../gnat-mingw-w64_4.8.2-12ubuntu2+12.1_all.deb ... Unpacking gnat-mingw-w64 (4.8.2-12ubuntu2+12.1) ... Selecting previously unselected package mingw-w64. Preparing to unpack .../mingw-w64_3.1.0-1_all.deb ... Unpacking mingw-w64 (3.1.0-1) ... Processing triggers for man-db (2.6.7.1-1ubuntu1) ... Setting up binutils-mingw-w64-i686 (2.23.52.20130620-1ubuntu1+3build1) ... Setting up binutils-mingw-w64-x86-64 (2.23.52.20130620-1ubuntu1+3build1) ... Setting up mingw-w64-common (3.1.0-1) ... Setting up mingw-w64-i686-dev (3.1.0-1) ... Setting up gcc-mingw-w64-base (4.8.2-10ubuntu2+12) ... Setting up gcc-mingw-w64-i686 (4.8.2-10ubuntu2+12) ... Setting up g++-mingw-w64-i686 (4.8.2-10ubuntu2+12) ... Setting up mingw-w64-x86-64-dev (3.1.0-1) ... Setting up gcc-mingw-w64-x86-64 (4.8.2-10ubuntu2+12) ... Setting up g++-mingw-w64-x86-64 (4.8.2-10ubuntu2+12) ... Setting up g++-mingw-w64 (4.8.2-10ubuntu2+12) ... Setting up gcc-mingw-w64 (4.8.2-10ubuntu2+12) ... Setting up gfortran-mingw-w64-i686 (4.8.2-10ubuntu2+12) ... Setting up gfortran-mingw-w64-x86-64 (4.8.2-10ubuntu2+12) ... Setting up gfortran-mingw-w64 (4.8.2-10ubuntu2+12) ... Setting up gnat-mingw-w64-base (4.8.2-12ubuntu2+12.1) ... Setting up gnat-mingw-w64-i686 (4.8.2-12ubuntu2+12.1) ... Setting up gnat-mingw-w64-x86-64 (4.8.2-12ubuntu2+12.1) ... Setting up gnat-mingw-w64 (4.8.2-12ubuntu2+12.1) ... Setting up mingw-w64 (3.1.0-1) ...
そしてビルドするとエラーが出る。こんな感じで。
$ cargo build --target x86_64-pc-windows-gnu Compiling ... v0.1.0 ( ) error: linking with `gcc` failed: exit code: 1 | = note: "gcc" "-Wl,--enable-long-section-names" "-fno-use-linker-plugin" "-Wl,--nxcompat" "-nostdlib" "-m64" "..." "..." "-L" "..." "..." "-o" "..." "-Wl,--gc-sections" "-nodefaultlibs" "-L" "..." "-L" "..." "-Wl,-Bstatic" "-Wl,-Bdynamic" "..." "..." "..." "..." "..." "..." "..." "..." "..." "..." "..." "-l" "ws2_32" "-l" "userenv" "-l" "shell32" "-l" "advapi32" "-l" "gcc_eh" "-lmingwex" "-lmingw32" "-lgcc" "-lmsvcrt" "-luser32" "-lkernel32" "..." = note: /usr/bin/ld: unrecognized option '--enable-long-section-names' /usr/bin/ld: use the --help option for usage information collect2: error: ld returned 1 exit status error: aborting due to previous error error: Could not compile `dumpdir`. To learn more, run the command again with --verbose.
既知の問題のようだ。
https://github.com/rust-lang/rust/issues/32859
ひとまずこれで回避できるとのこと。
$ cat .cargo/config [target.i686-pc-windows-gnu] linker = "i686-w64-mingw32-gcc" [target.x86_64-pc-windows-gnu] linker = "x86_64-w64-mingw32-gcc"
できた
$ cargo build --target x86_64-pc-windows-gnu Compiling .... v0.1.0 (file:///.....) Finished debug [unoptimized + debuginfo] target(s) in 0.19 secs
補足
各種バージョン情報
$ rustup --version rustup 0.6.5 (88ef618 2016-11-04) $ rustc --version rustc 1.13.0 (2c6933acc 2016-11-07) $ cargo --version cargo 0.13.0-nightly (eca9e15 2016-11-01) $ uname -a Linux workspace 3.13.0-92-generic #139-Ubuntu SMP Tue Jun 28 20:42:26 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux $ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=14.04 DISTRIB_CODENAME=trusty DISTRIB_DESCRIPTION="Ubuntu 14.04.4 LTS"
x86_64-pc-windows-msvc
の情報
Rust メモ String に対して match
Rust の match
は非常に強力。
様々なパターンマッチや、 destructuring が使えて非常に便利
詳しくは
たまに String
な変数に対して match
を使いたいことがある。
が、こんなふうにやろうとしてももちろんできない。
s
は String
なのに対して、 パターンである "hoge"
は &'static str
なので型が一致しない。
String
リテラルはないし、パターンの部分には enum variant や構造体、リテラルを書くことができるが(slice は experimentalだけど)、式を書くことはできない。
ので、"hoge".to_string()
なんて書くことはできない。
つーことで String
ではムリ。
だが、 &str
ならできる。
そして &str
は String
のビューに過ぎなかった。 String
から &str
を作るのは簡単でコストも小さい。
こんな感じで。
1つ目 &*s
String
は str
への Deref
を実装しているので、 *s
で dereference することで str
が手に入るので &*s
で &str
という寸法。
2つ目 &s[..]
String
を 全体をスライシングしても &str
が手に入る
3つ目は単純にメソッドがあるのでそれを使って &str
に変換している
--
ついでに、 String
から &str
は安いが、&str
から String
にするのはメモリアロケーションが発生してそれなりにコストがかかるので、 match に限らず文字列を比較したい時には文字列リテラルを to_string()
とかで String
にするより、今回のように String
を &str
にしたほうがいい
Rust メモ Option | 値を取得して None で置き換える
Option
を持つ構造体を扱っているときなどに、Option
の値を取得してその後 None
で置き換えたいことがある。
そんなときは take
メソッド が使える。
fn main() { let mut x = Some(10); // Some(v) に対して呼び出すと Some(v) が返ってくる assert_eq!(x.take(), Some(10)); // x は None になってる assert_eq!(x, None); // None に対して呼び出すと None が返ってくる assert_eq!(x.take(), None); }
Rust メモ リテラル
Reference
ここにまとまっている
https://doc.rust-lang.org/reference.html#literals
文字列関連
文字列関連は6つ
- 文字リテラル
- 文字列リテラル
- Raw文字列リテラル ( rの後に0個以上の任意個の
#
と"
で囲う ) - バイトリテラル
- バイト文字列リテラル
- Rawバイト文字列リテラル ( brの後に0個以上の任意個の
#
と"
で囲う )
type | 例 | 値 | |
---|---|---|---|
文字 | char |
'A' |
'A' |
文字列 | &str |
"aaa" |
"aaa" |
Raw文字列 | &str |
r"aaa" , r#"aaa"# |
"aaa" |
バイト | u8 |
b'A' |
97 |
バイト文字列 | &[u8] |
b"aaa" |
[97, 97, 97] |
Rawバイト文字列 | &[u8] |
br#"aaa"# |
[97, 97, 97] |
Raw文字列リテラルは
- 複数行の文字列を書いたり
#
を付ければ"
をエスケープなしで書くとか\
でエスケープさせたくないとか
という時に使える。
整数
整数型は10種類
u8
u16
u32
u64
usize
i8
i16
i32
i64
isize
型の名前を数字の後につけると型が定まる。
1u8
で u8
, 10i32
で i32
型を宣言しているときや型が一意に定まるときは、サフィックスをつけなくても推論される。
任意の位置に_
を入れることができる
123_456 == 123456
プリフィックスとしては一般的な感じ
0x
で16進数0o
で8進数0b
で2進数
として扱う
浮動小数点数
f32
とf64
整数と同様に_
を任意の場所に入れられる。
整数と同様に型をサフィックスにすることで型を指定でき、指定しない場合は推論される。
値が溢れた場合は型エラーになる。
Rust メモ 文字列
文字列
&str
と String
がある。
通常の文字列リテラルは String
ではなく &str
. ( staticな生存期間を持つので &'static str
. )
Raw String Literal
https://doc.rust-lang.org/reference.html#raw-string-literals
いわゆるヒアドキュメント的なもの。
複数行の文字列を書きたいときとか、エスケープが多くなるときに使える記法。
fn main() { let s = r"foo bar"; println!("s: {}", s); let s2 = r#""foo" "bar""#; println!("s2: {}", s2); }
https://play.rust-lang.org/?gist=570032ed1f2969b4a83bd2c958e61adc&version=stable&backtrace=0
変換
String
→ &str
&str
が要求されている場合は、単に &s
でいい
fn print_str(s: &str) { println!("str: {}", s); } fn main() { let s: String = "hoge".to_string(); print_str(&s); }
https://play.rust-lang.org/?gist=8735d3d12183b7e3af000db46f0213e3&version=stable&backtrace=0
ドキュメントにある通り、 ToSocketAddr
など &str
に実装されているトレイトが必要な場合は明示的に変換する必要がある。
変換する場合は
&*s
s.as_str()
が使える
&str
→ String
to_string
が使える
let s: String = "hoge".to_string();
文字列 → 数値 (など)
文字列から数値に変換するには、str
の parse
が使える。
変数に型を明示するか、 parse::<i32>()
といった形式で型を指定することで特定の型に変換できる。
変換先にできるのは数値に限らず FromStr
を実装しているもの。
fn main() { let s = "1"; let i = s.parse::<i32>().unwrap(); println!("i: {:?}", i); let u: u32 = s.parse().unwrap(); println!("u: {:?}", u); }
https://play.rust-lang.org/?gist=1428b04c59de601c19bbc62fbd935bdb&version=stable&backtrace=0
FromStr
を実装すれば自分で定義した構造体にも変換できる
use std::str::FromStr; #[derive(Debug)] struct Person { age: i32, name: String, } #[derive(Debug)] struct PersonParseError; impl FromStr for Person { type Err = PersonParseError; fn from_str(s: &str) -> Result<Self, Self::Err> { let ss:Vec<&str> = s.split(",").collect(); if ss.len() != 2 { return Err(PersonParseError{}); } let age = match ss[0].parse() { Ok(v) => v, _ => return Err(PersonParseError{}), }; let name = ss[1].to_string(); Ok(Person{age: age, name: name}) } } fn main() { let s = "32,totem"; let p: Person = s.parse().unwrap(); println!("{:?}", p); }
https://play.rust-lang.org/?gist=a69a67aa2888fdaad9a811ad712f45ab&version=stable&backtrace=0
整形
出力するだけなら println!
マクロや print!
マクロ、 文字列として返すなら format!
マクロを使う。
フォーマット文字列が printf
系とは違っている。ドキュメントに詳しく書いてある。
対応はこうなる。 println!("{}", s)
や format!("{:?}", x)
と使う。
{}
→Display
{?}
→Debug
{o}
→Octal
{x}
→LowerHex
{X}
→UpperHex
{p}
→Pointer
{b}
→Binary
{e}
→LowerExp
{E}
→UpperExp
第一引数にフォーマット文字列を渡すが、ここには変数を渡すことはできなくて文字列リテラルしか渡すことができない。
幅についてはパラメータを渡すことができるので動的に変えることができる。
fn main() { println!("'{:0width$}'", 10, width = 5); }
https://play.rust-lang.org/?gist=fe8190d386e7a4ef0282ababa494a6b1&version=stable&backtrace=0
TCPのTail Loss Probeと再送周りについて少し
仕様は RFC にはないっぽくて(?) 2013年にgoogleの方々が出してるinternet draft で定義されている模様。
Tail Loss Probe は、一連の送信パケットの最後のパケットがロスした場合に、送信側が再送タイムアウトを待たずにロスを検知して回復することを目的としている。
パケットがロスした時、後続のパケットが次々と送られている場合はduplicate ackが返されるため、fast retransmitによってリカバリすることができる。
しかし、末尾のパケットや途中から末尾までウィンドウいっぱいのパケットがロスしてしまった場合などは、dup ackが届かないためfast retransmitはトリガーされないため再送タイムアウトまでリカバリできない。
再送タイムアウトを待つと時間がかかりすぎるし、タイムアウトが発生すると輻輳ウィンドウが小さくなってスロースタートからやり直しになってしまう。
そのためなんとか末尾のパケットのロスもタイムアウトより先に検知してリカバリしたいというのが動機。
それをどうやって実現しているかというと、簡単には次のような感じ。
再送タイムアウト(RTO)とは別に、probe timeout (PTO) を定義する。PTOの初期値は先述のdraftで max(2 * SRTT, 10ms)
となっており、linuxのRTOの初期値の1秒と比べると短め。
で、データを送信するたびにPTOをスケジュールして、タイムアウトを迎えたらプローブを送信するというもの。
プローブとしては、まだ送っていないセグメントがある場合は新しいセグメントを送り、ない場合は最後に送信したセグメントを再送する。
プローブを送ることで、
- 最後のセグメントだけがロスしている場合、最後のセグメントを再送することによってリカバリできる。
- 複数のセグメントがロスしている場合、プローブによって他のリカバリの仕組みがトリガーされてリカバリできる。
他のリカバリの仕組みというのは、early retransmit[RFC5827]とFACK fast recoveryなど。
early retransmitは、ネットワークに送出されているセグメントが4つ未満である場合、fast retransmitをトリガーするduplicate ackの回数を減らすというもの。
SACKオプションが有効な場合は、(送出中のセグメント-1)回のdup ackを受け取り、かつ最後のセグメントがSACKされている場合は再送する。
例えば最後の2つのセグメントがロスした場合、duplicate ackを受け取ることはないので、TLPがない場合はタイムアウトまで再送されることはない。
TLPがある場合は、PTO後に最後のセグメントを再送するので、それがちゃんと届けば送信側は最後のセグメントがSACKされたdup ackを受け取り、early retransmitにより(dup ackのthreasholdが低くなっているのでfast retransmitがトリガーし)ロスしたセグメントが再送される。
最後の3つのセグメントがロスした場合については1つのプローブを再送しただけではRFC5827のearly retransmitの範囲では再送するには足りないので、最後のセグメントがSACKされたら再送し始めるアルゴリズムを提案している。(proportional rate reduction algorithm。http://conferences.sigcomm.org/imc/2011/docs/p155.pdf)
FACK fast recoveryも、同じくduplicate ackの数が少なくても再送しようというもの。
再送する時duplicate ackを3つ以上受け取ったら、となっているのはパケットの順序が入れ替わっただけの場合でもduplicate ackを受け取る可能性はあるからだが、FACK fast recoveryでは順序の入れ替わりであっても抜けが多かったら再送してしまおうというもの。(超ざっくり)
SACKされている最大のack番号と、送信済みでackされていない最小のシーケンス番号の差が3(fast retransmitをトリガーするdup ackの閾値)以上なら再送する。
最後のセグメントを含む4つ以上のセグメントがロスしている場合、再送されたプローブが届いたらこれで再送が始まり、リカバリできる。
ちなみに上記の通り色々な箇所でSACKが使われているので受信者側がSACKオプションを使えることは必須。
ということでTLPでロスをリカバリできるようになる、ということでした。
(主に参照したdraftが古いので、既に古いところはありそう)