Rust速習会4

Ba655e3712aaabfbca289fe136f85fe4?s=47 Masaki Hara
December 13, 2018

 Rust速習会4

Ba655e3712aaabfbca289fe136f85fe4?s=128

Masaki Hara

December 13, 2018
Tweet

Transcript

  1. Rust速習会(4) コマンドラインツール 2018-12-13 @Wantedlyオフィス (白金台) 原 将己 (@qnighy) 実況用ハッシュタグ: #rust_jp

  2. 今日の予定 • Rust2018の話 • コマンドラインツールを書いてみよう

  3. 先にやっておくこと • Rust2018に対応した最新版の入手 $ rustup update $ rustc –version rustc

    1.31.0 (abe02cefd 2018-12-04)
  4. 先にやっておくビルド(1) • ripgrepのビルド $ git clone https://github.com/BurntSushi/ripgrep.git $ cd ripgrep

    $ cargo build
  5. 先にやっておくビルド(2) • 以下のプロジェクトの作成 $ cargo new ferris_watch $ cd ferris_watch

    # Cargo.tomlを編集 $ cargo build
  6. 先にやっておくビルド(2) • Cargo.tomlはこんな感じにする [dependencies] log = "0.4.6" env_logger = "0.6.0"

    failure = "0.1.3" signal-hook = "0.1.6" clap = "2.32.0" pancurses = "0.16.0"
  7. 突然ですが、Rust2018の紹介 をしたいと思います。なぜかというと12/06の1.31.0リリースにより Rust2018が心おきなく使えるようになっただけではなく、これがデフォル トになっていくからです。非互換なのに相互運用可能なのはどういうこと なのかをちゃんと知っておいたほうがいいと思うので説明します。

  8. Rust2018とは? •既存のエコシステムとの 相互運用性 を保ちつつ、言語に 非互換な変更 を入れ、より使いやすい言語へと進化させ る仕組み ……つまりどういうことか、以下解説します

  9. Rustのコンパイラバージョン • Rustのコンパイラバージョンはsemver 1.31.0 メジャーバージョン 遠い将来に上がるかもしれない 基本的には上げない マイナーバージョン 後方互換なアップデート 基本は6週に1度のペースで上がる

    パッチバージョン 重要なバグの修正などで上がる
  10. リリースサイクル (6週間ごと) nightly beta 1.28 beta 1.29 beta 1.30 beta

    1.31 stable 1.30 stable 1.29 stable 1.28 2018/06/21 2018/08/02 2018/09/13 2018/10/25 2018/12/06
  11. リリースサイクル (6週間ごと) nightly beta 1.28 beta 1.29 beta 1.30 beta

    1.31 stable 1.30 stable 1.29 stable 1.28 2018/06/21 2018/08/02 2018/09/13 2018/10/25 2018/12/06 最新版かつ、全ての機能が使える。 バグが入ることもあるし、破壊的変更が入ることもある
  12. リリースサイクル (6週間ごと) nightly beta 1.28 beta 1.29 beta 1.30 beta

    1.31 stable 1.30 stable 1.29 stable 1.28 2018/06/21 2018/08/02 2018/09/13 2018/10/25 2018/12/06 安定化する予定の機能だけに制限される。 この期間で、stableからのリグレッションや、新機能のバグを直す。
  13. リリースサイクル (6週間ごと) nightly beta 1.28 beta 1.29 beta 1.30 beta

    1.31 stable 1.30 stable 1.29 stable 1.28 2018/06/21 2018/08/02 2018/09/13 2018/10/25 2018/12/06 betaがstableに昇格する。 重大なバグやリグレッションがあったときだけアップデートされる。
  14. エディション ≠ バージョン 1.30 Rust2015 モード 1.31 Rust2018 モード Rust2015

    モード 1.32 Rust2018 モード Rust2015 モード
  15. エディション ≠ バージョン 1.30 Rust2015 モード 1.31 Rust2018 モード Rust2015

    モード 1.32 Rust2018 モード Rust2015 モード コンパイラバージョン間には互換性がある。 つまり、同じソースを次のバージョンのコンパイラでもコンパイルできる。
  16. エディション ≠ バージョン 1.30 Rust2015 モード 1.31 Rust2018 モード Rust2015

    モード 1.32 Rust2018 モード Rust2015 モード エディション間には相互運用性がある。 つまり、同じコンパイラで異なるエディションのソースをコンパイルし、 リンクすることができる。
  17. 相互運用性 • エディションの垣根を越えて依存できる [package] name = "a" version = "0.1.0"

    edition = "2015" [package] name = "b" version = "0.1.0" edition = "2018" [dependencies] a = "0.1.0" [package] name = "c" version = "0.1.0" edition = "2015" [dependencies] b = "0.1.0"
  18. ボーナス機能 • 厳密には非互換性ではないが、Rust2018だけで使える機能や、 Rust2018で本格的に導入される新しいイディオムもある • NLL – より正確なライフタイム推論 (Rust2018で先に有効化される) •

    extern crate が不要に (Rust2018のみ) • dyn Trait によるトレイトオブジェクトの明示 (Rust2018イディオム) • Iter<'_> のようなライフタイム出現箇所の明示 (Rust2018イディオム) • mod.rs が不要に (Rust2018のみ)
  19. Rust2018の主な非互換性

  20. 非互換性(1) キーワード • 文脈キーワードからキーワードに昇格 ※これにあわせて、以下の4つのワードがキーワードから識別子に降格された(Rust2015/2018共通): alignof offsetof pure sizeof async

    await dyn try
  21. 非互換性(1) キーワード • 文脈キーワードからキーワードに昇格 async await dyn try 文脈キーワード: 識別子と紛らわしくないときだけ使えるキーワード。

    ※これにあわせて、以下の4つのワードがキーワードから識別子に降格された(Rust2015/2018共通): alignof offsetof pure sizeof
  22. 非互換性(1) キーワード • Rust2015 async fn main2() -> Result<(), ()>

    { ... await!(expr) ... } fn main() { tokio::run(main2().boxed().compat()); }
  23. 非互換性(1) キーワード • Rust2018 fn main() { tokio::run(async { ...

    await expr ... }.boxed().compat()); } asyncブロックが使えるようになる await式が使えるようになる async {} はRust2015では async という名前の構造体として解釈される。 await(expr) はRust2015では await という名前の関数呼び出しとして解釈される。
  24. 非互換性(1) キーワード • Rust2015 -> Result<(), dyn (::std::error::Error)> 構文のコーナーケース回避のために()が必要 dyn

    ::std::error::Error はRust2015では dyn というモジュールとして解釈される
  25. 非互換性(1) キーワード • Rust2018 -> Result<(), dyn (::std::error::Error)> コーナーケースがなくなってわかりやすくなった

  26. 非互換性(1) キーワード • Rust2018 let res = try { let

    f = File::open("/proc/cpuinfo")?; }; try構文が使えるようになる try {} はRust2015では try という構造体として解釈される
  27. 非互換性(2) モジュール (1/4) • extern crate が不要になった extern crate serde_json;

    fn main() { … serde_json::from_str … } fn main() { … serde_json::from_str … } ※これ自体は非互換な変更ではない
  28. 非互換性(2) モジュール (2/4) • #[macro_use] extern crate が不要になった #[macro_use] extern

    crate failure; #[derive(Fail)] struct Foo { … } use failure::Fail; #[derive(Fail)] struct Foo { … } ※これ自体は非互換な変更ではない
  29. 非互換性(2) モジュール (3/4) • 旧形式の絶対パスは非推奨になった ::std::mem::replace(…) ::my_module::foo(…) std::mem::replace(…) crate::my_module::foo(…) ※これ自体は非互換な変更ではない

  30. 非互換性(2) モジュール (4/4) • use 内でのパスの解釈が変更された use std::sync::Mutex; use my_mod::Foo;

    use std::sync::Mutex; use crate::my_mod::Foo; ※下は非互換な変更
  31. 非互換性(2) モジュール • Rust2015 use または pub(in) それ以外 abc::def 絶対パス

    相対パス ローカル変数など self::abc::def super::abc::def 相対パス ::abc::def 絶対パス crate::abc::def 絶対パス Rust2018との 互換性のために存在
  32. 非互換性(2) モジュール • Rust2018 use または pub(in) それ以外 abc::def (相対パス)

    外部crate 相対パス 外部crate ローカル変数など self::abc::def super::abc::def 相対パス ::abc::def 絶対パス 外部crate crate::abc::def 絶対パス Rust2015との 互換性のために存在
  33. 非互換性(3) 匿名引数 • 中身のない関数宣言でもダミーの引数名が必須に trait Foo { fn foo(u8); }

    trait Foo { fn foo(_: u8); }
  34. 非互換性(3) 匿名引数 • 構文的な一貫性が保証されるようになる fn foo((x, y): (i32, i32)) {}

    trait Foo { fn foo((x, y): (i32, i32)) {} } Rust2015/2018で使用可能 Rust2018で使用可能
  35. Rust2018を使ってみよう

  36. Rust2018の始め方1 1. 最新版コンパイラにアップデートします。 2. cargo new を実行します。 3. Rust2018が有効な状態でスタートできま す。

  37. Rust2018の始め方2 • 既存のライブラリを2段階マイグレートする Rust2015対応コード Rust2018対応コード cargo fix --edition cargo fix

    --edition-idioms
  38. 例: ripgrep • ダウンロードして実行 $ git clone https://github.com/BurntSushi/ripgrep.git $ cd

    ripgrep $ cargo build $ cargo run '¥d{20,}' デバッグビルドなので速くはない
  39. 例: ripgrep • Rust2018対応コードに変換 $ rm globset/benches/bench.rs $ cargo fix

    --edition --all --allow-dirty $ cargo test --all ベンチマークはnightlyでしかビルドできないので一旦外す。 これによりdirty treeになるので --allow-dirty が必要になる。 (練習なので削除してコミットしてしまってもよい)
  40. 例: ripgrep • Rust2018を有効化する $ sed -i.bak '4iedition = "2018"'

    $(find . -name Cargo.toml) $ rm $(find . -name Cargo.toml.bak) $ cargo test --all # fail
  41. 例: ripgrep • Rust2018を有効化する $ sed -i.bak '4iedition = "2018"'

    $(find . -name Cargo.toml) $ rm $(find . -name Cargo.toml.bak) $ cargo test --all # fail 「全部3行目に挿入すればいいや」というズボラ処理。 要するに以下のような行が入ればよい edition = "2018"
  42. 例: ripgrep • 残念ながら上手く変換できていない場所があるので手動で直す • src/args.rs • use log; が通らないので

    use crate::log; にする。 • src/search.rs • json! が解決できないので use serde_json::json; を加える。
  43. 例: ripgrep • Rust2018イディオムに適合させる $ cargo fix --edition-idioms –all --allow-dirty

    $ cargo test --all 残念ながらうまく適用できないfixがあるので、手動で適用してあげ るとよい。
  44. 例: ripgrep • Rust2018対応完了! • さらに追加でできること • extern crate を自動で削除してくれるが、行番号が変わらないよう

    に空行を残すので、手動で削除してあげる • #[macro_use] extern crate は自動では変換できないので、うま く対応してあげる
  45. RustにおけるCLI

  46. RustはCLIに向いている? • GolangがCLIに向いているのと同じような理由で、RustもCLI には比較的向いているはず • 容易なクロスコンパイル、Windowsの第一級サポート • シングルバイナリ • ただ、CLI用途での非同期I/Oはまだ未開拓感がある

    • 正直、この辺の機微はあんまり詳しくないので、書いてみて判 断してみてほしい • Awesome Rustとか一瞥してみるといいかも https://github.com/rust-unofficial/awesome-rust
  47. watchを作ろう

  48. watchとは • コマンドを定期的に実行して、最新状態を表示してくれる • watch ls とか watch date のように使う

  49. プロジェクト作成 $ cargo new ferris_watch $ cd ferris_watch

  50. 依存関係 • Cargo.tomlはこんな感じにする [dependencies] log = "0.4.6" env_logger = "0.6.0"

    failure = "0.1.3" signal-hook = "0.1.6" clap = "2.32.0" pancurses = "0.16.0"
  51. ロギング • src/main.rs use log::debug; fn main() { env_logger::init(); debug!("ferris_watch

    starting..."); } RUST_LOG=ferris_watch=debug cargo run と実行して、デバッグ出力がでる ことを確認
  52. エラー処理 • 面倒なので全部failure::Errorに回収させる fn main() -> Result<(), failure::Error> { …

    Ok(()) } こうしておくと main の中で ? が好き勝手使えて便利
  53. コマンド引数(1) • パーサーを起動する use clap::App; … let matches = App::new("ferris_watch")

    .version("0.1.0") .author("Your Name <yourname@example.com>") .about("cute watch command") .get_matches(); cargo run -- --help と実行して、ヘルプがでることを確認 ※よりベーシックな機能に絞った getopts 、 clap とは逆にヘルプからパーサーを作る docopts 、 clap の 結果を構造体マップできるようにした structopt などのライブラリもある。
  54. コマンド引数(2) • 実行するコマンドを指定する引数 use clap::{App, Arg}; … .arg( Arg::with_name("command") .required(true)

    .multiple(true) .help("The command to run periodically"), ) cargo run -- --help と実行して、ヘルプがでることを確認 .get_matches() の直前に追加
  55. コマンド引数(3) • 実行する間隔を指定する .arg( Arg::with_name("interval") .long("interval") .short("n") .takes_value(true) .default_value("2.0") .help("The

    period to run a command"), ) cargo run -- --help と実行して、ヘルプがでることを確認 .get_matches() の直前に追加
  56. コマンド引数(4) • 引数を取り出す use clap::{App, Arg, value_t}; … let command

    = matches.values_of("command").unwrap().collect::<Vec<_>>(); let interval = value_t!(matches, "interval", f64)?; debug!("command = {:?}", command); debug!("interval = {:?}", interval); RUST_LOG=ferris_watch cargo run -- -n 0.5 -- ls -a などを実行してみよう
  57. 補完スクリプトの生成 • clapを使うと補完スクリプトの生成もできる(らしい) • 今回はout of scope • See→ https://clap.rs/2016/10/25/an-update/

  58. コマンドを実行する • 実行する use std::process::Command; … let output = Command::new(command[0]).args(&command[1..]).output()?;

    debug!("output = {:?}", output); let output = String::from_utf8_lossy(&output.stdout); println!("{}", output); cargo run -- -- ls -a などを実行してみよう 実行して出力の取得までやってくれるヘルパー関数。 もちろん、もっと複雑な操作もできる
  59. コマンドを実行する • 実行する use std::process::Command; … let output = Command::new(command[0]).args(&command[1..]).output()?;

    debug!("output = {:?}", output); let output = String::from_utf8_lossy(&output.stdout); println!("{}", output); cargo run -- -- ls -a などを実行してみよう この場合 • 実行に失敗したら終了 • コマンドのステータスは無視 という挙動になる
  60. 休眠 • コマンド実行後に指定秒数休眠する use std::thread::sleep; use std::time::Duration; … let interval10

    = (interval * 10.0) as u32; … for _ in 0..interval10 { sleep(Duration::from_millis(100)); } あとで書く処理の都合上、定期的に起 きるようにする
  61. 無限ループ • 休眠しつつ無限に実行するようにする loop { // … コマンドを実行する処理 … //

    … 休眠する処理 … } Ok(()) // 到達しないという警告が出るが、いったん残しておく あとで書く処理の都合上、定期的に起 きるようにする
  62. graceful shutdown • Ctrl-Cで自動で死なないようにする use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; let

    interrupted = Arc::new(AtomicBool::new(false)); signal_hook::flag::register( signal_hook::SIGINT, interrupted.clone())?; let interrupted = || interrupted.load(Ordering::SeqCst); この状態でうっかり実行してしまった場合、Ctrl-Cでは止められないので SIGTERM (killコマンドのデフォルト) などで止めてあげる
  63. graceful shutdown • Ctrl-Cで自動で死なないようにする use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; let

    interrupted = Arc::new(AtomicBool::new(false)); signal_hook::flag::register( signal_hook::SIGINT, interrupted.clone())?; let interrupted = || interrupted.load(Ordering::SeqCst); この状態でうっかり実行してしまった場合、Ctrl-Cでは止められないので SIGTERM (killコマンドのデフォルト) などで止めてあげる SIGINTで止まらずにフラグだけ立てる
  64. graceful shutdown • かわりに、Ctrl-Cを見張って自分で脱出する 'outer: loop { for … {

    // … 休眠処理 … if interrupted() { break 'outer; } } } forが1回も実行されなかった場合に備えているとなおよい
  65. graceful shutdown • かわりに、Ctrl-Cを見張って自分で脱出する 'outer: loop { … } debug!("end");

    Ok(()) endが表示されることを確認しよう
  66. Cursesを使う • POSIX系の環境ではncurseswを使ったほうがよい $ sudo apt install libncursesw5-dev

  67. Cursesを使う • POSIX系の環境ではncurseswを使ったほうがよい [dependencies] pancurses = { version = "0.16.0",

    features = ["wide"] }
  68. Cursesを使う • ウインドウを立ち上げる let window = pancurses::initscr(); struct EndWin; impl

    Drop for EndWin { fn drop(&mut self) { pancurses::endwin(); } } let _endwin = EndWin; 確実に実行してほしいので drop で実行する (scopeguard や defer クレートを使う手もある)
  69. Cursesを使う • println! のかわりに printw で出力する // println!("{}", output); window.clear();

    window.printw(output); window.refresh(); cargo run date などを試してみよう
  70. 画面系ライブラリのサーベイ • CursesをもとにしたよりリッチなTUIライブラリとしては Cursive や tui-rs がある • readline (REPL的な入力機能)

    だけ必要なら rustyline が良さそ う
  71. 設定ファイル • ネタ的に今回は扱わない • パッと調べた感じ、 config-rs というのがよくできてそう https://github.com/mehcode/config-rs

  72. CI buildをしよう

  73. 手元でクロスコンパイルをしてみる • クロスコンパイルに必要なもの • ターゲットアーキテクチャ用の標準ライブラリ • ターゲットアーキテクチャ用の外部ライブラリ • ターゲットアーキテクチャ用のリンカ (gcc)

    • 通常のコンパイラに他アーキテクチャ向けのバックエンドも同 梱されているので、コンパイラは不要 • メタビルド系の処理もcargoがいい感じにハンドルしてくれる
  74. 標準ライブラリをGet • 自分のアーキテクチャの32bit版で試すのがオススメ $ rustup target list $ rustup target

    add i686-unknown-linux-gnu
  75. ターゲットを指定してビルド • 以下のようなコマンドを叩くだけ! (だいたいリンクで失敗する) $ cargo build --target i686-unknown-linux-gnu Ubuntuの場合は以下が参考になる:

    https://askubuntu.com/questions/522372/installing-32-bit- libraries-on-ubuntu-14-04-lts-64-bit
  76. Travisでツールをリリースする • GitHubのリポジトリを立てる • ferris_watchをpush • Travisを有効化する

  77. Travisでツールをリリースする • こんな感じで.travis.ymlを書く • https://github.com/qnighy/ferris_watch/blob/63adc870937351aead ea7ae8f77804877cbf2d14/.travis.yml • TravisはWindowsをベータサポートしているので、3つのOSで ビルドできる •

    さすがに同じOSでビルドしたほうが楽 • クロスコンパイルで32bit用バイナリは生成できる • ……という話をする予定だったが、ncursesとの相性が異様に 悪くてmuslのコンパイルが通らない……
  78. Travisでツールをリリースする • 自動でデプロイするには https://docs.travis- ci.com/user/deployment/releases を参考にしてシークレット を設定する必要がある • travis setup

    releases を実行していい感じに編集するの が楽
  79. やろうとしたけど準備できなかったこと • reqwestを使った自動アップデート

  80. まとめ • コマンドラインツールを一通り作ってみた • それなりにまとまったライブラリ群が用意されており、Rustの 中では比較的とっつきやすい • Rustで書く利点もある