Copyright (c) Hyperion Entertainment and contributors.

Difference between revisions of "Exec Messages and Ports"

From AmigaOS Documentation Wiki
Jump to navigation Jump to search
m (→‎Port2.c: Added missing IExec-> to FindPort call)
m (→‎Port2.c: Put ASOT_Message in upper case)
 
Line 423: Line 423:
 
}
 
}
 
else IDOS->Printf("Can't find 'xyport'; start port1 in a separate shell\n");
 
else IDOS->Printf("Can't find 'xyport'; start port1 in a separate shell\n");
IExec->FreeSysObject(ASOT_Message, xymsg);
+
IExec->FreeSysObject(ASOT_MESSAGE, xymsg);
 
}
 
}
 
else IDOS->Printf("Couldn't get memory\n");
 
else IDOS->Printf("Couldn't get memory\n");

Latest revision as of 23:36, 21 January 2021

Exec Messages and Ports

For inter-process communication, Exec provides a consistent, high-performance mechanism of messages and ports. This mechanism is used to pass message structures of arbitrary sizes from task to task, interrupt to task, or task to software interrupt. In addition, messages are often used to coordinate operations between cooperating tasks. This section describes many of the details of using messages and ports that the casual Amiga programmer won't need. See Introduction to Exec for a general introduction to using messages and ports.

A message data structure has two parts: system linkage and message body. The system linkage is used by Exec to attach a given message to its destination. The message body contains the actual data of interest. The message body is any arbitrary data up to 64K bytes in size. The message body data can include pointers to other data blocks of any size.

Messages are always sent to a predetermined destination port. At a port, incoming messages are queued in a first-in-first-out (FIFO) order. There are no system restrictions on the number of ports or the number of messages that may be queued to a port (other than the amount of available system memory).

Messages are always queued by reference, i.e., by a pointer to the message. For performance reasons message copying is not performed. In essence, a message between two tasks is a temporary license for the receiving task to use a portion of the memory space of the sending task; that portion being the message itself. This means that if task A sends a message to task B, the message is still part of the task A context. Task A, however, should not access the message until it has been replied; that is, until task B has sent the message back, using the ReplyMsg() function. This technique of message exchange imposes important restrictions on message access.

Message Ports

Message ports are rendezvous points at which messages are collected. A port may contain any number of outstanding messages from many different originators. When a message arrives at a port, the message is appended to the end of the list of messages for that port, and a pre-specified arrival action is invoked. This action may do nothing, or it may cause a predefined task signal or software interrupt (see Exec Interrupts).

Like many Exec structures, ports may be given a symbolic name. Such names are particularly useful for tasks that must rendezvous with dynamically created ports. They are also useful for debugging purposes.

A message port consists of a MsgPort structure as defined in the <exec/ports.h> and <exec/ports.i> include files. The C structure for a port is as follows:

struct MsgPort {
    struct Node  mp_Node;
    UBYTE        mp_Flags;
    UBYTE        mp_SigBit;
    struct Task *mp_SigTask;
    struct List  mp_MsgList;
};
mp_Node
is a standard Node structure. This is useful for tasks that might want to rendezvous with a particular message port by name.
mp_FLags
are used to indicate message arrival actions. See the explanation below.
mp_SigBit
is the signal bit number when a port is used with the task signal arrival action.
mp_SigTask
is a pointer to the task to be signaled. If a software interrupt arrival action is specified, this is a pointer to the interrupt structure.
mp_MsgList
is the list header for all messages queued to this port. (See Exec Lists and Queues).

The mp_Flags field contains a subfield indicated by the PF_ACTION mask. This sub-field specifies the message arrival action that occurs when a port receives a new message.

The possibilities are as follows:

PA_SIGNAL
This flag tells Exec to signal the mp_SigTask using signal number mp_SigBit on the arrival of a new message. Every time a message is put to the port another signal will occur regardless of how many messages have been queued to the port.
PA_SOFTINT
This flag tells Exec to Cause() a software interrupt when a message arrives at the port. In this case, the mp_SigTask field must contain a pointer to a struct Interrupt rather than a Task pointer. The software interrupt will be Caused every time a message is received.
PA_IGNORE
This flag tells Exec to perform no operation other than queuing the message. This action is often used to stop signaling or software interrupts without disturbing the contents of the mp_SigTask field.

