LoginSignup
5
8

More than 1 year has passed since last update.

RustでPythonをコマンド実行

Last updated at Posted at 2021-06-08

メインスレッドを止めず、Pythonからの出力結果は変数で受け取りたくて調べてみた。

単純なソースコード

python

1秒毎に文字を標準出力するスクリプト。

import time
for i in range(5):
    print(i)
    time.sleep(1)
print("end")

Rust

thread::sleepを使って0.5秒毎にPythonの出力結果をstdout.read_line(&mut line)lineに書き込み、println!で表示させる。

追記:このままではstdout.read_lineでPythonからの出力を待つ状態になるので、下部の「制御も別スレッドに分ける」に記載しているコードのように、メインスレッドとは完全に分けるか、非同期処理で動作させる方が良いかもしれない。

use std::{env, thread};
use std::io::{BufReader, BufRead};
use std::process::{Stdio, Command};
use std::time::Duration;

/// 実行ファイルの場所を取得する
pub fn get_exe_dir() -> String {
    match env::current_exe() {
        Ok(p) => {
            let mut path = p.clone();
            path.pop();
            match path.into_os_string().into_string() {
                Ok(p2) => { return p2; }
                Err(_) => "./".to_string()
            }
        }
        Err(_) => { "./".to_string() }
    }
}

fn main() {
    let mut child = Command::new("python")
        .args(&["-u".to_string(), get_exe_dir() + "/../../test.py"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();
    let mut stdout = BufReader::new(child.stdout.as_mut().unwrap());
    let mut line = String::new();
    for _ in 0..10 {
        line.clear();
        match stdout.read_line(&mut line) {
            Ok(_) => println!("{}", line),
            Err(e) => println!("{}", e)
        };
        thread::sleep(Duration::from_millis(500));
    };
}

解説

Python -u

Rustに限らず、外からPythonを実行させるときは、-u引数を付けて実行させないと、結果がバッファにたまり、スレッドがブロックされ、完了後に再び動作するという状態になる。なかなか気が付きにくいハマりどころ。

Stdio::piped()

.stdout(Stdio::piped())をつけることで、Commandの出力をBufReader::new(child.stdout.as_mut().unwrap());を使って結果を取得する。

Command::new("python")

Command::new("python")でPythonを呼び出しているが、仮想環境やEmbeddedを使う場合は、"python"の部分に絶対パスを指定すれば良い。

例えばAnacondaやminicondaを使っている場合は、コマンドプロンプト等でconda info -eを実行すれば絶対パスを取得出来る。

制御も別スレッドに分ける

Pythonの監視も別スレッドにして結果をチャンネルを使って受け取ったりすれば、使い勝手が良くなるかもしれない。

use std::{env, thread};
use std::io::{BufReader, BufRead};
use std::process::{Stdio, Command};
use std::time::Duration;
use std::sync::mpsc::{Sender, Receiver, channel};
use std::any::Any;

/// 実行ファイルの場所を取得する
pub fn get_exe_dir() -> String {
    match env::current_exe() {
        Ok(p) => {
            let mut path = p.clone();
            path.pop();
            match path.into_os_string().into_string() {
                Ok(p2) => { return p2; }
                Err(_) => "./".to_string()
            }
        }
        Err(_) => { "./".to_string() }
    }
}

fn main() {
    // メッセージ用のチャンネル
    let (message_tx, message_rx): (Sender<String>, Receiver<String>) = channel();
    // Python停止命令用のチャンネル
    let (is_end_tx, is_end_rx): (Sender<bool>, Receiver<bool>) = channel();
    //
    // ここから別スレッド
    //
    thread::spawn(move || {
        let mut child = Command::new("python")
            .args(&["-u".to_string(), get_exe_dir() + "/../../test.py"])// 実行ファイルから2つ上のtest.pyを実行する。
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .unwrap();
        // この変数に結果が入る
        let mut line = String::new();
        // ループさせる。返り値はPythonが終了しているかどうか。
        let is_terminate = 'l: loop {
            // childが終了命令より前に終了しているかどうかを確認。
            match child.try_wait() {
                Ok(res) => {
                    match res {
                        Some(res2) => {
                            // 終了したかどうか
                            if res2.success() {
                                message_tx.clone().send(format!("exit code {}", res2.code().unwrap_or(-1)));
                                break 'l true;
                            }else{
                                // resに値が入っていても、success()がtrueとは限らない
                            }
                        }
                        None => {}
                    }
                }
                Err(e) => {
                    println!("{}", e);
                    break 'l false;
                }
            }
            let mut stdout = BufReader::new(child.stdout.as_mut().unwrap());
            // 終了命令が無いか調べる
            match is_end_rx.try_recv() {
                Ok(res) => {
                    // あればループを抜ける。
                    if res { break 'l false; };
                }
                Err(_) => {}
            }
            line.clear();
            // Pythonから出力があるまでこのスレッドは停止。
            match stdout.read_line(&mut line) {
                // メッセージをメインスレッドに送る。
                Ok(_) => { message_tx.clone().send(line.to_string()) }
                Err(e) => {
                    println!("{}", e);
                    // メッセージが読み込まれない場合は終了する。
                    break 'l false;
                }
            };
        };
        if !is_terminate {
            // Python終了
            match child.kill() {
                Ok(()) => println!("-> python end"),
                Err(e) => println!("{}", e)
            };
        };
    });
    //
    // ここからはメインスレッド
    //
    // n秒後に停止するまでメッセージを受け取る
    for _ in 0..10 {
        // try_recvで受け取るようにすると、スレッドが停止しない。
        match message_rx.try_recv() {
            Ok(res) => {
                // Pythonの出力を受け取る。
                println!("rust receiver {}", res);
            }
            Err(_) => {
                // 開始のタイミングがズレて、開始前にここに到達することがあるため、ここでbreakはしない。
            }
        }
        thread::sleep(Duration::from_millis(1000));
    }
    // 別スレッドへ終了命令を送る
    is_end_tx.send(true);
    thread::sleep(Duration::from_millis(3000));
    println!("program end");
}
5
8
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
5
8