gitリポジトリ向けの高速ctagsラッパー：ptags （ソースコード解説）

# はじめに
gitリポジトリ内のソースコードを高速にctagsするツール[ptags](https://github.com/dalance/ptags)をRustで作りました。この記事では簡単にソースコードの解説をします。

ツール自体の話は以下の記事をどうぞ。
[gitリポジトリ向けの高速ctagsラッパー：ptags](https://qiita.com/dalance/items/c76141a097e25fabefe8)

# リポジトリ

https://github.com/dalance/ptags

# ソースコード解説
あまり日本語の記事を見かけないライブラリの使い方を中心に解説します。
結構試行錯誤しながら使っている部分もあるので詳しい方のご指摘もお待ちしています。

## [error-chain](https://github.com/rust-lang-nursery/error-chain)
error-chainはエラーハンドリングを簡単に行うためのライブラリです。
Rustのエラーハンドリングについては[こちら](https://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/book/error-handling.html)に書いてありますが、この通りに厳密にやろうとすると結構たくさんのコードを書く必要があります。
これをマクロを使って自動生成するのがerror-chainになります。

例えば以下はエラーの定義部になります。
独自のエラー型を定義したい時はerrorsに書きますが、見ての通り`description`と`display`だけ定義すれば大丈夫です。
foreign_linksは後で説明します。

```rust:src/cmd_ctags.rs
error_chain! {
    foreign_links {
        Io(::std::io::Error);
        Utf8(::std::str::Utf8Error);
        Recv(::std::sync::mpsc::RecvError);
        Nix(::nix::Error) #[cfg(linux)];
    }
    errors {
        CtagsFailed(cmd: String, err: String) {
            description("ctags failed")
            display("ctags failed: {}\n{}", cmd, err)
        }
        CommandFailed(path: PathBuf, err: ::std::io::Error) {
            description("ctags command failed")
            display("ctags command \"{}\" failed: {}", path.to_string_lossy(), err)
        }
    }
}
```


error-chainは`error_chain!`マクロから`struct Error`や`enum ErrorKind`を以下のように生成してくれます。
（コードはイメージです）

```rust
struct Error {
  ErrorKind
}
enum ErrorKind {
  Io(std::io::Error),
  Utf8(std::str::Utf8Error);
  Recv(std::sync::mpsc::RecvError);
  CtagsFailed(String, String)
  CommandFailed(PathBuf, std::io::Error)
}
```

このようにモジュール内で扱いたいエラーは全て単一の`Error`型に集約できるので、エラーを返す可能性のある関数の戻り値は全て`Result<T,Error>`に統一できます。

```rust
type Result<T> = Result<T, Error>;
```

実際にはこのような型エイリアスも定義してくれるので`Result<T>`でOKです。
次はエラーを返す関数を呼ぶところです。

```rust:src/cmd_ctags.rs
pub fn get_tags_header(opt: &Opt) -> Result<String> {
        let tmp_empty = NamedTempFile::new()?;
        let tmp_tags = NamedTempFile::new()?;
...
```

返ってきた`Result<T>`は`?`オペレータを使って`T`を取り出せます。（この`?`はerror-chainではなくRustの言語機能です）
例えば`result?`はだいたい以下のような動作をします。

```rust
match result {
    Ok(val) => val,
    Err(err) => return Err(err),
})
```

このとき返ってきたエラー型が`Error`ではなくて例えば`std::io::Error`だとそのままでは`Err`に入れられません。そのため実際には`return Err(From::from(e))`という感じの変換関数を呼んでくれます。
この変換関数もerror-chainが生成してくれるのですが、どのエラーから変換するのかを指定するのが`error_chain!`の中の`links`と`foreign_links`です。`links`は他のerror-chainの場合、`foreign_links`はそれ以外のエラーからを指定する時に使います。

`links`を使った例はこんな感じです。他のモジュール内で`error_chain!`を使って宣言した`Error`と`ErrorKind`を取り込めます。

```rust:src/bin.rs
error_chain! {
    links {
        Git(cmd_git::Error, cmd_git::ErrorKind);
        Ctags(cmd_ctags::Error, cmd_ctags::ErrorKind);
    }
    foreign_links {
        Io(::std::io::Error);
        Utf8(::std::str::Utf8Error);
    }
}
```

実際にはいちいちエラー型を調べるのも面倒なので、とりあえず`?`を使ってみて、コンパイラに怒られたものを追加していけばいいと思います。例えば`std::io::Error`への`foreign_links`が抜けていた場合以下のように`From`トレイトの未実装エラーになります。

```console
error[E0277]: the trait bound `bin::Error: std::convert::From<std::io::Error>` is not satisfied
   --> src/bin.rs:169:9
    |
169 |         f.write("\n".as_bytes())?;
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::convert::From<std::io::Error>` is not implemented for `bin::Error`
    |
    = help: the following implementations were found:
              <bin::Error as std::convert::From<cmd_git::Error>>
              <bin::Error as std::convert::From<cmd_ctags::Error>>
              <bin::Error as std::convert::From<std::str::Utf8Error>>
              <bin::Error as std::convert::From<bin::ErrorKind>>
            and 2 others
    = note: required by `std::convert::From::from`
```

`thread::spawn`した先からエラーを返す場合は`channel`で返しています。
error-chainが生成する`Error`は`Send`なので`channel`で問題なく送れます。

```rust:src/cmd_ctags.rs
thread::spawn(move || {
    ...
    match child {
        Ok(mut x) => {
            ...
        }
        Err(x) => {
            let _ = tx.send(Err(ErrorKind::CommandFailed(bin_ctags.clone(), x).into()));
        }
    }
});
```

## [structopt](https://github.com/TeXitoi/structopt)
structoptは[clap](https://crates.io/crates/clap)をベースにしたオプションパーサです。`StructOpt`を`derive`した`struct`から自動でパーサを生成してくれます。
`#[structopt]`ディレクティブでオプション名やデフォルト値を設定できます。

```rust:src/bin.rs
#[derive(StructOpt, Debug)]
#[structopt(name = "ptags")]
#[structopt(raw(long_version = "option_env!(\"LONG_VERSION\").unwrap_or(env!(\"CARGO_PKG_VERSION\"))"))]
#[structopt(raw(setting = "clap::AppSettings::AllowLeadingHyphen"))]
#[structopt(raw(setting = "clap::AppSettings::ColoredHelp"))]
pub struct Opt {
    /// Number of threads
    #[structopt(short = "t", long = "thread", default_value = "8")]
    pub thread: usize,

    /// Output filename
    #[structopt(short = "f", long = "file", default_value = "tags", parse(from_os_str))]
    pub output: PathBuf,
```

例えば`pub thread: usize`に付いている`#[structopt]`の部分は以下のように展開されます。

```rust
Arg::with_name("thread")
    .short("t")
    .long("thread")
    .default_value("8");
```

`#[structopt(xxx = "yyy")]`の形式で呼び出せるのは`&str`を引数に取る`fn xxx(self, l: &'b str)`だけですが、
`raw`を使うと任意のコードを渡すことが出来ます。
すなわち`pub struct Opt`は以下のように展開されます。

```rust
App::with_name("ptags")
    .long_version(option_env!("LONG_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")))
    .setting(clap::AppSettings::AllowLeadingHyphen)
    .setting(clap::AppSettings::ColoredHelp);
```

特に`raw`を使って細かい調整をするあたりはstructopt側のドキュメントには書いていないのでclapの方の関数一覧を見ながら書くことになります。

また、通常は`let opt = Opt::from_args()`でコマンドライン引数から`Opt`を得ますが、テストを書きたいときは`from_iter()`を使ってこんな感じにできます。

```rust:src/bin.rs
#[test]
fn test_run_opt() {
    let args = vec!["ptags", "-s", "-v", "--validate-utf8", "--unsorted"];
    let opt = Opt::from_iter(args.iter());
    let ret = run_opt(&opt);
    assert!(ret.is_ok());
}
```

## [std::processs](https://doc.rust-lang.org/std/process/)
外部コマンドを呼び出す際に使います。だいたいのところは[こちら](http://keens.github.io/blog/2016/12/02/rustnopurosesu/)にまとまっているので、ここでは生成したプロセスの入出力周りだけ。
標準入力を使う場合のプロセス生成は以下の部分です。

```rust:src/cmd_ctags.rs
let child = Command::new(bin_ctags.clone())
    .args(args)
    .current_dir(dir)
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    //.stderr(Stdio::piped()) // Stdio::piped is x2 slow to wait_with_output() completion
    .stderr(Stdio::null())
    .spawn();
```

標準入力を書きたいときは多分`spawn()`するしかないです。それ以外（`output()`と`status()`）は呼んだ時点でブロックしてプロセス終了まで待ってしまうし、そもそも書き込むための`ChildStdin`を得る手段がありません。
また`stdin()/stdout()/stderr()`のうち使いたいものを`Stdio::piped()`に設定します。
これで`spawn()`から返ってきた`Child`の`stdin/stdout/stderr`から読み書きできます。

```rust:src/cmd_ctags.rs
match child {
    Ok(mut x) => {
        {
            let stdin = x.stdin.as_mut().unwrap();
            let _ = CmdCtags::set_pipe_size(&stdin, file.len() as i32)
                .or_else(|x| tx.send(Err(x.into())));
            let _ = stdin.write(file.as_bytes());
        }
        match x.wait_with_output() {
            Ok(mut x) => {
                let _ = tx.send(Ok(x));
            }
            Err(x) => {
                let _ = tx.send(Err(x.into()));
            }
        }
    }
```

ちょっと原因は追っていないのですが、`stderr()`を`Stdio::piped()`にしたところプロセス完了までの時間が倍近くに延びてしまいました。なので使わないところは`Stdio::null()`で閉じておいた方がいいかもしれません。

またRustとは直接関係ないのですが、`ChildStdin`への`write()`はブロックしてしまうことがあります。
実際ctagsを`-L- -f- --sort=no`で呼ぶと発生します。このモードでは標準入力から1ファイル入力される度にタグファイルを標準出力に出すのですが、ptags側は全部入力し終えるまで出力を見に行かないのでPIPEの最大サイズ（Linuxならデフォルト64KB）まで出力したところでctags側が止まってしまい、ptagsは入力に書き続けてこちらもPIPEの最大サイズで止まります。
本当は出力側を監視して一杯になる前に取り出すべきなのですが、とりあえずの対策として以下のように`fcntl`でPIPEの最大サイズを書き込む予定のサイズまで増やしています。

```rust:src/cmd_ctags.rs
#[cfg(linux)]
fn set_pipe_size(stdin: &ChildStdin, len: i32) -> Result<()> {
    fcntl(stdin.as_raw_fd(), FcntlArg::F_SETPIPE_SZ(len))?;
    Ok(())
}
```