Communication Library

Introduction

The VUnit communication library (com) provides a high-level communication mechanism based on the actor model. The actor model is a mathematical model of computation in which concurrent actors perform all computation. The only way actors can communicate is by sending messages to each other and the message passing is based on two important principles:

  • The sending actor only knows the name of the receiving actor. It doesn’t know the location of the receiver or how a message gets there.
  • Communication is asynchronous. The sender doesn’t know when the receiver will read the message.

By extending the basic communication provided by the actor model com can also provide synchronous communication and some more advanced communication patterns.

Setup

Message passing is not a core functionality of unit testing so com is provided as an optional add-on to VUnit. It is compiled to the vunit_lib library with the add_com method in your Python script

prj = VUnit.from_argv()
prj.add_com()

The VHDL functionality is provided to your testbench with the com_context.

library vunit_lib;
context vunit_lib.vunit_context;
context vunit_lib.com_context;

Basic Message Passing

Sending and Receiving

To send a message we must first create an actor for the receiver. The actor below has a name passed to the new_actor function but com also allows unnamed actors

constant my_receiver : actor_t := new_actor("my receiver");

To send a message to the receiver the sender must have access to the value of the my_receiver constant. If the receiver made my_receiver publically available, for example with a package, it can be accessed directly. If not, it can be found with the find function providing it has a name.

constant found_receiver : actor_t := find("my receiver");

The next step is to create a message to send and we start by creating an empty message

msg := new_msg;

where msg is a variable of type msg_t. Information is added to the message by pushing objects of different types into it.

push_string(msg, "10101010");
push(msg, my_integer);

com supports pushing of all native and standardized IEEE types. In case there is no ambiguity you can just do push, otherwise you have to use the more specific alias push_<type> as exemplified above.

To send the created message to the receiver you call the send procedure

send(net, my_receiver, msg);

send is asynchronous and takes no simulation time, only delta cycles. Messages will be stored in the receiver inbox until the receiver is ready to receive.

net is a network connecting actors and it is used to signal that an event has occurred, for example that a message has been sent. The event notifies all connected actors that something has happened which they may be interested in. For example, the event created when sending a message will wake up all receivers such that they can see if they are the receiver for the message.

An actor waiting for a message uses the receive procedure

receive(net, my_receiver, msg);

This procedure returns immediately if there are pending message(s) in the receiver’s inbox or blocks until the first message arrives. The returned message contains the oldest incoming message and its information can be retrieved using pop functions. The code below will verify that the message has the expected content using the VUnit check_equal procedure.

check_equal(pop_string(msg), "10101010");
my_integer := pop(msg);
check_equal(my_integer, 17);

Just like push there are both pop functions and more verbose aliases on the form pop_<type>.

Objects are always popped from the message in the same order they were pushed into the message and once all objects have been popped the message is empty. If you want to keep a message for later you can make a copy before popping.

msg_copy := copy(original_msg);

Message Types

In the example above the sender and the receiver exchanged one type of message (a string followed by an integer) but the normal use case is that a receiver can handle several types of messages. For example, if the receiver is a bus functional model (BFM) connected to a memory bus it would be able to handle both read and write messages.

Rather than using a regular type as the message type, for example the string "write" for a write message, com provides a special message type.

constant write_msg : msg_type_t := new_msg_type("write");

"write" is just a description of the message type and not a unique identifier. Even if we have two independently created BFMs, both providing the constant above in their own packages, they would be given different values by the new_msg_type function. This means that we can safely create the different types of write messages without any risk of mistaking one for the other.

msg := new_msg(memory_bfm_pkg.write_msg);
push(msg, my_unsigned_address);
push(msg, my_std_logic_vector_data);
send(net, memory_bfm_pkg.actor, msg);

The receiver starts by looking at the message type and then handles the message types it recognizes.

message_handler: process is
  variable request_msg : msg_t;
  variable msg_type : msg_type_t;
  variable address : unsigned(7 downto 0);
  variable data : std_logic_vector(7 downto 0);
  variable memory : integer_vector(0 to 255) := (others => 0);
begin
  receive(net, actor, request_msg);
  msg_type := message_type(request_msg);

  if msg_type = write_msg then
    address := pop(request_msg);
    data := pop(request_msg);
    memory(to_integer(address)) := to_integer(data);
  end if;
end process;

Normally a BFM would never be exposed to a write message aimed for another BFM but under certain cases it can happen. For example when using the publisher/subscriber pattern described later. A typical BFM would also provide a write transaction procedure which hides the message passing details (creating message, pushing data, and sending). That gives an extra level of type safety (and readability).

