在 C++ 裡面呼叫外部程式 Boost.Process

| | 0 Comments| 10:37
Categories:

如果想在 C++ 的程式裡面、呼叫外部的程式的話,一般傳統的做法,應該有使用 system()參考)或 C 風格的 popen() 這兩種方法(參考)。

不過,以 system() 來說,他並不能處理外部程式的輸出與輸入,在某些情況下不算實用。

popen() 的話,則是不存在於 MSVC 的環境中,需要使用替代的 _popen()MSDN)才行;同時,popen() 也僅有提供單向、不能雙向(只能寫或讀、不能同時讀寫);真要雙向的使用,其實相當地麻煩(參考)…

Boost.Process 則是一個還在開發中、也還沒正式進到 Boost C++ Libraries 的函式庫,他基本上就是希望可以提供一個跨平台、同時有更多功能的外部程序處理函式庫。

它的原始碼在:

https://github.com/klemens-morgenstern/boost-process

目前版本標記是 alpha06、需要搭配支援 C++11 的編譯器、以及 Boost 1.62.0 使用(試過 1.61 不行);文件則是在 http://klemens-morgenstern.github.io/process/

不過,Heresy 自己試過,是覺得目前的版本(不管是 alpha06 或是 develop 分支) ,似乎都有些問題。

以 alpha06 版來說,感覺有內部的語法直接有問題,導致整個程式無法編譯;而 develop 分支雖然可以運作,但是卻還是有不少功能似乎有問題…所以,個人是覺得,除非有必要,否則還是先不要用、等他穩定一點再說吧。


簡易使用

要使用 Boost.Process 需要引入 boost/process.hpp 這個 header 檔,之後的東西大多會在 boost::process 這個 namespace 下。

由於 Boost.Process 本身是 header-only 的,所以不需要編譯,不過因為他有用到 Boost.System,所以還是要建置 Boost C++ Libraries 才行。

至於要怎麼透過 Boost.Process 呼叫外部的程式呢?基本上有三種方法。


child

在使用上,他主要是提供了 child 這個類別,來處理外部的程序;他最簡單的寫法,大致上可以寫成:

#include <iostream>
#include <boost/process.hpp>
int main(int argc, char** argv)
{
	boost::process::child c("ping 127.0.0.1");
	c.wait();
	std::cout << c.exit_code() << std::endl;
}

在上面的例子裡面,他會去建立一個 child 的物件 c 去執行「ping 127.0.0.1」這個外部命令;之後則是透過 wait() 來確定他結束了,才會輸出這個外部程式回傳的結果(c.exit_code())、並輸出。


system()

而如果中間沒有打算對這個外部程式做特殊處理的話,其實也可以用 Boost.Process 提供的 system() 來簡化,其寫法如下:

#include <iostream>
#include <boost/process.hpp>
int main(int argc, char** argv)
{
	std::cout << boost::process::system("ping 127.0.0.1") << std::endl;
}

這個寫法和上面的是相同的。

至於和標準函式庫的 system() 看起來似乎一樣?不過實際上 Boost.Process 提供的 system() 還有許多其他的功能,這點就之後再提了。


spawn()

上面的程式寫法,基本上都是會等到外部的指令執行完成後,在結束程式。而如果希望把外部的程式執行起來後、就不管它的話,則就可以使用 spawn() 這個函式。

#include <iostream>
#include <boost/process.hpp>
int main(int argc, char** argv)
{
	boost::process::spawn("notepad");
}

像是以上面的程式來說,在他把 notepad 開啟後,自己就會先結束了。

而如果把 spawn() 改成 system() 的話,則會等到 notepad 被使用者關閉、程式才會結束。


執行屬性

上面基本上都是最基本的用法。不過如果只能這樣用的話,那其實用 Boost.Process 的用處就不大了。

在 Heresy 來看,Boost.Process 最大的好處,就是提供了許多執行時的屬性(properties),可以用來做進一步的控制~這些屬性包括了:cmd、args、exe、shell、env、std_in 等等…完整的列表可以參考官方文件(連結),下面 Heresy 就選一些做說明。


