こんにちは。田原です。いよいよ来月から消費増税、頭痛いです。

さて、第8回で軽く触れているのですが、今回は4つの情報提供変数(CMAKE_SOURCE_DIR、CMAKE_BINARY_DIR、CMAKE_CURRENT_SOURCE_DIR、CMAKE_CURRENT_BINARY_DIR)について詳しく解説します。これらの変数はソースの位置やビルド先を獲得できるもので少し複雑なプロジェクトを作る時にたいへん便利です。

1.ビルド・システム生成モード

これら4つの変数は、主にビルド・システム生成モードで使います。

1-1.ビルド・システム生成モードにおける振る舞い

まず、CMAKE_CURRENT_SOURCE_DIRは現在処理している CMakeLists.txt があるフォルダのフルパスです。そして、CMAKE_CURRENT_BINARY_DIRは現在処理している CMakeLists.txt に対応するビルド・フォルダのフルパスです。
CMakeLists.txtが1つの時は「以上で終わり」です。

しかし、あるCMakeLists.txtから、add_subdirectoryで他のCMakeLists.txtをプロジェクトに加えた時に_CURRENTがない2つが有用です。
CMAKE_SOURCE_DIRは「ビルド・システムを生成するためにCMakeコマンドで直接指定」した CMakeLists.txt があるフォルダのフルパスです。そして、CMAKE_BINARY_DIRはそれに対応するビルド・フォルダのフルパスです。

情報提供変数 内容
CMAKE_SOURCE_DIR cmakeコマンドで直接指定したCMakeLists.txtのあるフォルダのフルパス
CMAKE_BINARY_DIR ${CMAKE_SOURCE_DIR}のソースをビルドするフォルダのフルパス
CMAKE_CURRENT_SOURCE_DIR 現在処理中のCMakeLists.txtのあるフォルダのフルパス
CMAKE_CURRENT_BINARY_DIR ${CMAKE_CURRENT_SOURCE_DIR}のソースをビルドするフォルダのフルパス

ちなみにCMakeコマンドで直接指定した CMakeLists.txtを処理している時の CMAKE_CURRENT_SOURCE_DIRとCMAKE_CURRENT_BINARY_DIRは、それぞれCMAKE_SOURCE_DIRとCMAKE_BINARY_DIRと同じです。

1-2.サンプル

ポイントが判りやすいよう極簡単な例を作ってみました。(add_executableさえ入れていません。)

cmake_minimum_required(VERSION 3.10.2)
project(sample)

message(STATUS "----- CMakeLists.txt")
message(STATUS "CMAKE_SOURCE_DIR        =${CMAKE_SOURCE_DIR}")
message(STATUS "CMAKE_BINARY_DIR        =${CMAKE_BINARY_DIR}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "CMAKE_CURRENT_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}")

add_subdirectory(foo)
add_subdirectory(bar)
message(STATUS "----- foo/CMakeLists.txt")
message(STATUS "CMAKE_SOURCE_DIR        =${CMAKE_SOURCE_DIR}")
message(STATUS "CMAKE_BINARY_DIR        =${CMAKE_BINARY_DIR}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "CMAKE_CURRENT_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}")
message(STATUS "----- bar/CMakeLists.txt")
message(STATUS "CMAKE_SOURCE_DIR        =${CMAKE_SOURCE_DIR}")
message(STATUS "CMAKE_BINARY_DIR        =${CMAKE_BINARY_DIR}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "CMAKE_CURRENT_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}")
ビルドシステム生成コマンドの実行結果(関連部のみ抜粋)
> mkdir build
> cd build
> cmake ..
(中略)
-- ----- CMakeLists.txt
-- CMAKE_SOURCE_DIR        =C:/cpp-school/sample
-- CMAKE_BINARY_DIR        =C:/cpp-school/sample/build
-- CMAKE_CURRENT_SOURCE_DIR=C:/cpp-school/sample
-- CMAKE_CURRENT_BINARY_DIR=C:/cpp-school/sample/build
-- ----- foo/CMakeLists.txt
-- CMAKE_SOURCE_DIR        =C:/cpp-school/sample
-- CMAKE_BINARY_DIR        =C:/cpp-school/sample/build
-- CMAKE_CURRENT_SOURCE_DIR=C:/cpp-school/sample/foo
-- CMAKE_CURRENT_BINARY_DIR=C:/cpp-school/sample/build/foo
-- ----- bar/CMakeLists.txt
-- CMAKE_SOURCE_DIR        =C:/cpp-school/sample
-- CMAKE_BINARY_DIR        =C:/cpp-school/sample/build
-- CMAKE_CURRENT_SOURCE_DIR=C:/cpp-school/sample/bar
-- CMAKE_CURRENT_BINARY_DIR=C:/cpp-school/sample/build/bar
(後略)

