Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

先にやっておくこと • Rust2018に対応した最新版の入手 $ rustup update $ rustc –version rustc 1.31.0 (abe02cefd 2018-12-04)

Slide 4

Slide 4 text

先にやっておくビルド(1) • ripgrepのビルド $ git clone https://github.com/BurntSushi/ripgrep.git $ cd ripgrep $ cargo build

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

先にやっておくビルド(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"

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

リリースサイクル (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

Slide 11

Slide 11 text

リリースサイクル (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 最新版かつ、全ての機能が使える。 バグが入ることもあるし、破壊的変更が入ることもある

Slide 12

Slide 12 text

リリースサイクル (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からのリグレッションや、新機能のバグを直す。

Slide 13

Slide 13 text

リリースサイクル (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に昇格する。 重大なバグやリグレッションがあったときだけアップデートされる。

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

エディション ≠ バージョン 1.30 Rust2015 モード 1.31 Rust2018 モード Rust2015 モード 1.32 Rust2018 モード Rust2015 モード エディション間には相互運用性がある。 つまり、同じコンパイラで異なるエディションのソースをコンパイルし、 リンクすることができる。

Slide 17

Slide 17 text

相互運用性 • エディションの垣根を越えて依存できる [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"

Slide 18

Slide 18 text

ボーナス機能 • 厳密には非互換性ではないが、Rust2018だけで使える機能や、 Rust2018で本格的に導入される新しいイディオムもある • NLL – より正確なライフタイム推論 (Rust2018で先に有効化される) • extern crate が不要に (Rust2018のみ) • dyn Trait によるトレイトオブジェクトの明示 (Rust2018イディオム) • Iter<'_> のようなライフタイム出現箇所の明示 (Rust2018イディオム) • mod.rs が不要に (Rust2018のみ)

Slide 19

Slide 19 text

Rust2018の主な非互換性

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

非互換性(1) キーワード • 文脈キーワードからキーワードに昇格 async await dyn try 文脈キーワード: 識別子と紛らわしくないときだけ使えるキーワード。 ※これにあわせて、以下の4つのワードがキーワードから識別子に降格された(Rust2015/2018共通): alignof offsetof pure sizeof

Slide 22

Slide 22 text

非互換性(1) キーワード • Rust2015 async fn main2() -> Result<(), ()> { ... await!(expr) ... } fn main() { tokio::run(main2().boxed().compat()); }

Slide 23

Slide 23 text

非互換性(1) キーワード • Rust2018 fn main() { tokio::run(async { ... await expr ... }.boxed().compat()); } asyncブロックが使えるようになる await式が使えるようになる async {} はRust2015では async という名前の構造体として解釈される。 await(expr) はRust2015では await という名前の関数呼び出しとして解釈される。

Slide 24

Slide 24 text

非互換性(1) キーワード • Rust2015 -> Result<(), dyn (::std::error::Error)> 構文のコーナーケース回避のために()が必要 dyn ::std::error::Error はRust2015では dyn というモジュールとして解釈される

Slide 25

Slide 25 text

非互換性(1) キーワード • Rust2018 -> Result<(), dyn (::std::error::Error)> コーナーケースがなくなってわかりやすくなった

Slide 26

Slide 26 text

非互換性(1) キーワード • Rust2018 let res = try { let f = File::open("/proc/cpuinfo")?; }; try構文が使えるようになる try {} はRust2015では try という構造体として解釈される

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

非互換性(2) モジュール (2/4) • #[macro_use] extern crate が不要になった #[macro_use] extern crate failure; #[derive(Fail)] struct Foo { … } use failure::Fail; #[derive(Fail)] struct Foo { … } ※これ自体は非互換な変更ではない

Slide 29

Slide 29 text

非互換性(2) モジュール (3/4) • 旧形式の絶対パスは非推奨になった ::std::mem::replace(…) ::my_module::foo(…) std::mem::replace(…) crate::my_module::foo(…) ※これ自体は非互換な変更ではない

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

非互換性(3) 匿名引数 • 中身のない関数宣言でもダミーの引数名が必須に trait Foo { fn foo(u8); } trait Foo { fn foo(_: u8); }

Slide 34

Slide 34 text

非互換性(3) 匿名引数 • 構文的な一貫性が保証されるようになる fn foo((x, y): (i32, i32)) {} trait Foo { fn foo((x, y): (i32, i32)) {} } Rust2015/2018で使用可能 Rust2018で使用可能

Slide 35

Slide 35 text

Rust2018を使ってみよう

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

例: ripgrep • ダウンロードして実行 $ git clone https://github.com/BurntSushi/ripgrep.git $ cd ripgrep $ cargo build $ cargo run '¥d{20,}' デバッグビルドなので速くはない

Slide 39

Slide 39 text

例: ripgrep • Rust2018対応コードに変換 $ rm globset/benches/bench.rs $ cargo fix --edition --all --allow-dirty $ cargo test --all ベンチマークはnightlyでしかビルドできないので一旦外す。 これによりdirty treeになるので --allow-dirty が必要になる。 (練習なので削除してコミットしてしまってもよい)

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

例: ripgrep • Rust2018を有効化する $ sed -i.bak '4iedition = "2018"' $(find . -name Cargo.toml) $ rm $(find . -name Cargo.toml.bak) $ cargo test --all # fail 「全部3行目に挿入すればいいや」というズボラ処理。 要するに以下のような行が入ればよい edition = "2018"

Slide 42

Slide 42 text

例: ripgrep • 残念ながら上手く変換できていない場所があるので手動で直す • src/args.rs • use log; が通らないので use crate::log; にする。 • src/search.rs • json! が解決できないので use serde_json::json; を加える。

Slide 43

Slide 43 text

例: ripgrep • Rust2018イディオムに適合させる $ cargo fix --edition-idioms –all --allow-dirty $ cargo test --all 残念ながらうまく適用できないfixがあるので、手動で適用してあげ るとよい。

Slide 44

Slide 44 text

例: ripgrep • Rust2018対応完了! • さらに追加でできること • extern crate を自動で削除してくれるが、行番号が変わらないよう に空行を残すので、手動で削除してあげる • #[macro_use] extern crate は自動では変換できないので、うま く対応してあげる

Slide 45

Slide 45 text

RustにおけるCLI

Slide 46

Slide 46 text

RustはCLIに向いている? • GolangがCLIに向いているのと同じような理由で、RustもCLI には比較的向いているはず • 容易なクロスコンパイル、Windowsの第一級サポート • シングルバイナリ • ただ、CLI用途での非同期I/Oはまだ未開拓感がある • 正直、この辺の機微はあんまり詳しくないので、書いてみて判 断してみてほしい • Awesome Rustとか一瞥してみるといいかも https://github.com/rust-unofficial/awesome-rust

Slide 47

Slide 47 text

watchを作ろう

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

依存関係 • 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"

Slide 51

Slide 51 text

ロギング • src/main.rs use log::debug; fn main() { env_logger::init(); debug!("ferris_watch starting..."); } RUST_LOG=ferris_watch=debug cargo run と実行して、デバッグ出力がでる ことを確認

Slide 52

Slide 52 text

エラー処理 • 面倒なので全部failure::Errorに回収させる fn main() -> Result<(), failure::Error> { … Ok(()) } こうしておくと main の中で ? が好き勝手使えて便利

Slide 53

Slide 53 text

コマンド引数(1) • パーサーを起動する use clap::App; … let matches = App::new("ferris_watch") .version("0.1.0") .author("Your Name ") .about("cute watch command") .get_matches(); cargo run -- --help と実行して、ヘルプがでることを確認 ※よりベーシックな機能に絞った getopts 、 clap とは逆にヘルプからパーサーを作る docopts 、 clap の 結果を構造体マップできるようにした structopt などのライブラリもある。

Slide 54

Slide 54 text

コマンド引数(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() の直前に追加

Slide 55

Slide 55 text

コマンド引数(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() の直前に追加

Slide 56

Slide 56 text

コマンド引数(4) • 引数を取り出す use clap::{App, Arg, value_t}; … let command = matches.values_of("command").unwrap().collect::>(); let interval = value_t!(matches, "interval", f64)?; debug!("command = {:?}", command); debug!("interval = {:?}", interval); RUST_LOG=ferris_watch cargo run -- -n 0.5 -- ls -a などを実行してみよう

Slide 57

Slide 57 text

補完スクリプトの生成 • clapを使うと補完スクリプトの生成もできる(らしい) • 今回はout of scope • See→ https://clap.rs/2016/10/25/an-update/

Slide 58

Slide 58 text

コマンドを実行する • 実行する 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 などを実行してみよう 実行して出力の取得までやってくれるヘルパー関数。 もちろん、もっと複雑な操作もできる

Slide 59

Slide 59 text

コマンドを実行する • 実行する 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 などを実行してみよう この場合 • 実行に失敗したら終了 • コマンドのステータスは無視 という挙動になる

Slide 60

Slide 60 text

休眠 • コマンド実行後に指定秒数休眠する use std::thread::sleep; use std::time::Duration; … let interval10 = (interval * 10.0) as u32; … for _ in 0..interval10 { sleep(Duration::from_millis(100)); } あとで書く処理の都合上、定期的に起 きるようにする

Slide 61

Slide 61 text

無限ループ • 休眠しつつ無限に実行するようにする loop { // … コマンドを実行する処理 … // … 休眠する処理 … } Ok(()) // 到達しないという警告が出るが、いったん残しておく あとで書く処理の都合上、定期的に起 きるようにする

Slide 62

Slide 62 text

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コマンドのデフォルト) などで止めてあげる

Slide 63

Slide 63 text

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で止まらずにフラグだけ立てる

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

graceful shutdown • かわりに、Ctrl-Cを見張って自分で脱出する 'outer: loop { … } debug!("end"); Ok(()) endが表示されることを確認しよう

Slide 66

Slide 66 text

Cursesを使う • POSIX系の環境ではncurseswを使ったほうがよい $ sudo apt install libncursesw5-dev

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Cursesを使う • ウインドウを立ち上げる let window = pancurses::initscr(); struct EndWin; impl Drop for EndWin { fn drop(&mut self) { pancurses::endwin(); } } let _endwin = EndWin; 確実に実行してほしいので drop で実行する (scopeguard や defer クレートを使う手もある)

Slide 69

Slide 69 text

Cursesを使う • println! のかわりに printw で出力する // println!("{}", output); window.clear(); window.printw(output); window.refresh(); cargo run date などを試してみよう

Slide 70

Slide 70 text

画面系ライブラリのサーベイ • CursesをもとにしたよりリッチなTUIライブラリとしては Cursive や tui-rs がある • readline (REPL的な入力機能) だけ必要なら rustyline が良さそ う

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

CI buildをしよう

Slide 73

Slide 73 text

手元でクロスコンパイルをしてみる • クロスコンパイルに必要なもの • ターゲットアーキテクチャ用の標準ライブラリ • ターゲットアーキテクチャ用の外部ライブラリ • ターゲットアーキテクチャ用のリンカ (gcc) • 通常のコンパイラに他アーキテクチャ向けのバックエンドも同 梱されているので、コンパイラは不要 • メタビルド系の処理もcargoがいい感じにハンドルしてくれる

Slide 74

Slide 74 text

標準ライブラリをGet • 自分のアーキテクチャの32bit版で試すのがオススメ $ rustup target list $ rustup target add i686-unknown-linux-gnu

Slide 75

Slide 75 text

ターゲットを指定してビルド • 以下のようなコマンドを叩くだけ! (だいたいリンクで失敗する) $ cargo build --target i686-unknown-linux-gnu Ubuntuの場合は以下が参考になる: https://askubuntu.com/questions/522372/installing-32-bit- libraries-on-ubuntu-14-04-lts-64-bit

Slide 76

Slide 76 text

Travisでツールをリリースする • GitHubのリポジトリを立てる • ferris_watchをpush • Travisを有効化する

Slide 77

Slide 77 text

Travisでツールをリリースする • こんな感じで.travis.ymlを書く • https://github.com/qnighy/ferris_watch/blob/63adc870937351aead ea7ae8f77804877cbf2d14/.travis.yml • TravisはWindowsをベータサポートしているので、3つのOSで ビルドできる • さすがに同じOSでビルドしたほうが楽 • クロスコンパイルで32bit用バイナリは生成できる • ……という話をする予定だったが、ncursesとの相性が異様に 悪くてmuslのコンパイルが通らない……

Slide 78

Slide 78 text

Travisでツールをリリースする • 自動でデプロイするには https://docs.travis- ci.com/user/deployment/releases を参考にしてシークレット を設定する必要がある • travis setup releases を実行していい感じに編集するの が楽

Slide 79

Slide 79 text

やろうとしたけど準備できなかったこと • reqwestを使った自動アップデート

Slide 80

Slide 80 text

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