Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Rust速習会4
Search
Masaki Hara
December 13, 2018
Programming
4
2k
Rust速習会4
Masaki Hara
December 13, 2018
Tweet
Share
More Decks by Masaki Hara
See All by Masaki Hara
Arm移行タイムアタック
qnighy
0
320
Quine, Polyglot, 良いコード
qnighy
4
640
Prolog入門
qnighy
5
1.3k
Rubyのobject_id
qnighy
6
1.5k
Getting along with YAML comments with Psych
qnighy
2
2.1k
状態設計から「なんとなく」を無くそう
qnighy
84
27k
日付時刻A to Z
qnighy
1
580
Hands-on Native ESM @ JSConf JP 2022
qnighy
0
5.7k
computed_modelの紹介 / Introducing computed_model (2)
qnighy
0
590
Other Decks in Programming
See All in Programming
Contemporary Test Cases
maaretp
0
130
AI時代におけるSRE、 あるいはエンジニアの生存戦略
pyama86
6
1.1k
3 Effective Rules for Using Signals in Angular
manfredsteyer
PRO
1
100
subpath importsで始めるモック生活
10tera
0
300
RubyLSPのマルチバイト文字対応
notfounds
0
120
受け取る人から提供する人になるということ
little_rubyist
0
230
Jakarta EE meets AI
ivargrimstad
0
530
macOS でできる リアルタイム動画像処理
biacco42
9
2.4k
Laravel や Symfony で手っ取り早く OpenAPI のドキュメントを作成する
azuki
2
120
Kaigi on Rails 2024 〜運営の裏側〜
krpk1900
1
200
【Kaigi on Rails 2024】YOUTRUST スポンサーLT
krpk1900
1
330
Why Jakarta EE Matters to Spring - and Vice Versa
ivargrimstad
0
1.1k
Featured
See All Featured
Side Projects
sachag
452
42k
Building Your Own Lightsaber
phodgson
103
6.1k
Building an army of robots
kneath
302
43k
Design and Strategy: How to Deal with People Who Don’t "Get" Design
morganepeng
126
18k
The Straight Up "How To Draw Better" Workshop
denniskardys
232
140k
Facilitating Awesome Meetings
lara
50
6.1k
How to Think Like a Performance Engineer
csswizardry
20
1.1k
Building Adaptive Systems
keathley
38
2.3k
Making the Leap to Tech Lead
cromwellryan
133
8.9k
Happy Clients
brianwarren
98
6.7k
Large-scale JavaScript Application Architecture
addyosmani
510
110k
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
169
50k
Transcript
Rust速習会(4) コマンドラインツール 2018-12-13 @Wantedlyオフィス (白金台) 原 将己 (@qnighy) 実況用ハッシュタグ: #rust_jp
今日の予定 • Rust2018の話 • コマンドラインツールを書いてみよう
先にやっておくこと • Rust2018に対応した最新版の入手 $ rustup update $ rustc –version rustc
1.31.0 (abe02cefd 2018-12-04)
先にやっておくビルド(1) • ripgrepのビルド $ git clone https://github.com/BurntSushi/ripgrep.git $ cd ripgrep
$ cargo build
先にやっておくビルド(2) • 以下のプロジェクトの作成 $ cargo new ferris_watch $ cd ferris_watch
# Cargo.tomlを編集 $ cargo build
先にやっておくビルド(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"
突然ですが、Rust2018の紹介 をしたいと思います。なぜかというと12/06の1.31.0リリースにより Rust2018が心おきなく使えるようになっただけではなく、これがデフォル トになっていくからです。非互換なのに相互運用可能なのはどういうこと なのかをちゃんと知っておいたほうがいいと思うので説明します。
Rust2018とは? •既存のエコシステムとの 相互運用性 を保ちつつ、言語に 非互換な変更 を入れ、より使いやすい言語へと進化させ る仕組み ……つまりどういうことか、以下解説します
Rustのコンパイラバージョン • Rustのコンパイラバージョンはsemver 1.31.0 メジャーバージョン 遠い将来に上がるかもしれない 基本的には上げない マイナーバージョン 後方互換なアップデート 基本は6週に1度のペースで上がる
パッチバージョン 重要なバグの修正などで上がる
リリースサイクル (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
リリースサイクル (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 最新版かつ、全ての機能が使える。 バグが入ることもあるし、破壊的変更が入ることもある
リリースサイクル (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からのリグレッションや、新機能のバグを直す。
リリースサイクル (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に昇格する。 重大なバグやリグレッションがあったときだけアップデートされる。
エディション ≠ バージョン 1.30 Rust2015 モード 1.31 Rust2018 モード Rust2015
モード 1.32 Rust2018 モード Rust2015 モード
エディション ≠ バージョン 1.30 Rust2015 モード 1.31 Rust2018 モード Rust2015
モード 1.32 Rust2018 モード Rust2015 モード コンパイラバージョン間には互換性がある。 つまり、同じソースを次のバージョンのコンパイラでもコンパイルできる。
エディション ≠ バージョン 1.30 Rust2015 モード 1.31 Rust2018 モード Rust2015
モード 1.32 Rust2018 モード Rust2015 モード エディション間には相互運用性がある。 つまり、同じコンパイラで異なるエディションのソースをコンパイルし、 リンクすることができる。
相互運用性 • エディションの垣根を越えて依存できる [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"
ボーナス機能 • 厳密には非互換性ではないが、Rust2018だけで使える機能や、 Rust2018で本格的に導入される新しいイディオムもある • NLL – より正確なライフタイム推論 (Rust2018で先に有効化される) •
extern crate が不要に (Rust2018のみ) • dyn Trait によるトレイトオブジェクトの明示 (Rust2018イディオム) • Iter<'_> のようなライフタイム出現箇所の明示 (Rust2018イディオム) • mod.rs が不要に (Rust2018のみ)
Rust2018の主な非互換性
非互換性(1) キーワード • 文脈キーワードからキーワードに昇格 ※これにあわせて、以下の4つのワードがキーワードから識別子に降格された(Rust2015/2018共通): alignof offsetof pure sizeof async
await dyn try
非互換性(1) キーワード • 文脈キーワードからキーワードに昇格 async await dyn try 文脈キーワード: 識別子と紛らわしくないときだけ使えるキーワード。
※これにあわせて、以下の4つのワードがキーワードから識別子に降格された(Rust2015/2018共通): alignof offsetof pure sizeof
非互換性(1) キーワード • Rust2015 async fn main2() -> Result<(), ()>
{ ... await!(expr) ... } fn main() { tokio::run(main2().boxed().compat()); }
非互換性(1) キーワード • Rust2018 fn main() { tokio::run(async { ...
await expr ... }.boxed().compat()); } asyncブロックが使えるようになる await式が使えるようになる async {} はRust2015では async という名前の構造体として解釈される。 await(expr) はRust2015では await という名前の関数呼び出しとして解釈される。
非互換性(1) キーワード • Rust2015 -> Result<(), dyn (::std::error::Error)> 構文のコーナーケース回避のために()が必要 dyn
::std::error::Error はRust2015では dyn というモジュールとして解釈される
非互換性(1) キーワード • Rust2018 -> Result<(), dyn (::std::error::Error)> コーナーケースがなくなってわかりやすくなった
非互換性(1) キーワード • Rust2018 let res = try { let
f = File::open("/proc/cpuinfo")?; }; try構文が使えるようになる try {} はRust2015では try という構造体として解釈される
非互換性(2) モジュール (1/4) • extern crate が不要になった extern crate serde_json;
fn main() { … serde_json::from_str … } fn main() { … serde_json::from_str … } ※これ自体は非互換な変更ではない
非互換性(2) モジュール (2/4) • #[macro_use] extern crate が不要になった #[macro_use] extern
crate failure; #[derive(Fail)] struct Foo { … } use failure::Fail; #[derive(Fail)] struct Foo { … } ※これ自体は非互換な変更ではない
非互換性(2) モジュール (3/4) • 旧形式の絶対パスは非推奨になった ::std::mem::replace(…) ::my_module::foo(…) std::mem::replace(…) crate::my_module::foo(…) ※これ自体は非互換な変更ではない
非互換性(2) モジュール (4/4) • use 内でのパスの解釈が変更された use std::sync::Mutex; use my_mod::Foo;
use std::sync::Mutex; use crate::my_mod::Foo; ※下は非互換な変更
非互換性(2) モジュール • Rust2015 use または pub(in) それ以外 abc::def 絶対パス
相対パス ローカル変数など self::abc::def super::abc::def 相対パス ::abc::def 絶対パス crate::abc::def 絶対パス Rust2018との 互換性のために存在
非互換性(2) モジュール • Rust2018 use または pub(in) それ以外 abc::def (相対パス)
外部crate 相対パス 外部crate ローカル変数など self::abc::def super::abc::def 相対パス ::abc::def 絶対パス 外部crate crate::abc::def 絶対パス Rust2015との 互換性のために存在
非互換性(3) 匿名引数 • 中身のない関数宣言でもダミーの引数名が必須に trait Foo { fn foo(u8); }
trait Foo { fn foo(_: u8); }
非互換性(3) 匿名引数 • 構文的な一貫性が保証されるようになる fn foo((x, y): (i32, i32)) {}
trait Foo { fn foo((x, y): (i32, i32)) {} } Rust2015/2018で使用可能 Rust2018で使用可能
Rust2018を使ってみよう
Rust2018の始め方1 1. 最新版コンパイラにアップデートします。 2. cargo new を実行します。 3. Rust2018が有効な状態でスタートできま す。
Rust2018の始め方2 • 既存のライブラリを2段階マイグレートする Rust2015対応コード Rust2018対応コード cargo fix --edition cargo fix
--edition-idioms
例: ripgrep • ダウンロードして実行 $ git clone https://github.com/BurntSushi/ripgrep.git $ cd
ripgrep $ cargo build $ cargo run '¥d{20,}' デバッグビルドなので速くはない
例: ripgrep • Rust2018対応コードに変換 $ rm globset/benches/bench.rs $ cargo fix
--edition --all --allow-dirty $ cargo test --all ベンチマークはnightlyでしかビルドできないので一旦外す。 これによりdirty treeになるので --allow-dirty が必要になる。 (練習なので削除してコミットしてしまってもよい)
例: ripgrep • Rust2018を有効化する $ sed -i.bak '4iedition = "2018"'
$(find . -name Cargo.toml) $ rm $(find . -name Cargo.toml.bak) $ cargo test --all # fail
例: ripgrep • Rust2018を有効化する $ sed -i.bak '4iedition = "2018"'
$(find . -name Cargo.toml) $ rm $(find . -name Cargo.toml.bak) $ cargo test --all # fail 「全部3行目に挿入すればいいや」というズボラ処理。 要するに以下のような行が入ればよい edition = "2018"
例: ripgrep • 残念ながら上手く変換できていない場所があるので手動で直す • src/args.rs • use log; が通らないので
use crate::log; にする。 • src/search.rs • json! が解決できないので use serde_json::json; を加える。
例: ripgrep • Rust2018イディオムに適合させる $ cargo fix --edition-idioms –all --allow-dirty
$ cargo test --all 残念ながらうまく適用できないfixがあるので、手動で適用してあげ るとよい。
例: ripgrep • Rust2018対応完了! • さらに追加でできること • extern crate を自動で削除してくれるが、行番号が変わらないよう
に空行を残すので、手動で削除してあげる • #[macro_use] extern crate は自動では変換できないので、うま く対応してあげる
RustにおけるCLI
RustはCLIに向いている? • GolangがCLIに向いているのと同じような理由で、RustもCLI には比較的向いているはず • 容易なクロスコンパイル、Windowsの第一級サポート • シングルバイナリ • ただ、CLI用途での非同期I/Oはまだ未開拓感がある
• 正直、この辺の機微はあんまり詳しくないので、書いてみて判 断してみてほしい • Awesome Rustとか一瞥してみるといいかも https://github.com/rust-unofficial/awesome-rust
watchを作ろう
watchとは • コマンドを定期的に実行して、最新状態を表示してくれる • watch ls とか watch date のように使う
プロジェクト作成 $ cargo new ferris_watch $ cd ferris_watch
依存関係 • 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"
ロギング • src/main.rs use log::debug; fn main() { env_logger::init(); debug!("ferris_watch
starting..."); } RUST_LOG=ferris_watch=debug cargo run と実行して、デバッグ出力がでる ことを確認
エラー処理 • 面倒なので全部failure::Errorに回収させる fn main() -> Result<(), failure::Error> { …
Ok(()) } こうしておくと main の中で ? が好き勝手使えて便利
コマンド引数(1) • パーサーを起動する use clap::App; … let matches = App::new("ferris_watch")
.version("0.1.0") .author("Your Name <
[email protected]
>") .about("cute watch command") .get_matches(); cargo run -- --help と実行して、ヘルプがでることを確認 ※よりベーシックな機能に絞った getopts 、 clap とは逆にヘルプからパーサーを作る docopts 、 clap の 結果を構造体マップできるようにした structopt などのライブラリもある。
コマンド引数(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() の直前に追加
コマンド引数(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() の直前に追加
コマンド引数(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 などを実行してみよう
補完スクリプトの生成 • clapを使うと補完スクリプトの生成もできる(らしい) • 今回はout of scope • See→ https://clap.rs/2016/10/25/an-update/
コマンドを実行する • 実行する 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 などを実行してみよう 実行して出力の取得までやってくれるヘルパー関数。 もちろん、もっと複雑な操作もできる
コマンドを実行する • 実行する 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 などを実行してみよう この場合 • 実行に失敗したら終了 • コマンドのステータスは無視 という挙動になる
休眠 • コマンド実行後に指定秒数休眠する use std::thread::sleep; use std::time::Duration; … let interval10
= (interval * 10.0) as u32; … for _ in 0..interval10 { sleep(Duration::from_millis(100)); } あとで書く処理の都合上、定期的に起 きるようにする
無限ループ • 休眠しつつ無限に実行するようにする loop { // … コマンドを実行する処理 … //
… 休眠する処理 … } Ok(()) // 到達しないという警告が出るが、いったん残しておく あとで書く処理の都合上、定期的に起 きるようにする
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コマンドのデフォルト) などで止めてあげる
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で止まらずにフラグだけ立てる
graceful shutdown • かわりに、Ctrl-Cを見張って自分で脱出する 'outer: loop { for … {
// … 休眠処理 … if interrupted() { break 'outer; } } } forが1回も実行されなかった場合に備えているとなおよい
graceful shutdown • かわりに、Ctrl-Cを見張って自分で脱出する 'outer: loop { … } debug!("end");
Ok(()) endが表示されることを確認しよう
Cursesを使う • POSIX系の環境ではncurseswを使ったほうがよい $ sudo apt install libncursesw5-dev
Cursesを使う • POSIX系の環境ではncurseswを使ったほうがよい [dependencies] pancurses = { version = "0.16.0",
features = ["wide"] }
Cursesを使う • ウインドウを立ち上げる let window = pancurses::initscr(); struct EndWin; impl
Drop for EndWin { fn drop(&mut self) { pancurses::endwin(); } } let _endwin = EndWin; 確実に実行してほしいので drop で実行する (scopeguard や defer クレートを使う手もある)
Cursesを使う • println! のかわりに printw で出力する // println!("{}", output); window.clear();
window.printw(output); window.refresh(); cargo run date などを試してみよう
画面系ライブラリのサーベイ • CursesをもとにしたよりリッチなTUIライブラリとしては Cursive や tui-rs がある • readline (REPL的な入力機能)
だけ必要なら rustyline が良さそ う
設定ファイル • ネタ的に今回は扱わない • パッと調べた感じ、 config-rs というのがよくできてそう https://github.com/mehcode/config-rs
CI buildをしよう
手元でクロスコンパイルをしてみる • クロスコンパイルに必要なもの • ターゲットアーキテクチャ用の標準ライブラリ • ターゲットアーキテクチャ用の外部ライブラリ • ターゲットアーキテクチャ用のリンカ (gcc)
• 通常のコンパイラに他アーキテクチャ向けのバックエンドも同 梱されているので、コンパイラは不要 • メタビルド系の処理もcargoがいい感じにハンドルしてくれる
標準ライブラリをGet • 自分のアーキテクチャの32bit版で試すのがオススメ $ rustup target list $ rustup target
add i686-unknown-linux-gnu
ターゲットを指定してビルド • 以下のようなコマンドを叩くだけ! (だいたいリンクで失敗する) $ cargo build --target i686-unknown-linux-gnu Ubuntuの場合は以下が参考になる:
https://askubuntu.com/questions/522372/installing-32-bit- libraries-on-ubuntu-14-04-lts-64-bit
Travisでツールをリリースする • GitHubのリポジトリを立てる • ferris_watchをpush • Travisを有効化する
Travisでツールをリリースする • こんな感じで.travis.ymlを書く • https://github.com/qnighy/ferris_watch/blob/63adc870937351aead ea7ae8f77804877cbf2d14/.travis.yml • TravisはWindowsをベータサポートしているので、3つのOSで ビルドできる •
さすがに同じOSでビルドしたほうが楽 • クロスコンパイルで32bit用バイナリは生成できる • ……という話をする予定だったが、ncursesとの相性が異様に 悪くてmuslのコンパイルが通らない……
Travisでツールをリリースする • 自動でデプロイするには https://docs.travis- ci.com/user/deployment/releases を参考にしてシークレット を設定する必要がある • travis setup
releases を実行していい感じに編集するの が楽
やろうとしたけど準備できなかったこと • reqwestを使った自動アップデート
まとめ • コマンドラインツールを一通り作ってみた • それなりにまとまったライブラリ群が用意されており、Rustの 中では比較的とっつきやすい • Rustで書く利点もある