はじめに
Boostの「Statechart Library」を使ってみます。
きっかけは、ソースコードからUML(PlautUML形式)を出力できる記事を見かけて、興味をもったので調べてみました。
Boost状態遷移ライブラリについて
Boostには状態遷移ライブラリが2つ存在します。
私見ですが、特徴を記載します。
-
Statechart Library
状態遷移図ベースでコードを記載する。実行プログラムからPlantUMLの状態遷移図を出力するツールがあるので仕様通り実装しているかをコードを見なくてもUMLで確認できる(強力かも)。
ただし、設計の漏れ・抜けは状態遷移表などを作り確認する必要がある。 -
Meta State Machine
こちらは使ったことがあります。
状態遷移表の考えを素直にコードにしやすく、状態遷移テーブルをeUMLという記法で記載するので、そこを見ればだいたいの全体像はつかめる。(公式ドキュメント)
ただし、仕様通りに作っているかは状態遷移表を読み込まないと確認できない。
どっちが良いというのではなく、それぞれの特性を理解して使い分けるのが良いと感じました。
実行環境
今回、動作確認に使った環境です。
項目 | 内容 |
---|---|
OS | Ubuntu 20.04.6 LTS |
Boost | Version 1.71(ROS noeticでインストールされたものを使用) |
gtest | Googleのテストフレームワーク(Version 1.8) |
bosce | UML出力ツール(GitHub masterブランチ) |
Boost Statechart LibraryのチュートリアルStop Watchが理解できていることが前提となります。
付録に、Boostのチュートリアルを少し改造した今回使ったソースコードを載せています。
Statechart Libraryについて
UMLが出力できるので便利なのですが、テストで確認できないと積極的に使えないので、「UML出力の方法」と「google testで使えるか」を調べました。
UML(PlantUML形式)出力
UML出力は、bosceというプログラムを利用します。
ソースコードではなく、バイナリを解析してUML出力します。
解析対象のプログラムは、コンパイル時にデバッグオプション「-g」を付ける必要があります。
使い方は、以下の通りです。
$ cd 実行ファイルがあるディレクトリ
$ bosce 実行ファイル名 -l
Available state-machines:
「状態遷移マシン名」が表示
$ bosce 実行ファイル名 -s 状態遷移マシン名 > uml.pu
uml.puがPlantUML形式になっています。
複数の状態遷移マシンに対応しているのが良いですね。
以下、今回確認したプログラムから出力したUMLです。
bosceは、付録の手順で実行すると自動でコンパイルするようになっています。
google testで使えるか?
いつも使っているgoogle testを使ってテストできるかを確認しました。
こちらもサンプルコードを付録に載せておきます。
以下、確認のポイントです。
- 現在の状態を確認できるか?
- ハンドリングされていないイベントを確認できるか?
現在の状態を確認できるか?
API仕様を見ても現在の状態を取得するものを見つけられませんでした。
いろいろ試して以下のようにすれば取得できることが分かりました(他に良い方法があるかもしれません)。
C++なのでシンボル名はマングル処理されているので、デマングル処理が必要になります。
std::string cur_state;
StopWatch myWatch; // 状態遷移エンジン
myWatch.initiate(); // 初期化
cur_state = demangle(typeid(*(myWatch.state_begin())).name());
// cur_stateに現在の状態名が入っている
ハンドリングされていないイベントを確認できるか?
API仕様からunconsumed_event関数を定義すれば、ハンドリングされていないイベントを確認できることがわかりました。
今回は、unconsumed_event関数内で警告表示するようにしました。
// -- [状態エンジン定義] -----------------------------------------------
struct Active; // 前方参照用
struct StopWatch : sc::state_machine< StopWatch, Active > {
/**
* @brief ハンドリングされていないイベントが来た時によばれる関数
* @param[in] e 通知イベント
*/
void unconsumed_event( const sc::event_base &e) {
std::cout << "[Warn] Ignore Event : \""
<< demangle(typeid(e).name()) << "\" on \""
<< demangle(typeid(*(this->state_begin())).name()) << "\" state." << std::endl;
}
};
google testを使ってテストしてみた
チュートリアルの仕様に「Lap」イベントを追加しました。
確認コードでは、Stopped状態でLapイベントのハンドリングをわざと実装していません。
以下、実行結果を示しています。
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from StatechartTest
[ RUN ] StatechartTest.001
[Warn] Ignore Event : "EvLap" on "Stopped" state.
[Info][Running State] process Lap!!
[ OK ] StatechartTest.001 (0 ms)
[----------] 1 test from StatechartTest (0 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (0 ms total)
[ PASSED ] 1 test.
実行結果の[Warn]を見たらStopped状態でLapイベントがハンドリングされていないことがわかります。
まとめ
これまでMeta State Machine しか知らなかったので、勉強になりました。
実行ファイルからUMLの状態遷移図を出力できるので仕様の確認に効果を発揮しそうです。機会があれば使ってみようと思います。
Boostに限らずC++の標準ライブラリやOSSもたくさんあるのですが、調べるにしても時間がかかります。まずは、他人に進められたものや使っているものを調べるのが良いと思っています。
Boostとの出会い
Boostを知ったきっかけは、会社の同僚がログを出力するのにBoost::Logを使っていました。「これ何?」って聞いてみると、Boostというオープンソースなライブラリで便利なライブラリがたくさんあるということでした。
実際調べてみると(公式Link)…たくさんあることがわかりました。最初は、同僚が使っていたBoost::Logから調べました(かなり便利です)。
「作らずして作る」 が私のモットーなので
即、Boost教に入信しました。
使えるものがあるのに作ってしまうと、作ったところをテストしたりレビューしたり効率面からもよろしくないですし、自分としては何か損をした気分になります。
また、OSSのコードを見るとC++の勉強にもなっています。ただし、Boostはバージョンアップが激しく、バージョンが変わったらコンパイルエラーになる可能性が高いので、そこが注意点です。
付録
ファイルが3つあります(CMakeLists.txt, StateMachine.hpp, main.cpp)。
同じディレクトリに3つのファイルを置いて、以下のコマンドで実行できます。
google testやbosceもcmake時にGitHubから取得してコンパイルするようにしています。
# コンパイル
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
make
# テスト実行
./stm_sample
# 状態遷移エンジン確認
./bosce stm_sample -l
# UML出力
./bosce stm_sample -s StopWatch > uml.pu
CMakeLists.txt
## ==================================================================
## Boost状態遷移サンプル
## ==================================================================
cmake_minimum_required(VERSION 3.11.0)
project(stm_sample
VERSION 1.0.0
LANGUAGES C CXX)
## C++コンパイラ: C++14指定
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS "-O1 -Wall")
## $ cmake -DCMAKE_BUILD_TYPE=Debug ..
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g")
## $ cmake -DCMAKE_BUILD_TYPE=Release ..
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
## for Google Test : download & compile
include(FetchContent)
FetchContent_Declare(
gtest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.8.x
)
FetchContent_MakeAvailable(gtest)
## for bosce : download & compile
include(ExternalProject)
ExternalProject_Add(bosce
GIT_REPOSITORY https://github.com/kanje/bosce.git
GIT_SHALLOW OFF
INSTALL_COMMAND cp ${CMAKE_BINARY_DIR}/bosce-prefix/src/bosce-build/bosce ${CMAKE_BINARY_DIR}
)
## for Boost
find_package(Boost COMPONENTS system REQUIRED)
## ソースファイル
file(GLOB_RECURSE SRCS ${PROJECT_SOURCE_DIR}/main.cpp)
## 実行ファイル定義
add_executable(${PROJECT_NAME}
${SRCS}
)
## Linkライブラリ定義
target_link_libraries(${PROJECT_NAME}
${Boost_LIBRARIES}
gtest
)
StopWatch.hpp
/**
* @file StopWatch.hpp
* @brief statechart状態遷移サンプル
* 以下のコードを改変
* https://www.boost.org/doc/libs/1_71_0/libs/statechart/doc/tutorial.html#BasicTopicsAStopWatch
*/
#include <cstdio>
#include <ctime>
#include <thread>
#include <iostream>
#include <typeinfo>
#include <vector>
#include <boost/type_index.hpp>
#include <boost/statechart/state_machine.hpp>
#include <boost/statechart/simple_state.hpp>
#include <boost/statechart/event.hpp>
#include <boost/statechart/in_state_reaction.hpp>
#include <boost/statechart/transition.hpp>
#include <boost/statechart/custom_reaction.hpp>
#include <boost/mpl/list.hpp>
#include <boost/timer/timer.hpp>
#ifndef INCLUDE_STOP_WATCH_HPP_
#define INCLUDE_STOP_WATCH_HPP_
namespace sc = boost::statechart;
namespace mpl = boost::mpl;
// -- [Lib] ---------------------------------------------------
/**
* @brief マングルされた文字をデマングル文字にする
* @param[in] mangle_name マングリングされた文字列
* @return デマングルした文字
*/
inline std::string demangle(const char *mangle_name) {
int status;
char *law_data = abi::__cxa_demangle(mangle_name, 0, 0, &status);
std::string ret_str(law_data);
std::free(law_data);
return ret_str;
}
// -- [イベント定義] ---------------------------------------------------
struct EvStartStop : sc::event< EvStartStop > {}; //! 開始・停止イベント
struct EvReset : sc::event< EvReset > {}; //! リセットイベント
struct EvLap : sc::event< EvLap > {}; //! ラップイベント
// -- [状態エンジン定義] -----------------------------------------------
struct Active; // 前方参照用
struct StopWatch : sc::state_machine< StopWatch, Active > {
/**
* @brief Actionが定義されておらず、イベントが来た時によばれる関数
* @param[in] e 通知イベント
*/
void unconsumed_event( const sc::event_base &e) {
std::cout << "[Warn] Ignore Event : \""
<< demangle(typeid(e).name()) << "\" on \""
<< demangle(typeid(*(this->state_begin())).name()) << "\" state." << std::endl;
}
};
// -- [状態定義] ---------------------------------------------------
struct Stopped; // 前方参照
//(状態 , 親状態 , 子状態)
struct Active : sc::simple_state<Active, StopWatch, Stopped > {};
// サンプルから加筆 (状態 , 親状態)
struct Running :sc::simple_state< Running, Active > {
public:
/**
* @brief この状態の受付けイベント定義
*/
typedef mpl::list<
sc::custom_reaction< EvLap >, // EvLapが来たらreact関数を呼び出し
sc::transition< EvStartStop, Stopped > // EvStartStopが来たらStoppedへ遷移
> reactions;
/**
* @brief Lapイベントが来た時の処理
* @param[in] evt Lapイベント
* @return Running状態へ遷移(遷移しない)
*/
sc::result react( const EvLap &evt) {
std::cout << "[Info][Running State] process Lap!!" << std::endl;
return transit< Running >();
}
/**
* @brief StartStopイベントが来た時の処理
*
* @param[in] evt StartStopイベント
* @return Stoppe状態へ遷移
*/
sc::result react (const EvStartStop &evt) {
std::cout << "[Running State] StartStop!!" << std::endl;
return transit< Stopped >();
}
};
/**
* @brief 停止状態定義
*/
struct Stopped : sc::simple_state< Stopped, Active > {
// EvStartStopが来たら何もせず、Running状態へ遷移
typedef sc::transition< EvStartStop, Running > reactions;
// EvLapイベントは、わざと実装していない。
};
#endif /* INCLUDE_STOP_WATCH_HPP_ */
main.cpp
/**
* @file main.cpp
* @brief google testサンプル
*/
#include <cstdio>
#include "stop_watch.hpp"
#include <gtest/gtest.h>
// 簡単なテスト
TEST(StatechartTest, 001) {
std::string cur_state;
StopWatch myWatch;
myWatch.initiate();
// 初期状態は、Stopped
cur_state = demangle(typeid(*(myWatch.state_begin())).name());
EXPECT_STREQ(cur_state.c_str(), "Stopped");
// Stopped状態でLapイベント発行:状態遷移なし
myWatch.process_event( EvLap());
cur_state = demangle(typeid(*(myWatch.state_begin())).name());
EXPECT_STREQ(cur_state.c_str(), "Stopped");
// Stopped状態でStartStopイベント発行:Running状態へ遷移
myWatch.process_event( EvStartStop() );
cur_state = demangle(typeid(*(myWatch.state_begin())).name());
EXPECT_STREQ(cur_state.c_str(), "Running");
// Running状態でLapイベント発行:状態遷移なし
myWatch.process_event( EvLap());
cur_state = demangle(typeid(*(myWatch.state_begin())).name());
EXPECT_STREQ(cur_state.c_str(), "Running");
// Running状態でStartStopイベント発行:Stopped状態へ遷移
myWatch.process_event( EvStartStop() );
cur_state = demangle(typeid(*(myWatch.state_begin())).name());
EXPECT_STREQ(cur_state.c_str(), "Stopped");
// Stopped状態でStartStopイベント発行:Running状態へ遷移
myWatch.process_event( EvStartStop() );
cur_state = demangle(typeid(*(myWatch.state_begin())).name());
EXPECT_STREQ(cur_state.c_str(), "Running");
// Running状態でリセットイベント発行:状態遷移なし
myWatch.process_event( EvReset() );
cur_state = demangle(typeid(*(myWatch.state_begin())).name());
EXPECT_STREQ(cur_state.c_str(), "Running");
}
// Google Test メイン
int main(int argc, char *argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}