memory_bfm_pkg.write(net, my_unsigned_address, my_std_logic_vector_data);

If you do not expect the receiver to receive massages of a type it can’t handle you can add this else statement

else
  unexpected_msg_type(msg_type);
end if;

which will cause an unrecognize message to fail the testbench.

Message Ownership

The sender of a message is the owner of that message while it’s being created. As soon as the send procedure is called that ownership is handed over to the receiver and the message passed to the send call can no longer be used to retrieve the information you pushed into it. If you need to keep the message information you can make a copy before calling send.

Since memory is allocated whenever you push to a message its important that the receiver side deallocates that memory to avoid memory leaks. This can be done explicitly by deleting the message.

delete(msg);

However, the typical receiver is a looping process that calls the receive procedure as soon as the previous message has been handled. To simplify the design of the such a receiver the delete procedure is called first in the receive procedure to delete the message from the previous loop iteration.

Replying to a Message

Replying to a message is done with the reply procedure. Below is the previous message handler process which has been updated to also handle read request messages. Every such message results in a reply message targeting the requesting actor.

message_handler: process is
  variable request_msg, reply_msg : msg_t;
  variable msg_type : msg_type_t;
  variable address : unsigned(7 downto 0);
  variable data : std_logic_vector(7 downto 0);
  variable memory : integer_vector(0 to 255) := (others => 0);
begin
  receive(net, actor, request_msg);
  msg_type := message_type(request_msg);

  if msg_type = write_msg then
    address := pop(request_msg);
    data := pop(request_msg);
    memory(to_integer(address)) := to_integer(data);

  elsif msg_type = read_msg then
    address := pop(request_msg);
    data := to_std_logic_vector(memory(to_integer(address)), 8);
    reply_msg := new_msg(read_reply_msg);
    push(reply_msg, data);
    reply(net, request_msg, reply_msg);

  else
    unexpected_msg_type(msg_type);
  end if;
end process;

Just like the send procedure reply will hand message ownership to the receiver.

Receiving a Reply

If you want to await a specific message like the reply to a request message you can use the receive_reply procedure. Below is a read procedure for our memory BFM.

procedure read(
  signal net : inout event_t;
  constant address : in unsigned(7 downto 0);
  variable data : out std_logic_vector(7 downto 0)) is
  variable request_msg : msg_t := new_msg(read_msg);
  variable reply_msg : msg_t;
  variable msg_type : msg_type_t;
begin
  push(request_msg, address);
  send(net, actor, request_msg);
  receive_reply(net, request_msg, reply_msg);
  msg_type := message_type(reply_msg);
  data := pop(reply_msg);
  delete(reply_msg);
end;

receive_reply will block until the specified message is received. All other incoming messages will be ignored but can be retrieved later. Note that we didn’t need a message type for the reply messages, the read procedure just throws it away. However, we will see later that including it can be helpful when debugging a communication system.

Sending a request and directly receiving the reply is a common sequence of calls so it has been given a dedicated request procedure. The two lines above can be replaced by

request(net, actor, request_msg, reply_msg);

Another approach to the read procedure is to think of it as two steps. The first step sends the the non-blocking read request and the second waits to get the requested data. The link between the two is the request message. This message is sometimes called a future since it represents the requested data that will be available in the future. Splitting blocking procedures like this allow you to initiate several concurrent transactions on different DUT interfaces or perform other tasks while waiting for the replies.

memory_bfm_pkg.non_blocking_read(net, address => x"80", future => future1);
some_other_bfm_pkg.non_blocking_transaction(net, some_input_parameters, future2);

<Do other things>

memory_bfm_pkg.get(net, future1, data);
some_other_bfm_pkg.get(net, future2, requested_information);

Signing Messages

So far all request messages have been anonymous, I’ve only created an actor for the receiving part. In these situations the receiver reply call can’t send a reply back to the sender so the reply message is placed in the receiver outbox. The receive_reply procedure called by the sender knows that the request message was anonymous and waits for the reply to appear in the receiver outbox instead of its own inbox.

Some communication patterns, for example the publisher/subscriber pattern, requires that all messages are signed. To sign a message you can provide the sending actor when the message is created.

msg := new_msg(sender => sending_actor);

Sending/Receiving to/from Multiple Actors

