はじめに
gitリポジトリ内のソースコードを高速にctagsするツールptagsをRustで作りました。この記事では簡単にソースコードの解説をします。
ツール自体の話は以下の記事をどうぞ。
gitリポジトリ向けの高速ctagsラッパー:ptags
リポジトリ
ソースコード解説
あまり日本語の記事を見かけないライブラリの使い方を中心に解説します。
結構試行錯誤しながら使っている部分もあるので詳しい方のご指摘もお待ちしています。
error-chain
error-chainはエラーハンドリングを簡単に行うためのライブラリです。
Rustのエラーハンドリングについてはこちらに書いてありますが、この通りに厳密にやろうとすると結構たくさんのコードを書く必要があります。
これをマクロを使って自動生成するのがerror-chainになります。
例えば以下はエラーの定義部になります。
独自のエラー型を定義したい時はerrorsに書きますが、見ての通りdescription
とdisplay
だけ定義すれば大丈夫です。
foreign_linksは後で説明します。
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
を以下のように生成してくれます。
(コードはイメージです)
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です。
次はエラーを返す関数を呼ぶところです。
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!
の中のlinks
とforeign_links
です。links
は他のerror-chainの場合、foreign_links
はそれ以外のエラーからを指定する時に使います。
links
を使った例はこんな感じです。他のモジュール内でerror_chain!
を使って宣言したError
とErrorKind
を取り込めます。
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が生成するError
はSend
なのでchannel
で問題なく送れます。
thread::spawn(move || {
...
match child {
Ok(mut x) => {
...
}
Err(x) => {
let _ = tx.send(Err(ErrorKind::CommandFailed(bin_ctags.clone(), x).into()));
}
}
});
structopt
structoptはclapをベースにしたオプションパーサです。StructOpt
をderive
したstruct
から自動でパーサを生成してくれます。
#[structopt]
ディレクティブでオプション名やデフォルト値を設定できます。
#[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()
を使ってこんな感じにできます。
#[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
外部コマンドを呼び出す際に使います。だいたいのところはこちらにまとまっているので、ここでは生成したプロセスの入出力周りだけ。
標準入力を使う場合のプロセス生成は以下の部分です。
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
から読み書きできます。
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の最大サイズを書き込む予定のサイズまで増やしています。
#[cfg(linux)]
fn set_pipe_size(stdin: &ChildStdin, len: i32) -> Result<()> {
fcntl(stdin.as_raw_fd(), FcntlArg::F_SETPIPE_SZ(len))?;
Ok(())
}