LoginSignup
14
6

More than 1 year has passed since last update.

Rustでの外部コマンドの実行について調べたら、所有権についての理解が深まった件

Posted at

最近Rustを書き始めました。楽しいです。こういう初歩的な内容もあっていいと思ったので投稿します。Rustのバージョンは2021です。

TL;DR

  • パーシャルムーブを回避するための、Option.take()Option.as_ref()Option.as_mut()などについて理解を深めた。
  • 外部コマンドを実行する際に、子プロセスに渡す標準入力のファイルハンドルがクローズされないとデッドロックすることを学んだ。
  • ChildStdinがdropしないとファイルハンドルがクローズされないことから、Rustの所有権やdropについて理解を深めた。

Rustから外部のコマンドを実行したい

Rustから外部のコマンドを実行して実行結果を受け取る方法について調べていました。

例としてrustfmtコマンドを実行します。コマンドを実行する子プロセスを作成します。とりあえずmacOSで動かすことだけ考えます。

execute.rs
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)とパーシャルムーブだと怒られました。

execute.rs
    // 子プロセスの標準入力に書き込みたい
    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");

型推論はこんな感じです。
スクリーンショット 2021-12-23 17.51.35.png

たしかにパーシャルムーブだけど、どうやって回避すればいいのかなと調べたところ、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

ファイルハンドルのクローズ漏れで、デッドロックが発生

ということで、何も考えずにこのように書きました。

execute.rs
    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つあることがわかりました。

execute.rs
    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される。
execute.rs
    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の中身を可変参照で受け取るメソッドのようです。試してみます。

execute.rs
    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);

型推論はこんな感じです。
スクリーンショット 2021-12-23 19.45.06.png

デッドロックも発生せず、問題なく実行されました。可変参照でchild.stdinを受け取る場合、所有権は移動ではなく借用しているだけなので、きちんとChild.wait_with.output()ChildStdinが明示的にdropされるようです。そもそも、take()child.stdinの値を変更すること自体が問題ありそうなので、これで良いのではないでしょうか。

最後に完成形のプログラムと型推論を貼っておきます。おつかれさまでした。

execute.rs
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);
}

スクリーンショット 2021-12-23 19.53.20.png

14
6
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
14
6