IPC Implementation - C

Quick Example

The following is a minimal example for requesting the Cellular service data resource.

The example will trigger the following message sequence.

*   +------+                          +---------+
*   | user |                          | service |
*   +---+--+                          +----+----+
*       |     ipc_open_session_req         |
*       |--------------------------------->|
*       |     ipc_open_session_rsp         |
*       |<---------------------------------|
*       |     cellular_data_request_req    |
*       |--------------------------------->|
*       |     cellular_data_request_rsp    |
*       |<---------------------------------|
*       .                                  .
*       .                                  .

The code looks as follows. It is available in the demo actia-minimal-ipc-demo.

#define IPC_ID (A_IPC_GUEST_MIN_ID + 6)
#define IPC_OUTBOX_SIZE 10
#define IPC_INBOX_SIZE 15
A_IPC_STATIC_BUFFERS(ipc_buffers, IPC_INBOX_SIZE, IPC_OUTBOX_SIZE);

a_log_set_logger(a_log_stdout_logger);

int ret;
int epfd = epoll_create1(0);
struct epoll_event events[10];
int event_count;
int ipc_fd = 0;

a_ipc_handle *a_ipc = a_ipc_init(IPC_ID, &ipc_buffers);

A_IPC_MSG_ON_STACK(msg_buf, 256);

ipc_fd = a_ipc_get_fd(a_ipc);

events[0].events = EPOLLIN;
events[0].data.fd = ipc_fd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, ipc_fd, events);
if (ret < 0) {
    perror("epoll_ctl ADD ipc_fd");
    return -1;
}

A_IPC_RESULT rc = a_ipc_init_ipc_open_session_req(msg_buf);
if (rc != A_IPC_RET_OK) {
    printf("Failed to create ipc open session request: %s\n", a_ipc_result_str(rc));
    return -2;
}
rc = a_ipc_send(a_ipc, A_SERVICE_ID_CELLULAR, msg_buf);
if (rc != A_IPC_RET_OK) {
    printf("Failed to send open session request: %s\n", a_ipc_result_str(rc));
    return -2;
}

int from;
bool run = true;
do {
    event_count = epoll_wait(epfd, events, a_membersof(events), -1);
    for (int i = 0; i < event_count; i++) {
        if (events[i].data.fd == ipc_fd) {
            rc = a_ipc_recv(a_ipc, &from, msg_buf);
            if (rc != 0) {
                printf("a_ipc_recv error: %d\n", rc);
                continue;
            }
            printf("Received msg type: %s\n",
                    a_ipc_msg_type_str(msg_buf->info.type));
            switch (msg_buf->info.type) {
             case A_IPC_MSG_IPC_OPEN_SESSION_RSP:
                a_ipc_init_cellular_data_request_req(msg_buf, 20);
                msg_buf->cellular_data_request_req.user.value = "demouser";
                msg_buf->cellular_data_request_req.user.length = strlen("demouser");

                rc = a_ipc_send(a_ipc, A_SERVICE_ID_CELLULAR, msg_buf);
                if (rc != A_IPC_RET_OK) {
                    printf("Failed to send open session request: %s\n", a_ipc_result_str(rc));
                    run = false;
                }
                break;
             case A_IPC_MSG_IPC_SESSION_CLOSED_IND:
                run = false;
                break;
             case A_IPC_MSG_CELLULAR_DATA_REQUEST_RSP:
                run = false;
                break;
            default:
                printf("Unknown command: %d\n", msg_buf->info.type);
                break;
            }
        }
    }
} while (run);

a_ipc_destroy(a_ipc);
return rc;

Note that production code should always check for errors and release resources when they are not needed anymore.

API

Initialization

The IPC framework is initialized by calling a_ipc_init() which returns a handle to the IPC instance. When done using IPC, the handle shall be destroyed using a_ipc_destroy().

The application needs to allocate inbox and outbox buffers and supply them to the a_ipc_init() call. This can be done with the macro A_IPC_STATIC_BUFFERS, which will allocate the buffers statically. (Static allocation is preferred when targeting smaller devices, such as the ACU6-Lite.)

Each peer must have a system-unique ID, which is used for addressing. IDs available to user applications are given by A_IPC_GUEST_MIN_ID and A_IPC_GUEST_MAX_ID (inclusive).

Each service provided by Actia has an ID (e.g. A_SERVICE_ID_CELLULAR).

The IPC framework interface is single-threaded, which means that calls to the framework must be done from the same calling thread. Note that it is possible to create multiple instances of IPC and use them from separate threads.

Messages

Each message is of a specific message type (msg->info.type), which in turn belongs to a message class.

A message is represented by an enum constant, for the type ID, and a struct. Refer to Data types for a description of the types that the message members can have.

An example message with its enum constant and struct:

A_IPC_MSG_RESOURCE_TRIGGER_RSP = 10

