LoginSignup
7
5

More than 5 years have passed since last update.

gitリポジトリ向けの高速ctagsラッパー:ptags (ソースコード解説)

Last updated at Posted at 2018-03-15

はじめに

gitリポジトリ内のソースコードを高速にctagsするツールptagsをRustで作りました。この記事では簡単にソースコードの解説をします。

ツール自体の話は以下の記事をどうぞ。
gitリポジトリ向けの高速ctagsラッパー:ptags

リポジトリ

ソースコード解説

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

error-chain

error-chainはエラーハンドリングを簡単に行うためのライブラリです。
Rustのエラーハンドリングについてはこちらに書いてありますが、この通りに厳密にやろうとすると結構たくさんのコードを書く必要があります。
これをマクロを使って自動生成するのがerror-chainになります。

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

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 Errorenum ErrorKindを以下のように生成してくれます。
(コードはイメージです)

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>に統一できます。

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

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

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?はだいたい以下のような動作をします。

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

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

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

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トレイトの未実装エラーになります。

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が生成するErrorSendなのでchannelで問題なく送れます。

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

structoptはclapをベースにしたオプションパーサです。StructOptderiveしたstructから自動でパーサを生成してくれます。
#[structopt]ディレクティブでオプション名やデフォルト値を設定できます。

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]の部分は以下のように展開されます。

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は以下のように展開されます。

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()を使ってこんな感じにできます。

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

外部コマンドを呼び出す際に使います。だいたいのところはこちらにまとまっているので、ここでは生成したプロセスの入出力周りだけ。
標準入力を使う場合のプロセス生成は以下の部分です。

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()から返ってきたChildstdin/stdout/stderrから読み書きできます。

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の最大サイズを書き込む予定のサイズまで増やしています。

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(())
}
7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5