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

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

# TL;DR

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

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

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

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

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

```rust: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422103/1e3d8c8e-2f79-3086-3b0e-0662affe7569.png)

たしかにパーシャルムーブだけど、どうやって回避すればいいのかなと調べたところ、[Struct std::process::Childの説明](https://doc.rust-lang.org/std/process/struct.Child.html#structfield.stdin)にこういうことが書いてありました。

> stdin: Option<ChildStdin>
> 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://doc.rust-lang.org/std/option/enum.Option.html#method.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

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

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

```rust: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`に紐付いているファイルハンドルがクローズされないとデッドロックする。([参考](https://doc.rust-lang.org/std/process/struct.Child.html#method.wait_with_output))
- ファイルハンドルをクローズするには、`ChildStdin`がdropされる必要がある。([参考](https://doc.rust-lang.org/std/process/struct.ChildStdin.html))
- [Child.wait_with.output()](https://doc.rust-lang.org/std/process/struct.Child.html#method.wait_with_output) ([ソース](https://doc.rust-lang.org/src/std/process.rs.html#1828-1850))では、`self.stdin`を明示的に手動で[std::mem::drop()](https://doc.rust-lang.org/std/mem/fn.drop.html)しているが、stdinの実体はコードの前の部分で`take`によって所有権付きで移動してしまっているので、クローズに失敗している。
- 変数`child_stdin`が所有権を持つ`ChildStdin`のライフタイムは、この場合は関数の終わりになる。そこに到達しないと自動でdropされない。

# リソースをdropさせる

解決する方法として2つあることがわかりました。

- 手動で[std::mem::drop()](https://doc.rust-lang.org/std/mem/fn.drop.html)を呼び出す。

```rust: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される。

```rust: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](https://rust-lang-nursery.github.io/rust-cookbook/os/external.html#run-an-external-command-passing-it-stdin-and-check-for-an-error-code)では、Option型の[as_mutメソッド](https://doc.rust-lang.org/std/option/enum.Option.html#method.as_mut)を使っていることを知りました。Optionの中身を可変参照で受け取るメソッドのようです。試してみます。

```rust: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422103/e21961f7-fd67-cf2f-c242-be5079cb1c8b.png)

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

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

```rust: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422103/e7e9045a-de59-78fd-409b-8aefa7e8cdca.png)