Skip to content

Commit

Permalink
Allow deferring calls to lambdas and free functions
Browse files Browse the repository at this point in the history
  • Loading branch information
rmed committed Jun 29, 2024
1 parent bd5acc0 commit d03d651
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 10 deletions.
1 change: 1 addition & 0 deletions include/kouta/base/callback/base-callback.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ namespace kouta::base::callback
class BaseCallback
{
public:
/// @brief The type of the callable the Callback points to.
using Callable = std::function<void(TArgs...)>;

/// @brief Default constructor.
Expand Down
32 changes: 30 additions & 2 deletions include/kouta/base/callback/deferred-callback.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ namespace kouta::base::callback
///
/// Because calling a deferred Callback posts an event to the destination's thread, **arguments passed to the
/// callback must be copied during the call**, meaning that the following types of arguments should not be used
/// (or used with much care to prevent invalid memory access):
/// (or used with much care to prevent invalid memory access or race conditions):
///
/// - Pointers
/// - Raw pointers
/// - Non-copyable objects
/// - Objects referencing, but not owning, dynamic memory (e.g. @ref std::span)
///
Expand All @@ -26,6 +26,9 @@ namespace kouta::base::callback
class DeferredCallback : public BaseCallback<TArgs...>
{
public:
/// @brief The type of the callable the Callback points to.
using Callable = BaseCallback<TArgs...>::Callable;

// Copyable
DeferredCallback(const DeferredCallback&) = default;
DeferredCallback& operator=(const DeferredCallback&) = default;
Expand Down Expand Up @@ -59,5 +62,30 @@ namespace kouta::base::callback
object->template post<TClass, TArgs...>(method, args...);
});
}

/// @brief Callback constructor from a callable.
///
/// @details
/// This constructor initializes the intenal callable to the one specified in @p callable , so that it is
/// invoked from within the context of the provided @p object , which must implement a `post()` method to which
/// the invokation can be passed to defer execution.
///
/// The developer must guarantee the lifetime of the @p object to rpevent invalid memory access.
///
/// @tparam TClass Object type.
///
/// @param[in] object Pointer to the object whose method is going to be called.
/// @param[in] callable Callable to store. For instance, this could be a lambda or anything convertible
/// to `std::function`.
template<class TClass>
DeferredCallback(TClass* object, const DeferredCallback::Callable& callable)
: BaseCallback<TArgs...>{}
{
this->set_callable(
[object, callable](TArgs... args)
{
object->template post<TArgs...>(callable, args...);
});
}
};
} // namespace kouta::base::callback
35 changes: 29 additions & 6 deletions include/kouta/base/component.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

#include <boost/asio.hpp>

#include <kouta/base/callback.hpp>

namespace kouta::base
{
/// @brief Base class for asynchronous components.
Expand All @@ -22,7 +20,8 @@ namespace kouta::base
/// hence its lifetime must, at least, surpass that of the child.
explicit Component(Component* parent = nullptr)
: m_parent{parent}
{}
{
}

// Not copyable
Component(const Component&) = delete;
Expand Down Expand Up @@ -50,12 +49,12 @@ namespace kouta::base
///
/// @warning Arguments are **copied** before being passed to the event loop.
///
/// @tparam TClass Child class whose method is going to be executed.
/// @tparam TClass Child class whose method is going to be invoked.
/// @tparam TMethodArgs Types of the arguments that the method accepts.
/// @tparam TArgs Types of the arguments provided to the invocation.
///
/// @param[in] method Method to execute. Its signature must match `void(TArgs...)`
/// @param[in] args Arguments to execute the method with.
/// @param[in] method Method to invoke. Its signature must match `void(TArgs...)`
/// @param[in] args Arguments to invoke the method with.
template<class TClass, class... TMethodArgs, class... TArgs>
void post(void (TClass::*method)(TMethodArgs...), TArgs... args)
{
Expand All @@ -67,6 +66,30 @@ namespace kouta::base
});
}

/// @brief Post a function call to the event loop for deferred execution.
///
/// @details
/// This allows other components, even those residing in another thread/event loop, to post a functor to this
/// specific component, for example a lambda function.
///
/// @warning Arguments are **copied** before being passed to the event loop.
///
/// @tparam TFuncArgs Types of the arguments that the function accepts.
/// @tparam TArgs Types of the arguments provided to the invocation.
///
/// @param[in] functor Functor to invoke invoked. Its signature must match `void(TArgs...)`
/// @param[in] args Arguments to invoke the functor with.
template<class... TFuncArgs, class... TArgs>
void post(const std::function<void(TFuncArgs...)>& functor, TArgs... args)
{
boost::asio::post(
context().get_executor(),
[functor, args...]()
{
functor(std::move(args)...);
});
}

private:
Component* m_parent;
};
Expand Down
19 changes: 17 additions & 2 deletions tests/base/test-base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,17 @@ namespace kouta::tests::base
RootMock root{};

callback::DeferredCallback<std::uint16_t> cb_a{&root, &RootMock::handler_a};
callback::DeferredCallback<std::uint16_t> cb_a_2{
&root,
[&root](std::uint16_t value)
{
root.handler_a(value);
}};
callback::DirectCallback<std::int32_t, const std::string&> cb_b{&root, &RootMock::handler_b};
callback::DeferredCallback<const std::vector<std::uint8_t>&> cb_c{&root, &RootMock::handler_c};

std::uint16_t data_a{127};
std::uint16_t data_a_2{564};

std::int32_t data_b_a{42};
std::string data_b_b{"this is a test"};
Expand All @@ -86,7 +93,9 @@ namespace kouta::tests::base

EXPECT_CALL(root, handler_c(data_c_copy)).Times(1);

EXPECT_CALL(root, handler_a(data_a))
EXPECT_CALL(root, handler_a(data_a)).Times(1);

EXPECT_CALL(root, handler_a(data_a_2))
.WillOnce(
[&root]
{
Expand All @@ -99,6 +108,7 @@ namespace kouta::tests::base

cb_a(data_a);
cb_b(data_b_a, data_b_b);
cb_a_2(data_a_2);

alarm(1);
root.run();
Expand All @@ -123,7 +133,12 @@ namespace kouta::tests::base
callback::CallbackList<std::int32_t, const std::string&> cb_list_deferred{
callback::DeferredCallback{&root, &RootMock::handler_b},
callback::DeferredCallback{&root, &RootMock::handler_b},
callback::DeferredCallback{&root, &RootMock::handler_b}};
callback::DeferredCallback<std::int32_t, const std::string&>{
&root,
[&root](std::int32_t value_a, const std::string& value_b)
{
root.handler_b(value_a, value_b);
}}};

std::uint16_t data_a{127};

Expand Down

0 comments on commit d03d651

Please sign in to comment.