typedef struct {
    bool success;
    int counts[10];
    string message;
} a_ipc_msg_resource_trigger_rsp;

The sample below shows how to create a message and fill it with data

void my_function()
{
   A_IPC_MSG_ON_STACK(msg, 256);

   a_ipc_init_cellular_data_request_req(msg, 20);
   /* msg->info.type is now A_IPC_MSG_CELLULAR_DATA_REQUEST_REQ */

   msg->cellular_data_request_req.user.value = "demouser";
   msg->cellular_data_request_req.user.length = strlen("demouser");

   /* a_ipc_send(a_ipc, PEER_ID, msg); is often called here to
      send the message. */
}

The macros A_IPC_MSG_ON_STACK and A_IPC_STATIC_MSG create buffers of the given size and initializes the message metadata in the info member. Both make the given message an a_ipc_msg pointer.

The initialization is done using a_ipc_init_<msgtype>() and sets up the message buffer for the requested message type. If the message has dynamic length members (such as strings), the length to reserve for them in the buffer must be given.

A message buffer can be reused multiple times. One can change the values or change the message type by calling a_ipc_init_<msgtype>().

Each message type has a corresponding struct that follows the pattern a_ipc_msg_<msgtype>. For example, the message cellular_data_request_req has a struct representing it named a_ipc_msg_cellular_data_request_req.

Member data of a message is accessed by standard member-access in C, using the name of the member. All message types and their members can be found in IPC Message Reference.

Sending messages

After initializing a message, it can simply be sent by a_ipc_send().

a_ipc_send(a_ipc, PEER_ID, msg);

Receiving messages

Messages are received into an inbox. a_ipc_recv() is used to fetch messages from the inbox. The msg parameter must point to a valid message buffer.

Since a_ipc_recv() is non-blocking, it might return A_IPC_RET_NO_MSG to indicate that no message has been received. Therefore, it’s recommended to use a_ipc_get_fd() to get the IPC file descriptor and listen for when it is ready for reading.

After receiving a message, the type (msg->info.type) can be used to determine how to interpret it and access the correct members. Always check that the return value is A_IPC_RET_OK or A_IPC_RET_SEND_ERROR before reading the message buffer.

A_IPC_STATIC_MSG(msg, 256);

int ipc_fd = a_ipc_get_fd();

/* epoll setup with ipc_fd */

while (true) {
   epoll_wait();

   int from;
   A_IPC_RESULT rc = a_ipc_recv(a_ipc, &from, msg);

   if (rc == A_IPC_RET_OK || rc == A_IPC_RET_SEND_ERROR) {
      switch (msg->info.type) {
         case A_IPC_MSG_CELLULAR_DATA_REQUEST_RSP:
            printf("Request resp: %d\n", msg->cellular_data_request_rsp.result);
            break;
      }
   }
}

Refer to the SDK demo applications for a full example of waiting for and receiving messages.

Data types

The generic message data types described in Data types is mapped into C language data types as follows:

Primitive types

Integers and booleans are stored as their corresponding C type.

bool

int8

int16

int32

int64

uint8

uint16

uint32

uint64

bool

int8_t

int16_t

int32_t

int64_t

uint8_t

uint16_t

uint32_t

uint64_t

Strings

All strings should be NUL terminated, and strings in received or copied messages are always terminated. NUL-terminating input arguments is not needed, but recommended. Termination is added automatically.

Fixed length strings are represented as a char array of the strings length plus one.

Dynamic length strings are represented by a_ipc_dyn_string. It has two members: value and length. The member value is an array of char and length describes the length of the string, but does not include the final NUL-termination.

Enumerations

Enumerations will have a corresponding C enumeration. Enum types have the following pattern a_ipc_enum_<enum_type>, and the enumerators will have the following pattern A_IPC_ENUM_<ENUM-TYPE>_<ENUMERATOR-NAME>.

For example ipc_send_error_reason will have the enum type a_ipc_enum_ipc_send_error_reason and its SEND_FAILED enumerator value will have the identifier A_IPC_ENUM_IPC_SEND_ERROR_REASON_SEND_FAILED.

Structs

Internal structures will map to C structures, with name a_ipc_struct_<struct-name>. For example the struct connectivity_provider_config_s will map to a_ipc_struct_connectivity_provider_config_s.

Nested members in a struct are handled just as direct message members.

Dynamic arrays

Dynamic arrays have two members, data and length. The content of the array is accessed by data and the length of the array is stored in length.

The pattern of a dynamic array type name is a_ipc_dyn_array_<data-type>, where <data-type> is the data type that the dynamic array stores. For example a dynamic array of type uint8_t will be named a_ipc_dyn_array_uint8_t.

If a dynamic array stores an enum the pattern will instead look like a_ipc_dyn_array_enum_<enum-type>. For example a dynamic array of enum type can_state will be named a_ipc_dyn_array_enum_can_state.