It is important to realize that a port's arrival action will occur for each new message queued, and that there is not a one-to-one correspondence between messages and signals. Task signals are only single-bit flags so there is no record of how many times a particular signal occurred. There may be many messages queued and only a single task signal; sometimes however there may be a signal, but no messages. All of this has certain implications when designing code that deals with these actions. Your code should not depend on receiving a signal for every message at your port. All of this is also true for software interrupts.

Creating a Message Port

To create a new message port use AllocSysObject() with an object type of ASOT_PORT. If you want to make the port public, you will need to use the ASOPORT_Name tag. Don't make a port public when it is not necessary for it to be so.

Prior to V50 of the operating system, functions such as CreatePort() and CreateMsgPort() were often used. Some older applications may even have created their own static message ports by hand. Although all of the older methods still function for backwards compatibility, they should no longer be used.

Using dynamic message ports with ASOT_PORT is the only way to ensure your applications will remain compatible and its message ports automatically freed by the system when required.

Deleting a Message Port

Before a message port is deleted, all outstanding messages from other tasks must be returned. This is done by getting and replying to all messages at the port until message queue is empty. Of course, there is no need to reply to messages owned by the current task (the task performing the port deletion). Public ports must be removed from the system properly before deallocation. If a signal was allocated for the message port, it must also be freed. FreeSysObject() handles all of this automatically.

Prior to V50 of the operating system, message ports must be freed using the correct corresponding function such as DeletePort() or DeleteMsgPort(). The FreeSysObject() function must only be used on ports which were allocated with AllocSysObject().

How to Rendezvous at a Message Port

The FindPort() function provides a means of finding the address of a public port given its symbolic name. For example, FindPort("Griffin") will return either the address of the message port named "Griffin" or NULL indicating that no such public port exists. Since FindPort() does not do any arbitration over access to public ports, the usage of FindPort() must be protected with Forbid()/Permit(). Names should be unique to prevent collisions among multiple applications. It is a good idea to use your application name as a prefix for your port name. FindPort() does not arbitrate for access to the port list. The owner of a port might remove it at any time. For these reasons a Forbid()/Permit() pair is required for the use of FindPort(). The port address can no longer be regarded as being valid after Permit() unless your application knows that the port cannot go away (for example, if your application created the port).

The following is an example of how to safely put a message to a specific port:

#include <exec/types.h>
#include <exec/ports.h>
 
BOOL SafePutToPort(struct Message *message, CONST_STRPTR portname)
{
    IExec->Forbid();
 
    struct MsgPort *port = IExec->FindPort(portname);
    if (port != NULL)
        IExec->PutMsg(port,message);
 
    IExec->Permit();
 
    return(port ? TRUE : FALSE);      /* If FALSE, the port was not found */
 
    /* Once we've done a Permit(), the port might go away and leave us with
       an invalid port address. So we return just a BOOL to indicate whether
       the message has been sent or not. */
}

Messages

As mentioned earlier, a message contains both system header information and the actual message content. The system header is of the Message form defined in <exec/ports.h>. This structure is as follows:

struct Message {
    struct Node     mn_Node;
    struct MsgPort *mn_ReplyPort;
    UWORD           mn_Length;
};
mn_Node
is a standard Node structure used for port linkage.
mn_ReplyPort
is used to indicate a port to which this message will be returned when a reply is necessary.
mn_Length
indicates the total length of the message, including the Message structure itself.

This structure is always attached to the head of all messages. For example, if you want a message structure that contains the x and y coordinates of a point on the screen, you could define it as follows:

struct XYMessage {
    struct Message xy_Msg;
    UWORD          xy_X;
    UWORD          xy_Y;
}

For this structure, the mn_Length field should be set to sizeof(struct XYMessage).

Creating a Message

Messages are allocated using AllocSysObject() with an object type of ASOT_MESSAGE. The ASOMSG_ReplyPort tag is set to the reply port if desired. Some messages may be sent in one direction in which case the ASOMSG_ReplyPort tag is not used. The size of the message is indicated with ASOMSG_Length and includes the size of the payload.

