rrcというghqクローンを書いたというハナシ

async/awaitasync-std が出たので、また Rust を書き始めています。どうも、僕です。

新しい言語やフレームワークがでた際、学習用にいろんなモノのクローンを書いているのですが、今回は以前より書こうと思っていた ghq のクローンを Rust で書いた話をしたいと思います。

今回は Rust にやっと取り込まれた async/await、それを利用するための crate async-std の学習するのが目的です。

またその他の理由に Rust を使っているユーザーの中には普段使っているツールも Rust 製のものを使う人が割といます。私自身もその一人です。 Go で書かれた ghq の代わりで自分の使いたいものを Rust で作るといった点もクローンを開発する動機になっています。

もちろん Rust で書かれた ghq クローン、あるいは同様な処理を行うツールが幾つか存在することも知っています。 rhqreposclg などがそれらにあたります。

async-std

async-std は Rust で非同期プログラミングを行うための crate です。

多くの API が Rust の標準ライブラリの API に寄せてあり、容易に導入できるのが特徴です。

通常のこのようなコードが

fn read_file(path: &str) -> io::Result<String> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

async-std ではこうなります。

async fn read_file(path: &str) -> io::Result<String> {
    let mut file = File::open(path).await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;
    Ok(contents)
}

変更点が少ないのがよくわかりますね。

実際には並列化すると色々ややこしいことが起こってきたりますが、それでも簡単に記述できるメリットは大きいです。

async-std 自身もまだこれから改良の余地があり、 新しいスケジューラー などパフォーマンス的にもまだ伸びしろがあり、この先が楽しみです。

今回は async-std の学習目的なのでできるだけ使っていきたいと思います。

rrc

ツール名は適当に rrc としました。

基本的に設定は無くても動作しますが、カスタマイズできる要件を満たせるよう設定ファイルによる設定をサポートしています。

細かい部分はこれからですが、仕事用、プライベート用などプロファイルごとに格納先を変えれるようにする予定です。

rrc はなるべくそのまま ghq を置き換えれるようサブコマンド名などは互換性を保つようにしています。

自分の用途としては listget があれば良かったのですが、某所のいつものメンバーに伺ったところ look もあった方がいいとのことなのでそれらを実装することにしました。

パフォーマンスに関してもやはり ghq よりも速いほうが嬉しいのでその辺も意識しました。

のっけから async/await 版で開発したのですが、元々 Rust でのマルチスレッド処理をまともにやってこなかったため、ライフタイムでつまづき、更には async fn の再帰でつまづいてしまいました。

そのため、まずは同期版で仕上げる方針としました。

同期処理で書き上げてみたのですが、十分速度が出たので非同期はいらなくなってしまいました。

とはいえそれでは学習目的が満たせないので 0.1.5 から少しずつパフォーマンスを気にしながら非同期処理を少し組み込んでいます。

async/awaitのパターンはまだ掴みきれてはいませんが、非同期で取得した結果を mpscchannel で送って集めるのではなく、 シンプルに Arc<Mutex<T>>でガードしつつ結果を集める方式としています。パフォーマンス的にはどちらがいいかまだ計測はしていません。

引数などスレッドをまたいでデータを参照するので Arc を多用せざるえないのですがそれ以外は割と素直に書けてるかも知れません。

fn walk_repository(root_path: &str, repos: &mut Arc<Mutex<Vec<LocalRepository>>>) -> Result<()> {
    let root = fs::read_dir(root_path)?;
    let root_path = Arc::new(root_path.to_owned());
    let mut futures: Vec<task::JoinHandle<Result<()>>> = vec![];

    for entry in root {
        let entry = entry?;
        let path = entry.path();
        let metadata = fs::metadata(&path)?;
        if metadata.is_file() {
            continue;
        }

        let repos = Arc::clone(&repos);
        let root_path = Arc::clone(&root_path);
        let f = task::spawn(async move {
            find_service_repositories(Arc::clone(&root_path), &path, &mut Arc::clone(&repos)).await
        });
        futures.push(f);
    }

    for f in futures {
        task::block_on(f)?;
    }

    Ok(())
}

最後に

やってみるとわかりますが新しい言語、フレームワークなどを学習するのにいろいろなものを再実装する方法はとても有効です。

マルチスレッドでのメモリ管理の関係で設計をどうすべきか手探りで書いている部分もありますが、やはり Rust は書いていて楽しいですね。

Rust で完結に非同期処理が書けるようになればいろいろなケースで Rust が採用されるケースが増えてくるかも知れません。