3.2.4. Messages

3.2.4.1. Prerequisites

To correctly understand the content of this page, you shall have read all the previous EGOS Kernel Architecture pages first.

3.2.4.2. Introduction

IPC

EGOS is composed of processes. Each of these processes has its own stack allocated in the memory. In order to synchronize them together or sharing data between them an inter-process communication is set up.

3.2.4.3. Messages rules

A process can send a message to every available process. It is the receiver which is designed to manage or not, to answer or not to a message. The receiver can only have a behavior for some messages ID or from some processes. The message may be not managed by the receiver and deallocates from memory.

3.2.4.4. Messages characteristics

A message is addressed to a process by giving the process' handle. This handle corresponds to the process' identity, so it is like the unique ID. This handle can be retrieved by the process creation macro mOSS_EXE__CREATE which returns it or with the help of mOSS_EXE_FIND macro. This one is used for getting a process handle by giving the name of the process. For reminder, both of processes have an handle and a name.

hHandle hP1;

hP1
=   mOSS_EXE__FIND
    ( STR_OS("egos.p1")
    );

The message has also a type and a number which form together the message ID. These two fields are used to inform the message's receiver and then it can be managed properly. The message ID is formed by using the mOSS_MSG__ID macro. This macro hashes the type and then concatenate it with the given number to create a unique message ID.

The message type is a constant definition. For example:

#define cAPP_MAIN_TST__MSG_INTER    STR_OS("between_P1_P2")

The message identifier is an enumeration declaration. One message type can have different identifiers. For example:

DECL_TYPEDEF(enum, eAppMainTst_Msg)
{ eAPP_MAIN_TST__MSG__PING  = 0                                     // Request answer
, eAPP_MAIN_TST__MSG__PONG                                          // Answer given
, eAPP_MAIN_TST__MSG__PING_TIMED                // Request answer with timeout
//
, eAPP_MAIN_TST__MSG
}   DECL_INSTANCE(enum, eAppMainTst_Msg);

In addition to the previous parameters, a message can eventually have some data to share with another process. These data will compose a data box structure. This box is composed of a reference to the data to be sent and of the length of these data. Sending data in a message is going to allocate a new memory block with the given size with the content given by the reference. This memory block will contain the variable content. Thus, when the data are given like that, the sent and received data can differ, since there is a new memory allocation, there are two variable versions. To avoid this behavior, it is also possible to use a reference of reference (that means pointer of pointer). It is going to allocate a memory block. That time, this memory block will contain the data reference to the the reception side will get the reference of the data. The use case of these two versions is going to be explained afterwards (Sending a message with data).

The message reception uses the FIFO mechanism. The first sent message by the emitter process is the first read message by the receiver process. In other words, the oldest sent message is the first message which is read.

The work flow to send a message is the following:

  1. The message's sender chooses the message's receiver by its handle.
  2. The message has a type (cAPP_MAIN_TST__MSG_INTER) and a value (eAPP_MAIN_TST__MSG__PING). These two data form the ID of the message.
  3. The receiver's process needs to be in a reception loop to manage the messages and to have the message ID case relevant with the received one.
  4. The receiver can correctly answer according to the received message

both_mode_message

3.2.4.5. Emission of a message

Each time a message is sent, a memory allocation is done. After that the message is read by the receiver, this memory is freed. A message can be sent only if the receiver is able to read them (if it is not the message is freed and the send macro returns that the receiver is not able to read the message). It does not imply that the receiver have a specific behavior for this message, but that it can read it and delete it from memory then.

3.2.4.5.1. Sending a message

To send a message, the sender needs to use the mOSS_MSG__SEND macro:

mOSS_MSG__SEND
( hP2                                                               // Handle of the receiver process
, mOSS_MSG__ID                                              // Message ID:
  ( cAPP_MAIN_TST__MSG_INTER                //      - Message type
  , eAPP_MAIN_TST__MSG__PING                //      - Message value
  )
);

3.2.4.5.2. Sending a message with data

The same actions are possible with data. To send a message containing some data, the macro mOSS_MSG__SEND_DATA can be used. It adds two additional parameters which are contained in a message box:

  • a pointer on the data to send
  • the size of the data to send

