Help us understand the problem. What is going on with this article?

Rustを使ったふつうのコマンドラインツール開発

More than 1 year has passed since last update.

これはKLab Advent Calendar 2017の17日目の記事です。

zothというツールを作ったので、ツール自体の紹介とRustでのコマンドラインツール開発についてまとめてみます。

https://github.com/hhatto/zoth

zothとは?

zothは、(標準)入力をキーにコマンド実行結果をキャッシュして、コマンド実行を高速化するツールです。(読み方は「ぞす」です。わりと適当です。)

主な機能は以下の通りです。

  • コマンド実行とキャッシュ
  • キャッシュクリア
  • キャッシュのヒット状況等の表示(未実装)

開発しようと思ったきっかけは、私が携わっているプロジェクトでPlantUMLを使ってデータベースのスキーマ定義からリレーション図を生成しているのですが、PlantUMLの実行が遅い(約40秒程かかる)のでどうにかできないかなと思ったのがきっかけです。

毎回以下のようなコマンドを実行するのですが、javaを毎回起動する必要があり遅くなっていました。

$ java -Xmx256m -jar -Dfile.encoding=UTF-8 plantuml.jar -tsvg -o ./output relation.puml

javaplantuml.jarはそう更新頻度が高くないので、差分は無視できると考えて、インプットが同じ時はキャッシュ化しておいた出力結果を返せば毎回実行する必要がないよね?と思って作ってみました。

少し話はそれますが、plantuml-serverというPlantUMLを変換するサーバがあり、これを使えば常駐プロセスにできjavaの起動を省略でき高速化できるのでは?と考えたのですが、入力用のファイルサイズが大きく変換できなかったためあきらめました。

アーキテクチャ

アーキテクチャというほどたいそうな仕組みではないですが、以下のような構成になっています。

  • クライアント-サーバモデル
  • サーバがコマンド実行やキャッシュ化等のハンドリングを行う
  • クライアントはそのフロントエンド
  • 通信部分はgRPC使ってみたかったので使ってみた

実行例

コマンド実行

$ time cat ~/relations.pu | zoth exec -- java -Xmx256m -jar -Dfile.encoding=UTF-8 plantuml.jar -tsvg -p > out.svg
cat ~/relations.pu  0.00s user 0.00s system 34% cpu 0.006 total
./target/release/zoth exec -- java -Xmx256m -jar -Dfile.encoding=UTF-8  -tsvg -p  60.15s user 1.56s system 121% cpu 50.658 total

# 2回目
$ time cat ~/relations.pu | zoth exec -- java -Xmx256m -jar -Dfile.encoding=UTF-8 plantuml.jar -tsvg -p > out.svg
cat ~/relations.pu  0.00s user 0.00s system 30% cpu 0.008 total
./target/release/zoth exec -- java -Xmx256m -jar -Dfile.encoding=UTF-8  -tsvg -p  0.00s user 0.00s system 46% cpu 0.010 total

同じ入力(relations.pu)なので、2回目の実行時にキャッシュの値を返すことができ、高速に処理できています。
標準入力から値を受け取って、標準出力に出力するために、PlantUMLの実行オプションを変更しています。(-pオプションを追加で指定し、-oオプションとその引数は削ります。)

キャッシュクリア

現時点ではキャッシュ情報はディスク上に保存しています。
clearサブコマンドでディスク上のキャッシュ情報を削除します。

$ zoth clear -- java -Xmx256m -jar -Dfile.encoding=UTF-8 plantuml.jar -tsvg -p

開発に関するTips

コマンドラインツールを作ってみると、Rustはすでに色々なパッケージが揃っており、スムーズに開発ができました。
以降にコマンドラインツール開発についてのTipsを紹介します。

コマンドラインパーサー

コマンドラインのオプション解析はclap-rsを使いました。

開発が活発で、サブコマンドやヘルプ文字列の自動生成等基本的な機能がそろっています。
また、サブコマンドがある前提ですが、各シェル毎の補完関数を出力することも可能です。

clapは日本語での紹介記事もすでにいくつかあるので、そちらを見るとよいでしょう。

gRPC in Rust

RustでgRPCを扱う場合、有名なものだと以下の2つの選択肢があります。

