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
310
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
初めてDefinitelyTypedにPRを出した話
syumai
0
400
Why Jakarta EE Matters to Spring - and Vice Versa
ivargrimstad
0
1k
色々なIaCツールを実際に触って比較してみる
iriikeita
0
330
macOS でできる リアルタイム動画像処理
biacco42
9
2.4k
TypeScript Graph でコードレビューの心理的障壁を乗り越える
ysk8hori
2
1.1k
EventSourcingの理想と現実
wenas
6
2.3k
3rd party scriptでもReactを使いたい! Preact + Reactのハイブリッド開発
righttouch
PRO
1
600
聞き手から登壇者へ: RubyKaigi2024 LTでの初挑戦が 教えてくれた、可能性の星
mikik0
1
130
Tauriでネイティブアプリを作りたい
tsucchinoko
0
370
Snowflake x dbtで作るセキュアでアジャイルなデータ基盤
tsoshiro
2
520
シェーダーで魅せるMapLibreの動的ラスタータイル
satoshi7190
1
480
.NET のための通信フレームワーク MagicOnion 入門 / Introduction to MagicOnion
mayuki
1
1.4k
Featured
See All Featured
Raft: Consensus for Rubyists
vanstee
136
6.6k
Product Roadmaps are Hard
iamctodd
PRO
49
11k
Optimising Largest Contentful Paint
csswizardry
33
2.9k
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
42
9.2k
Done Done
chrislema
181
16k
Bootstrapping a Software Product
garrettdimon
PRO
305
110k
Statistics for Hackers
jakevdp
796
220k
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
93
16k
KATA
mclloyd
29
14k
Designing Dashboards & Data Visualisations in Web Apps
destraynor
229
52k
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
47
2.1k
実際に使う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で書く利点もある