struct XYMessage xymsg = IExec->AllocSysObjectTags(ASOT_MESSAGE,
  ASOMSG_Size, sizeof(struct XYMessage),
  ASOMSG_ReplyPort, replyPort,
  TAG_END);

Deleting a Message

Messages are freed using FreeSysObject(). Programmers should be careful not to free a message which is still in use or queues in a message port.

Putting a Message

A message is delivered to a given destination port with the PutMsg() function. The message is queued to the port, and that port's arrival action is invoked. If the action specifies a task signal or a software interrupt, the originating task may temporarily lose the processor while the destination processes the message. If a reply to the message is required, the mn_ReplyPort field must be set up prior to the call to PutMsg().

Here is a code fragment for putting a message to a public port. A complete example is at the end of the article.

#include <exec/types.h>
#include <exec/memory.h>
#include <exec/ports.h>
#include <libraries/dos.h>
 
BOOL SafePutToPort(struct Message *, CONST_STRPTR);
 
struct XYMessage {
    struct Message xy_Msg;
    uint16         xy_X;
    uint16         xy_Y;
};
 
int main()
{
    struct MsgPort *xyport, *xyreplyport;
    struct XYMessage *msg;
    BOOL   foundport;
 
    /* Allocate the message we're going to send. */
    struct XYMessage *xymsg = IExec->AllocSysObjectTags(ASOT_MESSAGE,
      ASOMSG_Size, sizeof(struct XYMessage),
      TAG_END);
 
    if (xymsg != NULL)
    {
        /* The replyport we'll use to get response */
        if (xyreplyport = IExec->AllocSysObject(ASOT_PORT, NULL)) {
 
            xymsg->xy_Msg.mn_ReplyPort = xyreplyport;
            xymsg->xy_X = 10;
            xymsg->xy_Y = 20;
 
            /* Now try to send that message to a public port named "xyport".
             * If foundport eq 0, the port isn't out there.
             */
            if (foundport = SafePutToPort((struct Message *)xymsg, "xyport"))
            {
 
            . . .                /* Now let's wait till the someone responds... */
 
            }
            else IDOS->Printf("Couldn't find 'xyport'\n");
 
            IExec->FreeSysObject(ASOT_PORT, xyreplyport);
        else IDOS->Printf("Couldn't create message port\n");
 
        IExec->FreeSysObject(ASOT_MESSAGE, xymsg);
    }
    else IDOS->Printf("Couldn't get memory for xymessage\n");
 
    return 0;
}

Waiting for a Message

A task may go to sleep waiting for a message to arrive at one or more ports. This technique is widely used on the Amiga as a general form of event notification. For example, it is used extensively by tasks for I/O request completion.

The MsgPort.mp_SigTask field contains the address of the task to be signaled and mp_SigBit contains a preallocated signal number (as described in Exec Tasks).

You can call the WaitPort() function to wait for a message to arrive at a port. This function will return the first message (it may not be the only) queued to a port. Note that your application must still call GetMsg() to remove the message from the port. If the port is empty, your task will go to sleep waiting for the first message. If the port is not empty, your task will not go to sleep. It is possible to receive a signal for a port without a message being present yet. The code processing the messages should be able to handle this. The following code illustrates WaitPort().

struct MsgPort *xyport = IExec->AllocSysObjectTags(ASOT_PORT,
  ASOPORT_Name, "xyport",
  TAG_END);
 
if (xyport == NULL)
{
    IDOS->Printf("Couldn't create xyport\n");
    return RETURN_FAIL;
}
 
struct XYMessage *xy_msg = IExec->WaitPort(xyport);  /* go to sleep until message arrives */

A more general form of waiting for a message involves the use of the Wait() function (see Exec Signals). This function waits for task event signals directly. If the signal assigned to the message port occurs, the task will awaken. Using the Wait() function is more general because you can wait for more than one signal. By combining the signal bits from each port into one mask for the Wait() function, a loop can be set up to process all messages at all ports.

Here's an example using Wait():

struct XYMessage *xy_msg;
BOOL ABORT = FALSE;
 
struct MsgPort *xyport = IExec->AllocSysObjectTags(ASOT_PORT,
  ASOPORT_Name, "xyport",
  TAG_END);
 
if (xyport != NULL)
{
    uint32 portsig = 1 << xyport->mp_SigBit;
    uint32 usersig = SIGBREAKF_CTRL_C;            /* User can break with CTRL-C.  */
    for (;;)
    {
        uint32 signal = IExec->Wait(portsig | usersig);  /* Sleep till someone signals.  */
 
        if (signal & portsig)              /* Got a signal at the msgport. */
        {   .  .  .
        }
        if (signal & usersig)              /* Got a signal from the user.  */
        {
            ABORT = TRUE;                  /* Time to clean up.            */
             . . .
        }
        if (ABORT) break;
    }
    IExec->FreeSysObject(ASOT_PORT, xyport);
}
else IDOS->Printf("Couldn't create xyport\n");
WaitPort() Does Not Remove A Message
WaitPort() only returns a pointer to the first message in a port. It does not actually remove the message from the port queue.

Getting a Message

Messages are usually removed from ports with the GetMsg() function. This function removes the next message at the head of the port queue and returns a pointer to it. If there are no messages in a port, this function returns a zero.

The example below illustrates the use of GetMsg() to print the contents of all messages in a port:

while (xymsg = IExec->GetMsg(xyport))
    IDOS->Printf("x=%ld y=%ld\n", xymsg->xy_X, xymsg->xy_Y);

Certain messages may be more important than others. Because ports impose FIFO ordering, these important messages may get queued behind other messages regardless of their priority. If it is necessary to recognize more important messages, it is easiest to create another port for these special messages.

Replying

When the operations associated with receiving a new message are finished, it is usually necessary to send the message back to the originator. The receiver replies the message by returning it to the originator using the ReplyMsg() function. This is important because it notifies the originator that the message can be reused or deallocated.

The ReplyMsg() function serves this purpose. It returns the message to the port specified in the mn_ReplyPort field of the message. If this field is zero, no reply is returned.

The previous example can be enhanced to reply to each of its messages:

while (xymsg = IExec->GetMsg(xyport)) {
    IDOS->Printf("x=%ld y=%ld\n", xymsg->xy_X, xymsg->xy_Y);
    IExec->ReplyMsg(xymsg);
}

Notice that the reply does not occur until after the message values have been used.

Often the operations associated with receiving a message involve returning results to the originator. Typically this is done within the message itself. The receiver places the results in fields defined (or perhaps reused) within the message body before replying the message back to the originator. Receipt of the replied message at the originator reply port indicates it is once again safe for the originator to use or change the values found within the message.

The following are two short example tasks that communicate by sending, waiting for and replying to messages. Run these two programs together.

Port1.c

// port1.c - port and message example, run at the same time as port2.c
 
#include <exec/types.h>
#include <exec/ports.h>
#include <dos/dos.h>
#include <proto/exec.h>
#include <proto/dos.h>
 
struct XYMessage {
    struct Message xym_Msg;
    int16          xy_X;
    int16          xy_Y;
};
 
int main()
{
    BOOL ABORT = FALSE;
 
    struct MsgPort *xyport = IExec->AllocSysObjectTags(ASOT_PORT,
      ASOPORT_Name, "xyport",
      TAG_END);
 
    if (xyport != NULL)
    {
        uint32 portsig = 1U << xyport->mp_SigBit;       /* Give user a `break' signal. */
        uint32 usersig = SIGBREAKF_CTRL_C;
 
        IDOS->Printf("Start port2 in another shell.  CTRL-C here when done.\n");
        do
        {                                                   /* port1 will wait forever and reply   */
            uint32 signal = IExec->Wait(portsig | usersig); /* to messages, until the user breaks. */
            struct XYMessage *xymsg = 0;
 
                                   /* Since we only have one port that might get messages we     */
            if (signal & portsig)  /* have to reply to, it is not really necessary to test for   */
            {                      /* the portsignal. If there is not message at the port, xymsg */
 
                while(xymsg = (struct XYMessage *)IExec->GetMsg(xyport))  /* simply will be NULL. */
                {
                    IDOS->Printf("port1 received: x = %d y = %d\n", xymsg->xy_X, xymsg->xy_Y);
 
                    xymsg->xy_X += 50;       /* Since we have not replied yet to the owner of    */
                    xymsg->xy_Y += 50;       /* xymsg, we can change the data contents of xymsg. */
 
                    IDOS->Printf("port1 replying with: x = %d y = %d\n", xymsg->xy_X, xymsg->xy_Y);
                    IExec->ReplyMsg((struct Message *)xymsg);
                }
            }
 
            if (signal & usersig)  /* The user wants to abort. */
            {
                while(xymsg = (struct XYMessage *)IExec->GetMsg(xyport))  /* Make sure port is empty. */
                    IExec->ReplyMsg((struct Message *)xymsg);
                ABORT = TRUE;
            }
        }
        while (ABORT == FALSE);
            IExec->FreeSysObject(ASOT_PORT, xyport);
        }
    else IDOS->Printf("Couldn't create 'xyport'\n");
    return 0;
}

Port2.c

// port2.c - port and message example, run at the same time as port1.c
 
#include <exec/types.h>
#include <exec/ports.h>
#include <exec/memory.h>
#include <dos/dos.h>
#include <proto/exec.h>
#include <proto/dos.h>
 
BOOL SafePutToPort(struct Message *, CONST_STRPTR);
 
struct XYMessage {
    struct Message xy_Msg;
    int16          xy_X;
    int16          xy_Y;
};
 
int main()
{
    struct MsgPort *xyreplyport = IExec->AllocSysObjectTags(ASOT_PORT, TAG_END);
 
    if (xyreplyport != NULL)
    {
        struct XYMessage *reply = NULL;
 
        struct XYMessage *xymsg = IExec->AllocSysObjectTags(ASOT_MESSAGE,
          ASOMSG_Size, sizeof(struct XYMessage),
          ASOMSG_ReplyPort, xyreplyport,
          TAG_END);
 
        if (xymsg != NULL)
        {
            xymsg->xy_X = 10;   /* our special message information. */
            xymsg->xy_Y = 20;
 
            IDOS->Printf("Sending to port1: x = %d y = %d\n", xymsg->xy_X, xymsg->xy_Y);
 
                                                                   /* port2 will simply try to put  */
            if (SafePutToPort((struct Message *)xymsg, "xyport"))  /* one message to port1 wait for */
            {                                                      /*  the reply, and then exit     */
                IExec->WaitPort(xyreplyport);
                if (reply = (struct XYMessage *)IExec->GetMsg(xyreplyport))
                    IDOS->Printf("Reply contains: x = %d y = %d\n",   /* We don't ReplyMsg since   */
                            xymsg->xy_X, xymsg->xy_Y);                /* WE initiated the message. */
 
                      /* Since we only use this private port for receiving replies, and we sent     */
                      /* only one and got one reply there is no need to cleanup. For a public port, */
                      /* or if you pass a pointer to the port to another process, it is a very good */
                      /* habit to always handle all messages at the port before you delete it.      */
            }
            else IDOS->Printf("Can't find 'xyport'; start port1 in a separate shell\n");
            IExec->FreeSysObject(ASOT_MESSAGE, xymsg);
        }
        else IDOS->Printf("Couldn't get memory\n");
        IExec->FreeSysObject(ASOT_PORT, xyreplyport);
    }
    else IDOS->Printf("Couldn't create xyreplyport\n");
    return 0;
}
 
BOOL SafePutToPort(struct Message *message, CONST_STRPTR portname)
{
    IExec->Forbid();
    struct MsgPort *port = IExec->FindPort(portname);
    if (port != NULL) IExec->PutMsg(port, message);
    IExec->Permit();
    return(port ? TRUE : FALSE); /* FALSE if the port was not found */
 
         /* Once we've done a Permit(), the port might go away and leave us with an invalid port    */
}        /* address. So we return just a BOOL to indicate whether the message has been sent or not. */

Function Reference

The following chart gives a brief description of the Exec functions that control inter-task communication with messages and ports. See the SDK for details about each call.

Function Description
AddPort() Add a public message port to the system list.
AllocSysObject(ASOT_PORT) Allocate and initialize a new message port.
FindPort() Find a public message port in the system list.
FreeSysObject(ASOT_PORT) Free a message port.
GetMsg() Get next message from the message port.
PutMsg() Put a message to a message port.
RemPort() Remove a message port from the system list.
ReplyMsg() Reply to a message on its reply port.