2.スクリプト・モード

これらの4つの変数がスクリプト・モードで使えるのか?やってみました。
結論は使えます。これら4つの変数全てが作業フォルダのフルパスになります。

確認する際に用いたサンプル・スクリプトを示します。

message(STATUS "----- sample.cmake")
message(STATUS "CMAKE_SOURCE_DIR        =${CMAKE_SOURCE_DIR}")
message(STATUS "CMAKE_BINARY_DIR        =${CMAKE_BINARY_DIR}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "CMAKE_CURRENT_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}")

execute_process(COMMAND ${CMAKE_COMMAND} -P "foo/foo.cmake")
execute_process(COMMAND ${CMAKE_COMMAND} -P "foo.cmake" WORKING_DIRECTORY "foo")
execute_process(COMMAND ${CMAKE_COMMAND} -P "${CMAKE_SOURCE_DIR}/foo/foo.cmake" WORKING_DIRECTORY "C:/")

execute_process
これは外部コマンドを実行するコマンドです。COMMANDにて実行するコマンド・ラインを記述します。${CMAKE_COMMAND}はcmake へ展開されます。WORKING_DIRECTORYの次にそのコマンドを呼び出す時の作業フォルダを指定します。最後の例ではフルパス( “${CMAKE_SOURCE_DIR}/foo/foo.cmake”)でスクリプトを指定しているのは、”C:/”が作業フォルダでcmakeコマンドが呼ばれるからです。このような場合にスクリプト・ファイルを相対パスで指定するのは結構難しいです。

message(STATUS "----- foo.cmake")
message(STATUS "CMAKE_SOURCE_DIR        =${CMAKE_SOURCE_DIR}")
message(STATUS "CMAKE_BINARY_DIR        =${CMAKE_BINARY_DIR}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "CMAKE_CURRENT_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}")
スクリプト実行結果)
> cmake -P sample.cmake
-- ----- sample.cmake
-- CMAKE_SOURCE_DIR        =C:/cpp-school/sample
-- CMAKE_BINARY_DIR        =C:/cpp-school/sample
-- CMAKE_CURRENT_SOURCE_DIR=C:/cpp-school/sample
-- CMAKE_CURRENT_BINARY_DIR=C:/cpp-school/sample
-- ----- foo.cmake
-- CMAKE_SOURCE_DIR        =C:/cpp-school/sample
-- CMAKE_BINARY_DIR        =C:/cpp-school/sample
-- CMAKE_CURRENT_SOURCE_DIR=C:/cpp-school/sample
-- CMAKE_CURRENT_BINARY_DIR=C:/cpp-school/sample
-- ----- foo.cmake
-- CMAKE_SOURCE_DIR        =C:/cpp-school/sample/foo
-- CMAKE_BINARY_DIR        =C:/cpp-school/sample/foo
-- CMAKE_CURRENT_SOURCE_DIR=C:/cpp-school/sample/foo
-- CMAKE_CURRENT_BINARY_DIR=C:/cpp-school/sample/foo
-- ----- foo.cmake
-- CMAKE_SOURCE_DIR        =C:/
-- CMAKE_BINARY_DIR        =C:/
-- CMAKE_CURRENT_SOURCE_DIR=C:/
-- CMAKE_CURRENT_BINARY_DIR=C:/

3.応用例(Boost.Processを使ってみる)

第8回 サブ・プロジェクトとC++形式dllの作り方では、共有ライブラリとそれを呼び出す exe の例を取り上げました。同じものでは面白くないので、今回は作成したexeからexeを呼び出す例を取り上げてみます。(実際に業務用アプリを開発している場面ではexeを呼び出す機会は意外に多いと思います。)

system()関数で呼び出せばお手軽なのですが、呼び出したexeの標準出力を取り込みたいことって多いですよね? そこで、boost 1.64.0でBoost.Processが導入されましたので、これを使ってお手軽に子プロセスの標準出力を受け取るサンプルを作ってみました。(超簡単でびっくりしました。)

まず、使用するboostですが「第10回 find_packageの仕組みと使い方」でビルドしたものをそのまま使います。(Boost.Process自体はヘッダオンリのようですが、Boost.FileSystemに依存しているためBoostをビルドする必要があります。)

ubuntuについて
すいません。最近、ubuntuのことをすっかり忘れていました。久しぶりにubuntuを起動しようとするとVirtualBox上のubuntuがおかしくなっていたので、新たにVMWareに Ubuntu 18.04.2 LTS をインストールし確認しました。
ubuntuインストール後に、以下のコマンドで必要な開発ツールをインストールできます。
> sudo apt-get install g++
> sudo apt-get install cmake
> sudo apt-get install libboost-dev
> sudo apt-get install libboost-filesystem-dev
boostは必要なライブラリを手動でインストールする必要がありますが、今回のサンプルは上記で大丈夫でした。

それではCMakeLists.txtとソースです。

cmake_minimum_required(VERSION 3.10.2)
project(sample)