Below is an example which shows how to send the content of a variable (integer, string, structure, ...):

(The __PTR casts the pointer to a void pointer and its address to an integer)

mOSS_MSG__SEND_DATA
( hHandle
, mOSS_MSG__ID                                      // Message ID:
  ( cAPP_MAIN_TST__MSG_INTER        //      - Message type
  , eAPP_MAIN_TST__MSG__PONG        //      - Message value
  )
, __PTR(&l_nData)               // The address of the variable is given
, sizeof(l_nData)                           // The size of the data is given
);

Here is an example which shows how to send the address of the variable pointed by a pointer in a data message. This time the size of the pointed address is used instead of the variable's one. This case is similar to the previous one with a variable:

mOSS_MSG__SEND_DATA
( hHandle                                           // Handle of the receiver process
, mOSS_MSG__ID                                      // Message ID:
  ( cAPP_MAIN_TST__MSG_INTER        //      - Message type
  , eAPP_MAIN_TST__MSG__PONG        //      - Message value
  )
, __PTR(&l_pnData)              // The address of the pointer is given
, sizeof(pVoid)                                     // The size of the reference is given
);

⚠️ Warning: The reference way is tolerated but not the privileged way. As this method sends an address located in the sender memory to another process, it can create some simultaneous writing access problem. To avoid that, giving the content of the variable to create a new memory block is better.

3.2.4.5.3. Sending a message with a lifetime

The user can also send a message which will be deleted if this one is not read during a defined time. This time defines the lifetime of the message. For example, this functionality can be used to send a temperature value which will meaningless if it is read 2 hours after its emission. The lifetime value is given in microseconds. After its lifetime, the message is deleted from the memory. The macro mOSS_MSG__SEND_LIFETIME is used as follow:

mOSS_MSG__SEND_LIFETIME
( hHandle                                                   // Handle of the receiver process
, mOSS_MSG__ID                                              // Message ID:
  ( cAPP_MAIN_TST__MSG_INTER                //      - Message type
  , eAPP_MAIN_TST__MSG__PING_TIMED  //      - Message value
  )
, 5000                                 // Lifetime value in microseconds
);

3.2.4.6. Reception of a message

3.2.4.6.1. Management of the received messages

A process is able to read its messages only if it is in a message loop. A message loop is composed of a beginning and of an end. The beginning is in charge of find an unread messages (specific or not, it depends from the input parameters) and the end of deleting read and expired messages. Between the beginning and the end, the definition of the behavior is defined in cases associated to a message ID.

mOSS_MSG__RECEIVE_GENERIC(timeout, id, mask, handle)

It is possible to define when the process goes out from the loop with the timeout, to define which message ID needs to be find, to mask the message ID to get only some part of it, the emitter of the message with the handle.

Many macros are defined to easily use messages. By example,the previous macro can be replaced by mOSS_MSG__RECEIVE when all messages are going to be read for an unlimited time. The exhausted list of macro is given in the API documentation.

The following code is an example of message loop use:

mOSS_MSG__RECEIVE()
{
    case mOSS_MSG__ID(cAPP_MAIN_TST__MSG_INTER, eAPP_MAIN_TST__MSG__PING):
    {
        // Add here the relevant behavior to the corresponding message
    } break;

    // ...
    // list here the case of the different ID which can be received by this process
    //Add here how the process needs to manage the message
    default:
    {
        //The received message is not known in this message loop
    } break;

}
mOSS_MSG__LOOP()

The message loop must define all the cases which the process shall answer. If a message ID is received by the process but not defined in the message loop, it will be deleted at the end of the message loop. The mOSS_MSG__RECEIVE macro corresponds to the message loop's beginning and the mOSS_MSG__LOOP one to the loop's end.

When the message is loop is defined with an unlimited timeout and no message are available, the process is not scheduled until a new message is received.

msg_flow

The beginning macro waits for a new message in the message's box of the current process. It also checks the message's validity. If there are many messages it reads the first valid message. If there is no message or only expired messages, the expired message are deleted and the hand is given to the other processes.

