Rustでぷよぷよ風ゲーム

最近遊んでいたおもちゃが形になってきた。

こんなん。ターミナルでプレイするぷよぷよ風ゲームのようなもの。(得点とかないし、まだゲームオーバーすらない)

f:id:totem_3:20170417230721g:plain

(だいぶカクカクしているのは agif の変換の仕方の問題が大きい)

Rust でマルチスレッドでプログラミングする方法の理解をもう少し深めようと思って書いていたが、一番大変だったのは ncurses で UI を書くところ。

ただしスレッドを使ったプログラミングについては少し理解が深まったのでそれはまた別途。

Mutex の挙動を勘違いしてデッドロックして悩んだり非常に無駄な時間を使っていた。

自分の場合はそもそもマルチスレッドの理解が浅いのでだいぶ基礎からやる必要があったけど、基本的にはここを読んでおけば大丈夫な気がする

qiita.com

一通り書いたあとでググったのでもっと速くググっておけば良かった。

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 の情報

www.chriskrycho.com

Rust メモ String に対して match

Rust の match は非常に強力。

様々なパターンマッチや、 destructuring が使えて非常に便利

詳しくは

たまに String な変数に対して match を使いたいことがある。

が、こんなふうにやろうとしてももちろんできない。

sString なのに対して、 パターンである "hoge"&'static str なので型が一致しない。

String リテラルはないし、パターンの部分には enum variant や構造体、リテラルを書くことができるが(slice は experimentalだけど)、式を書くことはできない。

ので、"hoge".to_string() なんて書くことはできない。

つーことで String ではムリ。

だが、 &str ならできる。

そして &strString のビューに過ぎなかった。 String から &str を作るのは簡単でコストも小さい。

こんな感じで。

1つ目 &*s

Stringstr への 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 メソッド が使える。

Rust Playground

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つ

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

型の名前を数字の後につけると型が定まる。

1u8u8, 10i32i32

型を宣言しているときや型が一意に定まるときは、サフィックスをつけなくても推論される。

任意の位置に_を入れることができる

123_456 == 123456

プリフィックスとしては一般的な感じ

  • 0x で16進数
  • 0o で8進数
  • 0b で2進数

として扱う

浮動小数点数

f32f64

整数と同様に_を任意の場所に入れられる。

整数と同様に型をサフィックスにすることで型を指定でき、指定しない場合は推論される。

値が溢れた場合は型エラーになる。

Rust メモ 文字列

文字列

&strString がある。

通常の文字列リテラル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()

が使える

&strString

to_string が使える

let s: String = "hoge".to_string();

文字列 → 数値 (など)

文字列から数値に変換するには、strparse が使える。

変数に型を明示するか、 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 系とは違っている。ドキュメントに詳しく書いてある。

std::fmt - Rust

対応はこうなる。 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をスケジュールして、タイムアウトを迎えたらプローブを送信するというもの。

プローブとしては、まだ送っていないセグメントがある場合は新しいセグメントを送り、ない場合は最後に送信したセグメントを再送する。

プローブを送ることで、

  1. 最後のセグメントだけがロスしている場合、最後のセグメントを再送することによってリカバリできる。
  2. 複数のセグメントがロスしている場合、プローブによって他のリカバリの仕組みがトリガーされてリカバリできる。

他のリカバリの仕組みというのは、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が古いので、既に古いところはありそう)