if(MSVC)
    string(REPLACE "/W3" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /EHsc")
else()
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11")
endif()

include_directories("${CMAKE_SOURCE_DIR}")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")

add_subdirectory(parent)
add_subdirectory(child)

この11行目と12行目でCMAKE_SOURCE_DIRとCMAKE_BINARY_DIRを使っています。

前者のinclude_directories("${CMAKE_SOURCE_DIR}")は、version.hをルートCMakeLists.txtのあるフォルダに置いていますので、ルート・フォルダをインクルード・パスに含めるために指定しています。プロジェクト全体で共通な設定ファイルを(数が少ない時は)ソース群のルートに置くと結構便利です。

後者のset(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")にて複数のバイナリを1箇所のフォルダへ集めてこれます。他の方法でコピーしてもよいのですが、各種のIDEでデバッグする際にはコピーではIDEが認識できずデバッグ用の設定が面倒になることがあります。

message(STATUS "BOOST_ROOT=${BOOST_ROOT}")
set(Boost_USE_STATIC_LIBS    ON)
set(Boost_USE_MULTITHREADED  ON)

find_package(Boost REQUIRED COMPONENTS filesystem)

message(STATUS "  Boost_INCLUDE_DIR=${Boost_INCLUDE_DIR}")
message(STATUS "  Boost_LIBRARY_DIR=${Boost_LIBRARY_DIR}")
message(STATUS "  Boost_LIBRARIES  =${Boost_LIBRARIES}")

include_directories("${Boost_INCLUDE_DIRS}")
link_directories("${Boost_LIBRARY_DIR}")

add_executable(parent parent.cpp)
target_link_libraries(parent ${Boost_LIBRARIES})

boost用の基本設定(find_package周辺)については、第10回 find_packageの仕組みと使い方を参照下さい。

#ifdef _MSC_VER
    #define DISABLE_WARN   __pragma(warning(push)) __pragma(warning(disable:4244))
    #define ENABLE_WARN    __pragma(warning(pop))
#else
    #define DISABLE_WARN
    #define ENABLE_WARN
#endif

#include <iostream>
DISABLE_WARN
#include <boost/process.hpp>
#include <boost/filesystem.hpp>
ENABLE_WARN
#include "version.h"

namespace boostP = boost::process;
namespace boostF = boost::filesystem;

int main(int, char* argv[])
{
    std::cout << "parent " VERSION_NO "\n";

    boostP::ipstream pipe_stream;
    boostF::path dir(argv[0]);
    std::string command = boostF::absolute(dir.parent_path()).string()+"/child arg1 arg2 arg3";
    std::cout << "command = " << command << "\n";
    boostP::child process(command, boostP::std_out > pipe_stream);
    process.wait();

    std::string line;
    while (pipe_stream && std::getline(pipe_stream, line))
    {
        std::cout << line << std::endl;
    }
}

Boost.Processを呼び出している部分はboost公式のサンプルを少し修正して作りました。
公式のサンプルでは子プロセスからの標準出力の空行までを受け取った後でc.wait()しています。
どうせなら全部受け取りたいので先に子プロセスが終了してから標準出力を受け取っています。
時間のかからない処理を子プロセスへ依頼することも多いと思いますので、その際にはこのような呼び出し方はお手軽です。

時間のかかる処理を子プロセスへ依頼する時
メイン・スレッドを専有するとアプリがユーザの操作に応答できなくなるので、そのような時はサブ・スレッドで子プロセスの応答を待つ、非同期処理を用いて子プロセスの応答を待つ等の処理を行うことが多いです。std::threadBoost.Asio等を使うとスマートに実装できると思います。

次に、command変数へコマンド・ラインを設定してからboost::process::childを使って実行しています。command変数を設定する際に子プロセスのexeをフルパスで指定しています。parent.exeとchile.exeは同じフォルダに置くことが多いので、つい相対パスで指定してしまう場合が多いのですが、後で結構ハマります。(実は今回もやらかして急いで修正しました。)
parent.exeを起動する際の作業フォルダがparent.exeがあるフォルダとは限らないからです。例えば、Visual Studioから起動すると特に指定しなければプロジェクトのビルド・フォルダが作業フォルダとなって起動されます。(コマンド・プロンプトからなら起動するのに、Visual Studioから起動しないケースはこの「罠」にハマった時です。ははは)

msvcで発生する警告のディセーブル
msvcでビルドするとboost 内部で C4244警告がでます。boostはmsvcでの動作検証は行われている筈ですから、この警告はじゃまになるだけですのでディセーブルするため、ソース先頭で DISABLE_WARN を定義し、boostのヘッダのインクルード時だけ警告を禁止いています。
このテクニックは、Visual C++のC4996警告への対処方法についてで解説していますので、よかったら参照して下さい。

add_executable(child child.cpp)
#include <iostream>
#include "version.h"

int main(int argc, char* argv[])
{
    std::cout << "child " VERSION_NO "\n";
    for (int i=0; i < argc; ++i)
    {
        std::cout << "    argv[" << i << "]=" << argv[i] << "\n";
    }
}
parent実行結果
parent v1.2.3
command = C:\cpp-school\sample\build\bin\Release/child arg1 arg2 arg3
child v1.2.3
    argv[0]=C:\cpp-school\sample\build\bin\Release/child
    argv[1]=arg1
    argv[2]=arg2
    argv[3]=arg3

4.まとめ

今回はCMakeでプロジェクトを作る際に便利な4つのCMake変数について解説しました。振る舞い自体は単純ですが結構よく使う便利な変数です。
今回は、第8回で既に示しているのでサンプルに結構悩みました。そこで、第8回のexeとdllを1つのプロジェクトに含む場合とは異なるexeがexeを呼び出すプロジェクトを作ってみました。その際、標準出力をパイプで読み取れる方法を探したところ、boostに超便利なモジュールがあったので「これだ!」と紹介しました。
std_out > pipe_streamでリダイレクトするって衝撃的ですよね。(operator>()を定義しているのだと思います。)

私自身は最近はQtを使った開発が多いのでQProcessを良く使っています。Qtも非常に強力なのですがQtはboostより遥かに巨大でお手軽ではないしライセンスもちょっと面倒です。そこで、boostの方をご紹介しました。

それでは今回はこれにて終わります。お疲れ様でした!