今回はpingcap/grpc-rsを使うことにしました。
特に深くは考察しておらず、試しに動かして見たらさっくり動いたのでそのまま使った形です。

簡単に使い方を紹介すると、

まず、必要なツールをインストールします。

$ cargo install protobuf
$ cargo install grpcio-compiler

Protocol Buffersで信号を定義して、protocコマンドでgRPC用のファイルを出力します。

syntax = "proto3";

package zothcore;

service ZothCore {
    rpc Exec (ExecRequest) returns (ExecResponse) {}
}

message ExecRequest {
    string command = 1;
    string args = 2;
    string input = 3;
}

message ExecResponse {
    string result = 1;
}
$ protoc --rust_out=./src/ --grpc_out=./src/ --plugin=protoc-gen-grpc=`which grpc_rust_plugin` proto/zothcore.proto
$ ls src
zothcore_grpc.rs  zothcore.rs

あとは、grpcioクレートを使えるようにして、mod パッケージ名;mod パッケージ名_grpc;
モジュールを呼び出せば使うことができます。

Cargo.toml
[dependencies]
grpcio = "0.1"
protobuf = "1.2"
futures = "0.1"
mod zothcore;
mod zothcore_grpc;

その他サーバ、クライアントの必要最低限のコードはpingcap/grpc-rsの例zothのコードを見ると理解できると思います。

which

whichコマンドライクな処理をさせたい場合にwhichクレートが使えます。
便利です。
後述のbuild.rsで grpc_rust_pluginプラグインを探す際に使いました。
プラットフォームを気にせず使えます。

use which::which;

let result = which::which("rustc").unwrap();

その他開発に関するTips

sccache

コンパイル時間を短縮するためにccacheライクなツールを使っています。
(sccacheについては手前味噌ですがこの記事をどうぞ。)
sccachecargo buildコマンドを合わせて使う際は、カラー表示が無効になってしまうので、
--color=alwaysオプションを指定して強制的にカラー表示するようにするとよいでしょう。

build.rs

ビルド時にRustソースのビルド以外に何か別のものを準備する必要がでてきたりします。
例えばzothの場合はgRPC用の.protoファイルをコンパイルしたいという要望がありました。

Rustのパッケージシステムにはbuild.rsを利用したビルドスクリプトによるサポートがあります。
要はbuild.rsにさせたい処理を自由に定義できる感じです。

zothではfeaturesオプションとアトリビュートを使って、重い処理だと毎回実行するのが嫌なので必要に応じて--features=genprotoオプションを指定することでgRPC用のソースを生成するようにしています。

Cargo.toml
[package]
build = "build.rs"
build.rs
extern crate which;

#[cfg(feature = "genproto")]
mod inner {
    use std::process::Command;
    use which::which;

    // NOTE: enable log with `cargo build -vv`
    pub fn gen() {
        println!("gen protoc");
        let grpc_rust_plugin_path = which("grpc_rust_plugin").expect("fail get extension path")
            .to_str().expect("fail path string").to_string();

        let output = Command::new("protoc")
            .arg("--rust_out=./src/")
            .arg("--grpc_out=./src/")
            .arg(format!("--plugin=protoc-gen-grpc={}", grpc_rust_plugin_path).as_str())
            .arg("proto/zothcore.proto")
            .output().expect("fail command");
        println!("{}", output.status);
        println!("{}", String::from_utf8_lossy(&output.stdout));
        println!("{}", String::from_utf8_lossy(&output.stderr));
    }
}

#[cfg(not(feature = "genproto"))]
mod inner {
    pub fn gen() {}
}

fn main() {
    inner::gen();
}
$ cargo build --features=genproto

また、build.rs内の処理でlogger使うまでもないけどプリントデバッグしたい場合がありますが、cargo buildオプションなしまたは-vオプションをつけても何も出力されません。
そんなときは-vvオプションつけると出力されるので便利です。

まとめ

以上、雑多ですがコマンドラインツールをつくってみてのTipsをまとめてみました。

Rustは高速で外部パッケージもすでに豊富になってきているので、ツール開発にも有用だと思います。
ツール開発あたりからRustを導入してみてはいかがでしょうか?

hhatto
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away