執行參數(cmdexeargs

在前面的例子裡面,不管是 childsystem() 或是 spawn(),都是把指令以單一字串的形式傳進去。

而實際上,Boost.Process 在執行外部程式的時候,在參數的傳遞上給了相當大的自由度。

比如說,以「ping 127.0.0.1」這個指令,直接以 system() 來呼叫的話,可以寫成:

namespace bp = boost::process;
bp::system( "ping 127.0.0.1" );

而這邊其實可以把執行指令「ping」和執行參數「127.0.0.1」分開、寫成:

namespace bp = boost::process;
bp::system("ping", "127.0.0.1");

但是 Heresy 自己在測試的時候,卻發現這樣的寫法會出現找不到指定檔案的錯誤…而要讓他可以運作的話,則需要使用完整的路徑,也就是:

namespace bp = boost::process;
bp::system("C:\Windows\System32\PING.EXE", "127.0.0.1");

而除了直接以字串傳入指令之外,他其實也還有提供許多屬性(properties),在執行時可以做設定,包括了:cmd、args、exe、shell、env 等等…完整的列表可以參考官方文件(連結)。

比如說,上面的例子實際上就是:

bp::system(bp::exe="C:\Windows\System32\PING.EXE", bp::args="127.0.0.1");

而最初的寫法,也等同於:

bp::system(bp::cmd="ping 127.0.0.1");

如果要執行的參數不只一個的話,也可以有幾種寫法;下面幾個寫法的效果基本上都是相同的:

bp::system("ping 127.0.0.1 -n 1");
bp::system(bp::cmd="ping 127.0.0.1 -n 1");
bp::system("C:\Windows\System32\PING.EXE", "127.0.0.1", "-n", "1");
bp::system(	bp::exe = "C:\Windows\System32\PING.EXE",
		bp::args = { "127.0.0.1", "-n", "1" });

不過,個人覺得比較討厭的,是「-n 1」這種用空格隔開的參數不能寫成一個字串、而得分成兩個,算是比較不好用的點了。


使用系統殼層執行(shell

另外,如果想直接使用 DOS 內建的命令(例如「dir」)的話,直接寫

bp::system("dir");

應該也會出現錯誤。

而要解決的話,就是再加上 shell,讓 Boost.Process 透過系統的 shell 去執行,這樣就可以了~他的寫法就是:

bp::system("dir", bp::shell);

而使用系統 shell 的另一個好處,就是前面有的必須要寫出執行檔完整路徑的情況,也可以不用了!例如:

bp::system("ping", "127.0.0.1", "-n", "1", bp::shell);
bp::system(bp::exe="ping", bp::shell, bp::args = {"127.0.0.1","-n","1"});

所以目前看來,在使用 Boost.Process 的時候,全部都加上 shell 應該會是個比較直覺的做法


指定起始位置(start_dir

再來,如果想要指定程式運作時的路徑的話,則可以透過 start_dir 這個屬性來指定,比如說:

bp::system("dir", bp::shell, bp::start_dir="d:\");

就可以透過 dir 的指令、列出 d: 下的檔案了。


輸入輸出(std_instd_outstd_err

前面最初就有提到,Heresy 之所以不用 popen(),一個主要的原因就是他要同時操作 std input 和 std output 時會很麻煩…

而 Boost.Process 在這部分,就算是相對方便了!他提供了 std_instd_outstd_err 這三個屬性,來對應標準輸入輸出、以及錯誤的處理。

在使用時,std_outstd_err 都是透過「>」來指定重新導向的目標,而 std_in 則是透過「<」來指定來源;這邊的目標或來源,可以直接給檔案名稱、或是使用 Boost.Process 的 pipepstream 來做處理。

最簡單的例子,就是如果不希望外部程式的輸出顯示在 console 上的話,可以透過把 std_out 導到 null 來做;這樣的程式會是:

bp::system("dir", bp::shell, bp::std_out > bp::null);

如此一來,程式執行時就看不到 dir 的結果了。

而官方也提供了一個範例:

boost::filesystem::path log = "my_log_file.txt";
boost::filesystem::path input = "input.txt";
boost::filesystem::path output = "output.txt";
bp::system("my_prog", bp::std_out>output, bp::std_in<input, bp::std_err>log);

在這個範例裡,會把 input.txt 這個檔案的內容、透過標準輸入(std::cin)的方式送給程式(my_prog),而程式輸出到標準輸出(std::cout)的內容、則是會被寫到 output.txt 這個檔案;另外,輸出到標準錯誤(std::cerr)的內容則是會被寫到 my_log_file.txt 裡。

而如果希望把輸出都以字串的形式記錄下來的話,也可以寫成:

std::string line;
std::vector<std::string> outline;
namespace bp = boost::process;
bp::ipstream ps;
bp::child c("ping 127.0.0.1", bp::std_out > ps);
while (c.running() && std::getline(ps, line) && !line.empty())
{
	outline.push_back(line);
}

如此一來,ping 的輸出結果都會逐行儲存在 outline 裡面, 也就可以拿來做後續的分析了。

而如果同時要輸出輸入的話,則可以寫成類似下面的樣子:

namespace bp = boost::process;
bp::opstream ops;
bp::ipstream ips;
bp::child c("prog", bp::std_out > ips, bp::std_in < ops);
ops << "1";
std::string str;
ips >> str;
c.wait();

不過如果要和外部程式互動的話,實際上應該會有很多繁瑣的東西要處理就是了。


一般比較可能會用到的功能,大概就是這些了吧?

除了上面的功能之外,其實 child 本身也還有一些其他的函式可以使用;像是他實際上有提供 terminate() 這個函式,理論上應該可以用來中斷所建立的外部程式。但是很遺憾的是,在 Heresy 這邊測試都是失敗的…看來在 Windows 10 的環境下,他並沒有辦法把外部程式真的中斷掉。

另外,他實際上也支援 asynchronous 的操作,或是透過設定 on_eixt、來指定當外部程式結束後、要做什麼動作;在某些其況下,這樣的架構應該也算很實用的。

最後,他也有提供一個 boost::this_process,可以用來取得一些目前 process 的資訊;其中,也可以透過 environment() 這個函式、來取得環境變數;不過,由於 Windows 和 Linux 的環境變數定義差很多,所以如果是寫跨平台的程式,那這邊應該還是得分開處理了。


另外,這邊也補充一下。

根據《Where is Boost.Process?》 這篇,可以看到,實際上 Julio M. Merino Vidal 在之前也有試著開發過 Boost.Process 這個函式庫,他的語法和目前的差很多;而其版本最後應該是 0.5,之後就沒開發了。

不過考量到 Klemens D. Morgenstern 這個版本的 Boost.Process 在 GitHub 上的紀錄是從 0.5 開始、第一個 release 是 0.6,而且在文件上方的 Copyright 還是有保留 Julio M. Merino Vidal 等人的名字,所以應該可以把目前這個版本的 Boost.Process 視為之前版本的大改造吧?

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *