c++boost-asiocoroutineboost-msm

Boost MSM + ASIO with coroutines. A Simple test case not working


I'm trying to build a small example combining two boost examples from the docs. The first one is an example from the MSM (state machines) library: https://www.boost.org/doc/libs/1_75_0/libs/msm/doc/HTML/examples/AnonymousTutorialWithFunctors.cpp the second one is the echo server (with coroutines) example from Asio: https://www.boost.org/doc/libs/1_75_0/doc/html/boost_asio/example/cpp17/coroutines_ts/refactored_echo_server.cpp The echo server example runs correctly with my machine.

The state machine has 2 states, AsioInitState and RegisterServersState with only one anonymous transition on the transition table.

Following my source code: EchoServerMSM.h

#pragma once

// back-end
#include <boost/msm/back/state_machine.hpp>
//front-end
#include <boost/msm/front/state_machine_def.hpp>
// functors
#include <boost/msm/front/functor_row.hpp>
#include <boost/msm/front/euml/common.hpp>

// Asio
#include <boost/asio.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/signal_set.hpp>

namespace msm = boost::msm;
namespace mpl = boost::mpl;
using namespace boost::msm::front;

using boost::asio::ip::tcp;
using boost::asio::awaitable;
using boost::asio::co_spawn;
using boost::asio::detached;
using boost::asio::use_awaitable;
namespace this_coro = boost::asio::this_coro;

struct EchoServerSMFE : public msm::front::state_machine_def<EchoServerSMFE>
{
    // The list of FSM states
    struct AsioInitState : public msm::front::state<>
    {
        template <class Event, class FSM>
        void on_entry(Event const&, FSM& fsm) {
            std::cout << "entering: AsioInitState" << std::endl;
            // Signals Mask for ASIO
            boost::asio::signal_set signals(*(fsm.io_context), SIGINT, SIGTERM);
            signals.async_wait([&](auto, auto) { fsm.io_context->stop(); });
        }
        template <class Event, class FSM>
        void on_exit(Event const&, FSM&) { std::cout << "leaving: AsioInitState" << std::endl; }
    };
    struct RegisterServersState : public msm::front::state<>
    {
        template <class Event, class FSM>
        void on_entry(Event const&, FSM& fsm) {
            try {
                std::cout << "entering: RegisterServersState" << std::endl;
                co_spawn(*(fsm.io_context), boost::bind(&RegisterServersState::listener<Event, FSM>, this), detached);
                std::cout << "entering end: RegisterServersState" << std::endl;
            }
            catch (const boost::system::system_error& e) {
                std::cout << "> [Error - Server::start]: " << e.what() << std::endl;
            }
        }
        template <class Event, class FSM>
        void on_exit(Event const&, FSM&) { std::cout << "leaving: RegisterServersState" << std::endl; }

        template <class Event, class FSM>
        awaitable<void> listener() // all function is copy pasted from boost EchoServer example with added std::cout logs
        {
            auto executor = co_await this_coro::executor;
            tcp::acceptor acceptor(executor, { tcp::v4(), 55555 });
            for (;;)
            {
                std::cout << "listener - 0" << std::endl;
                tcp::socket socket = co_await acceptor.async_accept(use_awaitable);
                std::cout << "listener - 1" << std::endl;
                co_spawn(executor, echo(std::move(socket)), detached);
            }
        }
        awaitable<void> echo_once(tcp::socket& socket) // all function is copy pasted from boost EchoServer example
        {
            char data[128];
            std::size_t n = co_await socket.async_read_some(boost::asio::buffer(data), use_awaitable);
            co_await async_write(socket, boost::asio::buffer(data, n), use_awaitable);
        }
        awaitable<void> echo(tcp::socket socket)  // all function is copy pasted from boost EchoServer example
        {
            try
            {
                for (;;)
                {
                    // The asynchronous operations to echo a single chunk of data have been
                    // refactored into a separate function. When this function is called, the
                    // operations are still performed in the context of the current
                    // coroutine, and the behaviour is functionally equivalent.
                    co_await echo_once(socket);
                }
            }
            catch (std::exception& e)
            {
                std::printf("echo Exception: %s\n", e.what());
            }
        }
    };
    // the initial state of the player SM. Must be defined
    typedef AsioInitState initial_state;
    struct transition_table : mpl::vector<
        //             Start                Event                 Next                                Action               Guard
        //  +------------------------+----------------------------+-----------------------------+---------------------+----------------------+
        Row < AsioInitState          , none                       , RegisterServersState                                                     >
    > {};
    boost::asio::io_context* io_context = { nullptr };
};
// Pick a back-end
typedef msm::back::state_machine<EchoServerSMFE> EchoServerSM;

main.cpp

#include <iostream>
#include "EchoServerMSM.h"

using namespace std;


int main()
{
    EchoServerSM echoServer;
    boost::asio::io_service ios(1);

    echoServer.io_context = &ios;
    echoServer.start();
    ios.run();
    cout << "Hello CMake." << endl;

    return 0;
}

the console output I get is:

entering: AsioInitState
leaving: AsioInitState
entering: RegisterServersState
entering end: RegisterServersState
listener - 0
Hello CMake.

C:\Users\Andrea\source\repos\TGFLocalClient\out\build\x64-Debug (default)\TGFLocalClient\TGFLocalClient.exe (process 18280) exited with code 0.
To automatically close the console when debugging stops, enable Tools->Options->Debugging->Automatically close the console when debugging stops.
Press any key to close this window . . .

EDIT: a cmake to compile it(win or mac):

cmake_minimum_required (VERSION 3.8)

set(CMAKE_CXX_STANDARD 17)

# FOR MAC
# set(CMAKE_CXX_FLAGS "-fcoroutines-ts")

#FOR WIN
# set(CMAKE_CXX_FLAGS "/await /EHsc")

# Boost section

# set(Boost_DEBUG "ON")
set(Boost_LIB_PREFIX "lib")
set(BOOST_ROOT "C:/Users/YourFolder/boost_1_75_0")
set(BOOST_LIBRARIES "date_time" "regex")


find_package(Boost COMPONENTS ${BOOST_LIBRARIES})

message(STATUS "${BOOST_ROOT}")
message(STATUS "${BOOST_FOUND}")            
message(STATUS "${Boost_INCLUDE_DIRS}")     
message(STATUS "${Boost_LIBRARY_DIRS}")     
message(STATUS "${BOOST_LIBRARIES}")        
message("${Boost_regex_FOUND}")
message("${Boost_regex_LIBRARY}")

include_directories("${Boost_INCLUDE_DIRS}")

# Add source to this project's executable.
add_executable (TGFLocalClient "main.cpp" "EchoServerMSM.h")

target_link_libraries(TGFLocalClient Boost::date_time Boost::regex)

Solution

  • void on_entry(Event const & /*unused*/, FSM & /*unused*/) {
        std::cout << "entering: AsioInitState" << std::endl;
        // Signals Mask for ASIO
        boost::asio::signal_set signals(*(fsm.io_context), SIGINT,
                                        SIGTERM);
        signals.async_wait([&](auto, auto) { fsm.io_context->stop(); });
    }
    

    There's a problem there.

    1. signals is a local variable. On exiting the on_entry function, it is destructed.
    2. This means that it will cancel any pending async operations.
    3. This in turn means that the completion handler is called with ec = boost::asio::error::operation_aborted.
    4. The completion handler doesn't check the error code and just calls stop on the io_service.

    Nothing else happens since the service has been forcefully stopped, always.