最近Rustを書き始めました。楽しいです。こういう初歩的な内容もあっていいと思ったので投稿します。Rustのバージョンは2021です。
TL;DR
- パーシャルムーブを回避するための、
Option.take()
やOption.as_ref()
、Option.as_mut()
などについて理解を深めた。 - 外部コマンドを実行する際に、子プロセスに渡す標準入力のファイルハンドルがクローズされないとデッドロックすることを学んだ。
-
ChildStdin
がdropしないとファイルハンドルがクローズされないことから、Rustの所有権やdropについて理解を深めた。
Rustから外部のコマンドを実行したい
Rustから外部のコマンドを実行して実行結果を受け取る方法について調べていました。
例としてrustfmt
コマンドを実行します。コマンドを実行する子プロセスを作成します。とりあえずmacOSで動かすことだけ考えます。
use std::io::prelude::*;
use std::process::{Command, Stdio};
fn main() {
let rawcode = "use foo ;";
let expected = "use foo;\n";
// コマンドを作成する
let mut child = Command::new("rustfmt")
.arg("--emit=stdout")
.arg("--quiet")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to spawn child process");
// TODO: 続きを書く
}
次に、コマンドを実行する子プロセスの標準入力に書き込みます。何も考えず書いたら、 `child.stdin` partially moved due to this method call rustc(E0382)
とパーシャルムーブだと怒られました。
// 子プロセスの標準入力に書き込みたい
child
.stdin
// 以下のようにコンパイラに怒られる
// `child.stdin` partially moved due to this method call rustc(E0382)
.expect("child.stdin is None")
.write_all(rawcode.as_bytes())
.expect("Failed to write to stdin");
たしかにパーシャルムーブだけど、どうやって回避すればいいのかなと調べたところ、Struct std::process::Childの説明にこういうことが書いてありました。
stdin: Option
The handle for writing to the child’s standard input (stdin), if it has been captured. To avoid partially moving the child and thus blocking yourself from calling functions on child while using stdin, you might find it helpful:
let stdin = child.stdin.take().unwrap();
Option型にtakeメソッドがあることを学びました。
- https://qiita.com/hiyoko3m/items/8ff624795432187cce0c#option%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E5%AE%9F%E7%8F%BE%E3%81%99%E3%82%8B
- https://qiita.com/knknkn1162/items/1d190880efffe3578d92#struct-fields-that-can-be-loaned-or-taken2
ファイルハンドルのクローズ漏れで、デッドロックが発生
ということで、何も考えずにこのように書きました。
let mut child_stdin = child.stdin.take().expect("child.stdin is None");
child_stdin
.write_all(rawcode.as_bytes())
.expect("Failed to write to stdin");
// ここでデッドロックが発生する
let output = child.wait_with_output().expect("Failed to wait on child");
let result = String::from_utf8_lossy(&output.stdout);
assert_eq!(result, expected);
コンパイラは通りましたが、困ったことに実行結果がいつまでたっても返ってきません。デッドロックしているようです。仕方ないのでまた調べたところ、以下のことがわかりました。
-
ChildStdin
に紐付いているファイルハンドルがクローズされないとデッドロックする。(参考) - ファイルハンドルをクローズするには、
ChildStdin
がdropされる必要がある。(参考) -
Child.wait_with.output() (ソース)では、
self.stdin
を明示的に手動でstd::mem::drop()しているが、stdinの実体はコードの前の部分でtake
によって所有権付きで移動してしまっているので、クローズに失敗している。 - 変数
child_stdin
が所有権を持つChildStdin
のライフタイムは、この場合は関数の終わりになる。そこに到達しないと自動でdropされない。
リソースをdropさせる
解決する方法として2つあることがわかりました。
- 手動でstd::mem::drop()を呼び出す。
let mut child_stdin = child.stdin.take().expect("child.stdin is None");
child_stdin
.write_all(rawcode.as_bytes())
.expect("Failed to write to stdin");
drop(child_stdin);
- 変数
child_stdin
に代入したせいで、ライフタイムが関数の最後になっている。変数に代入せずに一時的な値にすれば、その式の終わりでライフタイムが完了してdropされる。
child
.stdin
.take()
.expect("child.stdin is None")
.write_all(rawcode.as_bytes())
.expect("Failed to write to stdin");
どちらにせよ注意が必要です。困りました。
Option.as_mut()が良さそう
と、rust-lang-nurseryのRust Cookbookでは、Option型のas_mutメソッドを使っていることを知りました。Optionの中身を可変参照で受け取るメソッドのようです。試してみます。
let child_stdin = child.stdin.as_mut().expect("child.stdin is None");
child_stdin
.write_all(rawcode.as_bytes())
.expect("Failed to write to stdin");
// 問題なく実行される
let output = child.wait_with_output().expect("Failed to wait on child");
let result = String::from_utf8_lossy(&output.stdout);
assert_eq!(result, expected);
デッドロックも発生せず、問題なく実行されました。可変参照でchild.stdin
を受け取る場合、所有権は移動ではなく借用しているだけなので、きちんとChild.wait_with.output()
でChildStdin
が明示的にdropされるようです。そもそも、take()
でchild.stdin
の値を変更すること自体が問題ありそうなので、これで良いのではないでしょうか。
最後に完成形のプログラムと型推論を貼っておきます。おつかれさまでした。
use std::io::prelude::*;
use std::process::{Command, Stdio};
fn main() {
let rawcode = "use foo ;";
let expected = "use foo;\n";
let mut child = Command::new("rustfmt")
.arg("--emit=stdout")
.arg("--quiet")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to spawn child process");
let child_stdin = child.stdin.as_mut().expect("child.stdin is None");
child_stdin
.write_all(rawcode.as_bytes())
.expect("Failed to write to stdin");
let output = child.wait_with_output().expect("Failed to wait on child");
let result = String::from_utf8_lossy(&output.stdout);
assert_eq!(result, expected);
}