On the other side, the end macro mOSS_MSG__LOOP switches to the other process. When it gets back the hand, it checks if there are some messages to destroy (previous read message and expired messages).

3.2.4.6.2. Getting data from a message

On the reception side, the message box, which is the structure which contains the message data (the reference and size of the sent data), is used to get the data. It can be accessed with the macro mpOSS_MSG__BOX_ADDR.

In the case of data content reception (not its reference):

value_message

As the received data is a void pointer to the data, to get all the received data, you need to cast them into the expected pointer type as you know which data you will receive (in this example pU32).

l_nDataReceived
= *( (pU32)mpOSS_MSG__BOX_ADDR() );    // Get the content of the address given by mpOSS_MSG__BOX_ADDR

In case of data reference reception (pointer of pointer) (not its content):

reference_message

The received data is also a pointer of pointer, the received pointer contains the sender pointer's address which contains the data's address.

l_pnDataReceived
= *( (ppU32) mpOSS_MSG__BOX_ADDR() );    // Get the address pointed by the pointer (pU32)

( p means that is a pointer, pp is a pointer of pointer)

3.2.4.6.3. Check the received data

As the received data is in fact a pointer to the data, it shall be a good choice to check if the data is what you expected before to use it. The mOSS_MSG__DATA_CHECK simplifies this check up when you received a message. The input parameter are needed to check the type of the received data, to store them in a local variable and to answer with a specific message ID if the data are not valid.

mOSS_MSG__DATA_CHECK(nU8, l_nMyVar, mOSS_MSG__ID(cAPP_MAIN_TST__MSG_INTER, eAPP_MAIN_TST__MSG__UNVALID_DATA));

3.2.4.6.4. Answer to the message’s sender

The receiver can answer to the sender by using the mOSS_MSG__REPLY macro in a message's loop. It will use the sender process' handle for addressing the message to the sender's process:

mOSS_MSG__REPLY
( mOSS_MSG__ID
  ( cAPP_MAIN_TST__MSG_INTER
  , eAPP_MAIN_TST__MSG__PONG
  )
);

3.2.4.6.5. Answer to the message’s sender with data

The mOSS_MSG__REPLY_DATA also exists. Like the mOSS_MSG__REPLY macro, It will use the sender process' handle for addressing the message to the sender's process but this time, it will also send some data.

mOSS_MSG__REPLY_DATA
( mOSS_MSG__ID
  ( cAPP_MAIN_TST__MSG_INTER
  , eAPP_MAIN_TST__MSG__PONG
  )
, __PTR(&l_nData)
, sizeof(l_nData)
  )
);

3.2.4.6.6. Leave a loop before the timeout

It is possible to leave a loop as soon as a message received to leave the loop and do some other stuff by using mOSS_MSG__EXIT instead of break.

Example:

  • The timeout is infinite here, the other stuff will be never executed
mOSS_MSG__RECEIVE()
{
    case mOSS_MSG__ID(cAPP_MAIN_TST__MSG_INTER, eAPP_MAIN_TST__MSG__PING):
    { // behavior
    } break;

    default:
    { // behavior
    } break;
}
mOSS_MSG__LOOP()
// other stuff
  • The timeout is infinite here, the other stuff will be executed as soon as the mOSS_MSG__ID(cAPP_MAIN_TST__MSG_INTER, eAPP_MAIN_TST__MSG__PING) message is received.
mOSS_MSG__RECEIVE()
{
    case mOSS_MSG__ID(cAPP_MAIN_TST__MSG_INTER, eAPP_MAIN_TST__MSG__PING):
    { // behavior
    } mOSS_MSG__EXIT();

    default:
    { // behavior
    } break;
}
mOSS_MSG__LOOP()
// other stuff

3.2.4.6.7. Note on nested message loop

The message loops can be nested. The recommended way to nest message loop is to define:

  • at the first level a standard loop (no timeout, no specific message id, no specific sender) to manage all the messages and free them from the memory
  • into the main loop only specific message loops to manage specific messages (with timeout or mOSS_MSG__EXIT to exit the loop)

Each loop deletes the message that it has read.

In a loop, only messages which are sent while this loop is running can be read. The old messages are not reachable by the new loops.