The message_handler process presented above had a single actor. However, the actor model is not limited to have one actor for each concurrently running process. A process may have several actors, each representing some other object like a channel. A typical receiver in such a design needs to act on messages from several actors and to support that you can call receive with an array of actors rather than a single actor. If several actors have messages the procedure will return the oldest message from the leftmost actor with a non-empty inbox.

receive(net, actor_vec_t'(channel_1, channel_2), msg);

It’s also possible to send a message to multiple receiving actors. Just call send with an array of receivers.

send(net, actor_vec_t'(receiver_1, receiver_2), msg);

There is no shared ownership of msg once it’s sent. The sender loses ownership and each receiver get its own copy.

Synchronous Communication

Message passing based on the actor model is inherently asynchronous in nature. Sending a message takes no time which means that the sender can send any number of messages before the receiver starts processing the first one. Transactions requesting a reply, like the read transaction presented before, will naturally break this flow of unprocessed messages by blocking while waiting for a reply. Sometimes it’s also useful to synchronize the sender and receiver on transactions which initiate an action without expecting a reply, a write transaction for example. To do that we can create a reply message with a positive or negative acknowledge to signal the completion of the transaction or the failure to complete the request. Rather than doing that explicitly you can use one of the convenience procedures that com provides.

Instead of using the reply procedure with a reply message the receiver can use acknowledge with a positive/negative response in the form of true/false boolean as the third parameter

acknowledge(net, request_msg, positive_ack);

On the sender side there is a matching receive_reply procedure that will return that boolean.

receive_reply(net, msg, positive_ack);

There is also a request procedure to be used in conjunction with acknowledge.

request(net, actor, msg, positive_ack);

Another approach to synchronization is to limit the number of unprocessed messages that a receiver can have in its inbox. If the limit is reached, a new send to that receiver will block. The default inbox size is integer'high but it can be set to some other value when the actor is created.

constant my_actor : actor_t := new_actor("my actor", inbox_size => 1);

It’s also possible to resize the inbox of an already created actor.

resize(my_actor, new_size => 2);

Reducing the size below the number of messages in the inbox will cause a run-time failure.

A third way to synchronize actors is to have a dedicated message for that purpose but without any information exchange. The message exchange will just be an indication that the receiver is idling waiting for new messages.

request_msg := new_msg(wait_until_idle_msg);
request(net, actor_to_synchronize, request_msg, reply_msg);

The sender will block on the request call until the actor to synchronize has replied and the two actors becomes synchronized. Since there is no information exchange there is no need to pop the reply message.

The actor to synchronize will have to add an if statement branch to handle the new message type. Below I’ve extended the message handling of the previous BFM example.

receive(net, actor, request_msg);
msg_type := message_type(request_msg);

if msg_type = wait_until_idle_msg then
  reply_msg := new_msg;
  reply(net, request_msg, reply_msg);
elsif msg_type = write_msg then

  ...

else
  unexpected_msg_type(msg_type);
end if;

Note that no information is pushed to the reply message in this example but you may want to have a message type for debugging purposes.

Message Handlers and Verification Component Interfaces (VCI)

The synchronization based on wait_until_idle_msg is something that can be used by many actors. We’ve seen before how we can create transaction procedures like read and write and we can also create such a procedure for this message. To synchronize with the memory BFM actor we would just do

wait_until_idle(net, memory_bfm_pkg.actor);

We can also create a reusable procedure for the message handling.

procedure handle_wait_until_idle(
  signal net : inout event_t;
  variable msg_type : inout msg_type_t;
  variable request_msg : inout msg_t) is
  variable reply_msg : msg_t;
begin
  if msg_type = wait_until_idle_msg then
    handle_message(msg_type);
    reply_msg := new_msg;
    reply(net, request_msg, reply_msg);
  end if;
end;

This is the same code I showed before to handle the wait until idle message with one addition - the call to the handle_message procedure. handle_message is in itself a message handler, the simplest message handler possible. The only thing it does is to set msg_type to a special value message_handled. To understand why we can look at the updated BFM.

receive(net, actor, request_msg);
msg_type := message_type(request_msg);

handle_wait_until_idle(net, msg_type, request_msg);

if msg_type = write_msg then

  ...

else
  unexpected_msg_type(msg_type);
end if;

After handle_wait_until_idle returns, msg_type has the value message_handled and no more message handling takes place in the following if statement. The unexpected_msg_type procedure of the else branch will be called but that procedure takes no action when the message type is message_handled.

By putting the wait_until_idle_msg message type and the wait_until_idle and handle_wait_until_idle procedures in a package we can create a reusable verification component interface (VCI) that can be added to actors. An actor can call several message handlers, that is add several interfaces, and you can create message handlers that call other message handlers to bundle interfaces. The interface I just presented is actually already provided as a part of VUnit’s synchronization VCI.

Timeout

Receive and send procedures which may block on empty or full inboxes have an optional timeout parameter. For example

receive(net, actor, msg, timeout => 10 ns);

Reaching the timeout limit is an error that will fail the testbench. If you need to timeout a receive call without failing you can use the wait_for_message, has_message, and get_message subprograms. The status returned by the wait_for_message procedure below will be ok if a message is received before the timeout and timeout if the timeout limit is reached.

wait_for_message(net, my_actor, status, timeout => 10 ns);

You can also see if an actor has at least one message in its inbox.

has_message(my_actor);

When there are messages in the inbox you can get the oldest with

get_message(net, my_actor, msg);

It’s also possible to wait for a reply with a timeout.

wait_for_reply(net, request_msg, status, timeout => 10 ns);
if status = ok then
  get_reply(net, request_msg, reply_msg);
end if;

Deferred Actor Creation

When finding an actor using the find function there is a potential race condition. What if the actor hasn’t been created yet? The default VUnit solution is that the find function creates a temporary actor with limited functionality and defer proper actor creation until the new_actor function is called. The process calling find can send messages to this actor and can’t tell the difference. However, it’s not possible to call receive type of procedures on such an actor. Full actor capabilities are acquired when the receiver process has created the actor with new_actor.

The danger with this approach is if the actor “found” by the sender is never created, maybe as a result of a misspelled name. In that case the sender will send messages that are never received but it will block on the second send since the temporary actor has an inbox of size one. The safest way to avoid this is to not use find but rather make the actor constant available to the sender. It’s also possible to to disable the deferred creation by adding an extra parameter to the find call

find("actor name", enable_deferred_creation => false);

If the actor isn’t found the function returns null_actor so to make this work you must make sure that the find function is called after new_actor, for example by adding an initial delay before making the call.

Another approach is to make sure that there are no deferred creations pending a short delay into the simulation, before the actual testing starts. You can find out by calling the num_of_deferred_creations function.

Publisher/Subscriber Pattern

A common message pattern is the publisher/subscriber pattern where a publisher actor publishes a messages rather than sending it. Actors interested in these messages subscribe to the publisher and the published messages are received just like messages sent directly to the subscribers. The purpose of this pattern is to decouple the publisher from the subscribers, it doesn’t have to know who the subscribers are and there is no need to update the publisher when subscribers are added or removed.

An example for how this pattern can be used is when you have a verification component monitoring an interface of the DUT. Let’s say we have a simple adder with streaming input and output interfaces. The input interface consists of two unsigned operands and a data valid signal while the output consists of an unsigned sum and a data valid. The input interface is controlled by a driver BFM which receives add transactions as well as wait_for_time to insert idle cycles in the input stream. wait_for_time is a standard VCI provided by the sync_pkg. The output interface has a monitor process which creates sum messages from valid output sums. Just like the input driver doesn’t know or care who’s sending the add transactions, the monitor doesn’t have to know who’s consuming the sum messages. To achieve that it will publish the sum messages and just provide the publishing actor (monitor).

monitor_process : process is
  variable msg : msg_t;
begin
  wait until rising_edge(clk) and (dv_out = '1');
  msg := new_msg(sum_msg);
  push(msg, to_integer(sum));
  publish(net, monitor, msg);
end process;

In addition to the driver and the monitor there is a scoreboard process to verify the adder functionality. The scoreboard subscribes to the sum messages published by the monitor using the subscribe procedure. Rather than having a single actor the scoreboard has several actors called channels and the slave_channel is setup to subscribe to messages published by the monitor actor.

subscribe(slave_channel, monitor);

The next step is to make sure that the scoreboard also receives the add transactions on the input interface. There are several ways to do this. One is to build another monitor for the input interface and another is to let the driver publish the add transactions. However, in order to demonstrate com functionality this scoreboard will use a third approach and let the scoreboard subscribe to inbound traffic to the driver. This can be done by adding a third parameter to the subscribe call.

subscribe(master_channel, driver, inbound);

The default value used before is published and it is also possible to subscribe to outbound traffic. outbound traffic is every output message from an actor regardless if that message is the result of a publish, send, or reply call.

With the two subscriptions at hand we can create a scoreboard process. The main flow of the code below is to wait for an add_msg on the master_channel (wait_for_time is ignored) and when it’s received wait for the associated sum_msg on the slave_channel. Once both these messages are available the scoreboard will use its reference model to verify that the output data matches the input.

scoreboard_process : process is
  variable master_msg, slave_msg : msg_t;
  variable msg_type         : msg_type_t;

  procedure do_model_check(indata, outdata : msg_t) is
    variable op_a, op_b, sum : natural;
  begin
    op_a := pop(indata);
    op_b := pop(indata);
    sum  := pop(outdata);
    check_equal(sum, op_a + op_b);
  end;
begin
  subscribe(master_channel, driver, inbound);
  subscribe(slave_channel, monitor);
  loop
    receive(net, master_channel, master_msg);
    msg_type := message_type(master_msg);

    handle_wait_until_idle(net, msg_type, master_msg);

    if msg_type = add_msg then
      receive(net, slave_channel, slave_msg);

      if message_type(slave_msg) = sum_msg then
        do_model_check(master_msg, slave_msg);
      end if;
    end if;
  end loop;
end process;

In order for the test sequencer to know when the verification is complete it will send a wait_for_idle transaction after all add transactions. That transaction is handled by the handle_wait_until_idle message handler on the scoreboard side. The example test sequencer below just sends 10 random add messages separated by a random delay (not good for functional coverage but good enough for this example).

for i in 1 to 10 loop
  msg := new_msg(add_msg);
  push(msg, rnd.RandInt(0, 255));
  push(msg, rnd.RandInt(0, 255));
  send(net, driver, msg);
  wait_for_time(net, driver, rnd.RandTime(0 ns, 10 * clk_period));
end loop;
wait_until_idle(net, master_channel);

Subscribing to messages actively being published is the classic form of the publisher/subscriber communication pattern while subscriptions on inbound or outbound traffic is more like eavesdropping. This has implications that you need to be aware of:

  • When receiving a message that has been published, a call to sender or receiver on that message will return the publisher and subscriber actors respectively. However, when receiving a message resulting from an inbound or outbound subscription the two functions will return the sender and the receiver for the original message transaction.
  • The subscriber of inbound and outbound traffic will receive all messages, not only those that would have been published if the decision was more active. For example, if someone sends a wait_for_idle transaction to the driver it will also be sent to the subscriber which will act upon it “thinking” it was from the test sequencer. This wouldn’t be a problem if we had a monitor for the input interface only publishing add messages. It’s still possible to fix though, for example by only handling wait_for_idle transactions aimed at the master channel.
if receiver(master_msg) = master_channel then
  handle_wait_until_idle(net, msg_type, master_msg);
end if;
  • Since you can subscribe on inbound traffic you can also subscribe to the inbound traffic of a subscriber. This may not have great practical value but can, if misused, create an infinite loop of subscriptions which will hang the simulation.
  • A subscription on the outbound traffic of an actor won’t pick up messages sent anonymously.
  • A subscription on the inbound traffic of an actor won’t pick up replies to an anonymously request.

Blocking subscribers

Although the intent of the publisher/subscriber pattern is to decouple the publisher from the subscribers it can still be affected if a subscriber inbox is full. A message transaction will be blocked until all of its subscribers and any regular receiver have available space in their inboxes.

Unsubscribing

An actor can unsubscribe from a subscription at any time by calling unsubscribe with the same parameters used when calling the subscribe procedure.

Debugging

Message passing provides a communication mechanism an abstraction level above the normal signalling in VHDL. This also means that there is a need for an equally elevated level of debugging. To support that com has a number of built-in features specially targeting debugging.

Logging Messages

One way of debugging is to inspect the messages that flow through the system, for example by subscribing to actor traffic. You can use previously presented functions to find out sender, receiver and message content but you can also convert a message to a string such that it can be logged.

to_string(reply_msg)

The resulting string may look something like this

3:2 memory BFM -> test sequencer (read reply)

The first number (3) is the message ID which is unique to this message. The second number (2) is present in reply messages and denotes the message ID for the request message. After that we have the sender (memory BFM) which sent the message to (->) the receiver (test sequencer). Finally, the value in parentheses (read reply) is the message type. All communicated messages have a message ID but not all messages are replies, sender and receivers may be anonymous and not all messages have a message type. Fields missing a value will be replaced with -.

Note that com has limited knowledge of the contents of a message. All data pushed into a message is encoded and is basically handled as a sequence of bytes without any overhead for type information. com doesn’t know if four bytes represents an integer, four characters or something else. The interpretation of these bytes takes place when the user pops data using a type specific pop function. The exception is the message type for which the type overhead is included to provide better debugging. Higher levels of debug information, for example that a message represents a read request to a specific address is something that the verification component using com provides.

Trace Log

Rather than manually logging messages you can quickly see all messages by showing the trace logs. com provides a logger, com_logger, and you enable the trace logs by showing log entries on the trace log level.

show(com_logger, display_handler, trace);

Ignoring the initial part introduced by the logging framework (everything up to and including TRACE -) we still see a difference when compared to the string presented above.

30000 ps - vunit_lib:com -   TRACE - test sequencer inbox => [3:2 memory BFM -> test sequencer (read reply)]

First is an actor mailbox (test sequencer inbox), than an arrow (=>) followed by the message string enclosed in square brackets. This means that the message was removed from the mailbox, for example as a result of a receive_reply call. com also logs when a message is put into a mailbox. In this example that event is logged 10 ns earlier and is the result of a reply call

20000 ps - vunit_lib:com -   TRACE - [3:2 memory BFM -> test sequencer (read reply)] => test sequencer inbox

Now that we have all these transactions available it becomes possible to follow sequences of events. For example, at time 0 ps we have the message with ID = 2 which is the request message for the reply above.

0 ps - vunit_lib:com -   TRACE - [2:- test sequencer -> memory BFM (read)] => memory BFM inbox

Again, if you want higher level of debug information you can add debug logging to your BFM which may result in something like this.

    0 ps - vunit_lib:com -   TRACE - [2:- test sequencer -> memory BFM (read)] => memory BFM inbox
10000 ps - vunit_lib:com -   TRACE - memory BFM inbox => [2:- test sequencer -> memory BFM (read)]
20000 ps - memory BFM    -   DEBUG - Reading x"21" from address x"80"
20000 ps - vunit_lib:com -   TRACE - [3:2 memory BFM -> test sequencer (read reply)] => test sequencer inbox
30000 ps - vunit_lib:com -   TRACE - test sequencer inbox => [3:2 memory BFM -> test sequencer (read reply)]

State Information

In addition to tracing messages you can also examine the state of the communication system. By calling get_mailbox_state you can take a snapshot and examine all messages in an actor mailbox.

mailbox_state := get_mailbox_state(memory_bfm_pkg.actor, inbox);

mailbox_state is a record that you can expand and examine in your simulator. Be aware that this gives you a glimpse of internal representations of data which we may change. It’s suitable for browsing but not something you should act upon programmatically.

You can also create a string representation of the mailbox state by calling

get_mailbox_state_string(memory_bfm_pkg.actor, inbox)

The result is something like this

Mailbox: inbox
  Size: 2147483647
  Messages:
    0. 5:- - -> memory BFM (write)
    1. 6:- - -> memory BFM (read)

The size is the maximum number of messages that the mailbox can contain (this is dynamically allocated) while the list at the bottom shows the actual messages in the mailbox. Message 0 is the oldest message and the first one to be returned when you call receive.

You can also get an actor’s state as well as the string representation for that state

actor_state := get_actor_state(driver);
debug(get_actor_state_string(driver));

The string representation contains information about both mailboxes, subscriptions and subscribers and if the actor’s creation is deferred. For example,

Name: driver
  Is deferred: no
  Mailbox: inbox
    Size: 2147483647
    Messages:
      0. 8:- - -> driver (add)
      1. 9:- - -> driver (add)
      2. 10:- - -> driver (add)
  Mailbox: outbox
    Size: 2147483647
    Messages:
  Subscriptions:
  Subscribers:
    driver channel subscribes to inbound traffic

In this case the outbox is empty and driver doesn’t subscribe to anything. However, the driver channel actor subscribes to inbound traffic to driver.

Finally, you can get the state for the messenger which is the manager of the communication system. That state contains two lists - one with the states of all active actors (those not deferred) and one with the states of all deferred actors.

messenger_state := get_messenger_state;
debug(get_messenger_state_string);

Deprecated APIs

com maintains a number of deprecated APIs for better backward compatibility. Using these APIs will result in a runtime error unless enabled by calling the allow_deprecated procedure.

Earlier releases of com would not cause a runtime error on timeout. This behavior can be enabled with the deprecated APIs by calling allow_timeout. If not, a timeout will result in an error with the exception of the wait_for_messages and wait_for_reply procedures which return a status.

The deprecated APIs will be removed in the future so it’s recommended to replace these with contemporary APIs.