The High Performance SDK provides its own IPC (Inter Process Communication) framework.
This IPC framework is based on SHM (shared memory), which allows efficient interprocess communication without copying data across process boundaries.
The IPC components can describe their interface using an IDL (interface definition language). The ICC (interface C compiler) creates proxy and stub C code for the caller and callee side, based on the given IDL. This allows creating IPC components very easily.
The whole design of the High Performance SDK is based on a single threaded design using asynchronous functions and callbacks. The main application loop is never allowed to call any blocking function except uaapplication_timed_docom(). This function drives the timers, network communication, and the IPC framework. For this reason, our IPC framework generates asynchronous method functions with a corresponding callback based on synchronous function definitions in the IDL.
Example IDL:
interface calculator
{
int multiply([in] int a, [in] int b, [out] int product);
}
The generate proxy function looks likes this on the caller side:
typedef void (*multiply_complete)(int _result, int product, void *cb_data, int prio);
int begin_multiply(int a, int b, multiply_complete cb, void *cb_data, int prio);
- Note
- The first line is a callback type definition. The second line is the asynchronous invocation function. Note that in addition to the defined parameters this function contains the callback pointer, callback data and a priority. The callback data is returned in the callback and can be used to get any necessary operation context. The
prio
parameters is used in the IPC framework to process queued operations according to their priority. Higher priorities will be processed first. Operations of same priority will be processed in FIFO order.
This way the caller can start the operation asynchronously and gets a callback when the operation has finished.
On the callee side, the stub function is synchronous by default which makes the implementation easy:
int multiply(int a, int b, int *product, int prio)
{
*product = a * b;
return 0;
}
The IPC functions are always called from the process main loop. There is no multithreading involved so you don’t need to care about race conditions.
Note that you should neither call any blocking functions in such IPC functions, nor do other long running operations which could block the main loop.
Asynchronous IPC Stubs
In case that the function is more complex than the multiply example above, e.g. if you want to delegate the operation to another asynchronous API, you might prefer asynchronous IPC stubs. Keep in mind that you are not allowed to call blocking functions like file I/O. In this example we’re using fictional asynchronous function for a firmware update.
The fictional API we want to use in this example looks like this:
#ifndef __FICTIONAL_FIRMWARE_H__
#define __FICTIONAL_FIRMWARE_H__
#include <stddef.h>
int fictive_firmware_burn(const char *data, size_t size, void (*callback)(int , void *), void *user_data);
#endif
The implementation of this function itself uses POSIX threads to call a blocking function in a separate thread. This synchronous function simply calls sleep to simulate a long running blocking function call.
To be able to call this via IPC we create the following IDL:
interface firmware
{
include "firmware_config.h";
include "fictional_firmware.h";
[async]
int burn([in] const char *data, [in] size_t len);
}
The keyword async
tells the ICC
to generate an asynchronous function stub. The include
statements specify a header files which should be included in the generated stub code as well.
The generated proxy code is asynchronous like in the previous example:
typedef void (*burn_complete)(int _result, void *cb_data, int prio);
int begin_burn(char *data, size_t len, burn_complete cb, void *cb_data, int prio);
The generated stub code (begin_burn_impl
) is now also asynchronous:
#include "fictional_firmware.h"
#include "firmware.h"
#include <common/errors.h>
#include <platform/memory.h>
struct firmware_ctx {
burn_complete cb;
void *user_data;
int prio;
};
void firmware_complete(int error, void *user_data)
{
struct firmware_ctx *ctx = user_data;
int result;
result = (error == 0) ? 0 : UA_EBAD;
ctx->cb(result, ctx->user_data, ctx->prio);
}
void begin_burn_impl(const char *data, size_t len, burn_complete cb, void *cb_data, int prio)
{
struct firmware_ctx *ctx;
int ret;
if (ctx == NULL) {
cb(UA_EBADNOMEM, cb_data, prio);
return;
}
ctx->cb = cb;
ctx->user_data = cb_data;
ctx->prio = prio;
ret = fictive_firmware_burn(data, len, firmware_complete, ctx);
if (ret != 0) {
cb(UA_EBAD, cb_data, prio);
}
}
This example assumes that you have an asynchronous API for implementing the asynchronous IPC call. Note that using a synchronous blocking implementation and calling stub_burn_complete
(via cb
) at the end of begin_burn_impl
would be wrong, because this would block processing the main loop. If you don’t have an asynchronous API, you need to create one by spawning a thread. So the above fictional API could be implemented like this:
#include "fictional_firmware.h"
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <platform/platform.h>
static int burn(const char *data, size_t size)
{
usleep(1000000);
return 0;
}
struct burn_context {
pthread_t thread;
const char *data;
size_t size;
void (*callback)(int, void *);
void *user_data;
};
static void *burn_thread(void *arg)
{
struct burn_context *ctx = arg;
int error;
error = burn(ctx->data, ctx->size);
ctx->callback(error, ctx->user_data);
free(ctx);
return NULL;
}
int fictive_firmware_burn(const char *data, size_t size, void (*callback)(int, void*), void *user_data)
{
struct burn_context *ctx;
pthread_attr_t attr;
int ret;
ctx = malloc(sizeof(*ctx));
if (ctx == NULL) goto memerror;
ctx->data = data;
ctx->size = size;
ctx->callback = callback;
ctx->user_data = user_data;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
ret = pthread_create(&ctx->thread, &attr, burn_thread, ctx);
pthread_attr_destroy(&attr);
if (ret != 0) goto threaderror;
return 0;
threaderror:
free(ctx);
memerror:
return -1;
}
Using the IPC interface
The ICC
generates init/cleanup
functions for proxy (caller side) and stub (callee side). This can be used to initialize the IPC components. In addition it generates code for dispatching the messages and for polling the IPC message queues.
There are two main use cases for this.
- Creating a separate process, which means the stub code runs in a separate independent process and this process calls the generated
firmware_stub_loop()
function. For this use case you need to call firmware_stub_init(NULL)
, which will create its own IPC message queue and register itself at the main application. The main process will only initialize the proxy side to be able to call IPC function in the other process.
- Using proxy and stub code in the same process from a single-threaded application. In this case we simply reuse the IPC_QUEUEID_MASTER of the main application, which means we are sending messages to ourselves.
The following example shows how to setup up the second approach. The uaapplication has some nice infrastructure for registering IPC components, initialize and cleanup IPC components and for message dispatching. This greatly simplifies the usage of such IPC components.
The following example code creates a minimal UA application (without server or client) just to demonstrate the IPC functionality.
First we need to initialize a uaapplication, which initializes the platform layer and sets up memory pools and IPC for us.
if (ret != 0) {
exit(EXIT_FAILURE);
}
The next step is the initialize the firmware
IPC component.
static void firmware_ipc_init(void)
{
int ret;
ret = uaapplication_register_ipc_component(
firmware_stub_vtable(),
firmware_stub_init,
firmware_proxy_init,
firmware_stub_cleanup,
firmware_proxy_dispatch_service,
&g_firmware_ipc_handle);
if (ret != 0) {
}
}
If there is an init
function you also need a cleanup
function, which we need to call at the end of the program.
static void firmware_ipc_cleanup(void)
{
uaapplication_unregister_ipc_components_by_handle(g_firmware_ipc_handle);
}
After calling firmware_ipc_init
we can use the IPC interface.
printf("Calling fictive_firmware_burn.\n");
ret = begin_burn(data, strlen(data), my_burn_complete, NULL, 0);
if (ret != 0) {
goto error;
}
To make this working we need to process the main loop, which in turn will run the IPC communication, timers and network.
The IPC framework will call our callback, which then stops the main loop be calling ua_shutdown_request_shutdown.
static void my_burn_complete(int _result, void *cb_data, int prio)
{
printf("burn_complete: %i\n", _result);
}
A complete working main.c
looks like this:
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <appconfig.h>
#include <uaapplication/application.h>
#include <trace/trace.h>
#include <platform/shutdown.h>
#include <ipc/queue.h>
#include <firmware.h>
#include <firmware_proxy.h>
#include <firmware_stub.h>
static uint8_t g_firmware_ipc_handle;
static void my_burn_complete(int _result, void *cb_data, int prio)
{
printf("burn_complete: %i\n", _result);
}
static void firmware_ipc_init(void)
{
int ret;
ret = uaapplication_register_ipc_component(
firmware_stub_vtable(),
firmware_stub_init,
firmware_proxy_init,
firmware_stub_cleanup,
firmware_proxy_dispatch_service,
&g_firmware_ipc_handle);
if (ret != 0) {
}
}
static void firmware_ipc_cleanup(void)
{
uaapplication_unregister_ipc_components_by_handle(g_firmware_ipc_handle);
}
int main(int argc, char *argv[])
{
const char *data = "ABCDEF";
int ret;
if (ret != 0) {
exit(EXIT_FAILURE);
}
firmware_ipc_init();
printf("Calling fictive_firmware_burn.\n");
ret = begin_burn(data, strlen(data), my_burn_complete, NULL, 0);
if (ret != 0) {
goto error;
}
printf("Waiting for callback...\n");
}
printf("Done.\n");
firmware_ipc_cleanup();
return EXIT_SUCCESS;
error:
firmware_ipc_cleanup();
return EXIT_FAILURE;
}
CMake Integration
It is easily possible to integrate the proxy/stub code generation into CMake. You only need to include the ICC CMake module and add the command ua_wrap_idl
. This will generate the Makefile commands to generate the code. The first two specified CMake variables receive the filenames for proxy and stub code. Either one of these or both need to be referenced by your build target, depending if this executable should contain proxy and stub code (single process configuration), or just one side (multi process configuration).
Example CMakeLists.txt:
project(firmware C)
cmake_minimum_required(VERSION 3.0)
set(CMAKE_MODULE_PATH $ENV{SDKDIR}/cmake ${CMAKE_MODULE_PATH})
SET(CMAKE_INCLUDE_CURRENT_DIR ON)
# include directories
include($ENV{SDKDIR}/sdk.cmake)
# Use ICC compiler
include(icc)
# Generate proxy/stub code
ua_wrap_idl(FIRMWARE_PROXY_SOURCES FIRMWARE_STUB_SOURCES firmware.idl)
add_executable(firmware
main.c
fictional_firmware.h
fictional_firmware.c
firmware.c
firmware.h
${FIRMWARE_PROXY_SOURCES}
${FIRMWARE_STUB_SOURCES})
target_link_libraries(firmware
${SDK_BASE_LIBRARIES}
${OS_LIBS}
${SDK_SYSTEM_LIBS}
pthread)