Skip to content

SO 5.8 InDepth Send Functions

Yauheni Akhotnikau edited this page Apr 20, 2023 · 1 revision

There are two kinds of send functions in SObjectizer. The first kind uses dynamic allocation of a message object. The second kind works with preallocated message instances.

send-function with the allocation of message objects

This send-function is a variadic template function with the following prototype:

template<typename Msg, typename Target, typename... Msg_Constructor_Args>
void send(Target && target, Msg_Constructor_Args && ...args);

A reference to an agent, instance of so_5::mbox_t or so_5::mchain_t can be passed as target argument.

All args arguments are forwarded to the call of a constructor of Msg type.

For example, if we have a message of that type:

class my_message final : public so_5::message_t {
public:
  int a_;
  char b_;
  my_message(int a, char b) : a_{a}, b_{b} {}
};

and call:

so_5::send<my_message>(some_target, 1, 'c');

Then this call will be transformed to something like that:

// Step 1: construction of a message instance.
auto msg__ = std::make_unique<my_message>(1, 'c');
// Step 2: delivery of the constructed message.
so_5::some::low::level::stuff::deliver(some_target, std::move(msg__));

In the case, if my_message is not derived from so_5::message_t:

struct my_message {
   int a_;
   char b_;
};

The actual code behind the send function will be somewhat more complex:

// Step 1: construction of a message instance.
// Note the creation of so_5::user_type_message_t<T>.
auto msg__ = std::make_unique< so_5::user_type_message_t<my_message> >(1, 'c');
// Step 2: delivery of the constructed message.
so_5::some::low::level::stuff::deliver(some_target, std::move(msg__));

It is important to note that send is a variadic template function that uses perfect-forwarding of its arguments to the constructor of message object. This perfect-forwarding should be taken into account if message contains plain-C array as a field. For example,

struct msg_with_c_array {
   int data_[8];
   int len_;
};

We can initialize an instance of that type by using the initializer list in the following form:

msg_with_c_array msg{
  {1, 2, 3, 4, 5, 6, 7, 8},
  8
};

But we'll get a compilation error on attempt to send message of that type:

so_5::send<msg_with_c_array>(some_target, {1, 2, 3, 4, 5, 6, 7, 8}, 8);

It is because compiler knows what {1, 2, 3, 4, 5, 6, 7, 8} means in that context:

msg_with_c_array msg{
  {1, 2, 3, 4, 5, 6, 7, 8},
  8
};

but doesn't know what {1, 2, 3, 4, 5, 6, 7, 8} means in arguments of send.

Even if we write send as that:

so_5::send<msg_with_c_array>(some_target, std::initializer_list{1, 2, 3, 4, 5, 6, 7, 8}, 8);

it doesn't help because the compiler will report an error in the constructor of so_5::user_type_message_t. To avoid that error it is necessary to explicitly define a constructor for msg_with_c_array that accepts std::initializer_list and int:

struct msg_with_c_array {
   int data_[8];
   int len_;

   msg_with_c_array(std::initializer_list<int> data, int len)
      :  len_{len}
   {
      std::copy(std::begin(data), std::end(data), std::begin(data_));
   }
};

In that case the call:

so_5::send<msg_with_c_array>(some_target, std::initializer_list{1, 2, 3, 4, 5, 6, 7, 8}, 8);

Well be compiled successfully.

But it is much easier to use std::array instead of plain-C arrays:

struct msg_with_array {
   std::array<int, 8> data_;
   int len_;
};

In that case we can write like that:

so_5::send<msg_with_array>(mbox, std::array{1, 2, 3, 4, 5, 6, 7, 8}, 8);

send-functions for working with preallocated objects

There are two forms of send-function which allow to send already allocated object. The first form accepts mhood_t<Msg> and intended to be used for redirection of received messages. For example:

class load_balancer final : public so_5::agent_t {
   // Handler for an incoming message.
   void on_new_message(mhood_t<some_message> cmd) {
      auto mbox = detect_target_for_next_message(*cmd);
      // Redirection of an existing message.
      so_5::send(mbox, std::move(cmd));
   }
   ...
};

The second form accepts message_holder_t instance and intended to be used when an object is preallocated earlier. For example, when a message is stored for some time and will be resent later:

class peak_deleter final : public so_5::agent_t {
   // Buffer for delayed messages.
   std::queue< so_5::message_holder_t<some_message> > delayed_;
   ...
   void on_new_message(mhood_t<some_message> cmd) {
      if(too_many_messages_in_progress()) {
         // New message should be delayed.
         delayed_.push(cmd.make_holder());
      }
      else
         // New message can be processed right now.
         so_5::send(actual_processor_, std::move(cmd));
   }
   ...
   void on_resend_time(mhood_t<resend_time> cmd) {
      // Now we can resend the first delayed message.
      auto m = std::move(delayed_.front());
      delayed_.pop_front();
      so_5::send(actual_processor_, std::move(m));
   }
};
Clone this wiki locally