High Performance OPC UA Server SDK  1.7.1.383
IPC

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.

singlethreaded_multithreaded.png

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 /* __FICTIONAL_FIRMWARE_H__ */

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:

/*****************************************************************************
* *
* Copyright (c) 2006-2023 Unified Automation GmbH. All rights reserved. *
* *
* Software License Agreement ("SLA") Version 2.8 *
* *
* Unless explicitly acquired and licensed from Licensor under another *
* license, the contents of this file are subject to the Software License *
* Agreement ("SLA") Version 2.8, or subsequent versions as allowed by the *
* SLA, and You may not copy or use this file in either source code or *
* executable form, except in compliance with the terms and conditions of *
* the SLA. *
* *
* All software distributed under the SLA is provided strictly on an "AS *
* IS" basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, *
* AND LICENSOR HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT *
* LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR *
* PURPOSE, QUIET ENJOYMENT, OR NON-INFRINGEMENT. See the SLA for specific *
* language governing rights and limitations under the SLA. *
* *
* The complete license agreement can be found here: *
* http://unifiedautomation.com/License/SLA/2.8/ *
* *
*****************************************************************************/
#include "fictional_firmware.h"
#include "firmware.h"
#include <common/errors.h>
#include <platform/memory.h>
/* a context we need to store some IPC parameters,
* when calling the fictive_firmware_burn function.
*/
struct firmware_ctx {
burn_complete cb;
void *user_data;
int prio;
};
/* fictional firmware callback function */
void firmware_complete(int error, void *user_data)
{
struct firmware_ctx *ctx = user_data;
int result;
/* handle firmware errors */
result = (error == 0) ? 0 : UA_EBAD;
/* call the IPC callback */
ctx->cb(result, ctx->user_data, ctx->prio);
ua_free(ctx);
}
/* Asynchronous IPC stub function implementation */
void begin_burn_impl(const char *data, size_t len, burn_complete cb, void *cb_data, int prio)
{
struct firmware_ctx *ctx;
int ret;
/* create context for async firmware operation */
ctx = ua_malloc(sizeof(*ctx));
if (ctx == NULL) {
/* return error in IPC callback */
cb(UA_EBADNOMEM, cb_data, prio);
return;
}
/* store IPC parameters in context */
ctx->cb = cb;
ctx->user_data = cb_data;
ctx->prio = prio;
/* start firmware burn operation using a fictional firmware library function */
ret = fictive_firmware_burn(data, len, firmware_complete, ctx);
if (ret != 0) {
/* return error in IPC callback */
cb(UA_EBAD, cb_data, prio);
ua_free(ctx);
}
}

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:

/*****************************************************************************
* *
* Copyright (c) 2006-2023 Unified Automation GmbH. All rights reserved. *
* *
* Software License Agreement ("SLA") Version 2.8 *
* *
* Unless explicitly acquired and licensed from Licensor under another *
* license, the contents of this file are subject to the Software License *
* Agreement ("SLA") Version 2.8, or subsequent versions as allowed by the *
* SLA, and You may not copy or use this file in either source code or *
* executable form, except in compliance with the terms and conditions of *
* the SLA. *
* *
* All software distributed under the SLA is provided strictly on an "AS *
* IS" basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, *
* AND LICENSOR HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT *
* LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR *
* PURPOSE, QUIET ENJOYMENT, OR NON-INFRINGEMENT. See the SLA for specific *
* language governing rights and limitations under the SLA. *
* *
* The complete license agreement can be found here: *
* http://unifiedautomation.com/License/SLA/2.8/ *
* *
*****************************************************************************/
#include "fictional_firmware.h"
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <platform/platform.h>
/* blocking firmware function */
static int burn(const char *data, size_t size)
{
UA_UNUSED(data);
UA_UNUSED(size);
usleep(1000000);
return 0;
}
/* Thread context */
struct burn_context {
pthread_t thread;
const char *data;
size_t size;
void (*callback)(int, void *);
void *user_data;
};
/* The firmware burn thread which uses a blocking function. */
static void *burn_thread(void *arg)
{
struct burn_context *ctx = arg;
int error;
/* burn the firmware */
error = burn(ctx->data, ctx->size);
/* send callback */
ctx->callback(error, ctx->user_data);
/* cleanup memory */
free(ctx);
return NULL;
}
/* Async firmware burn function.
* @param data Pointer to firmware data.
* @param size Length of data in bytes.
* @param callback The address of the callback function.
* @param A userdata pointer the will be passed back in the callback.
* @return Zero on success, -1 if the operation fails.
* If this function returns zero the callback will be called,
* if the operation fails the callback will not be called.
* */
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;
/* create thread context */
ctx = malloc(sizeof(*ctx));
if (ctx == NULL) goto memerror;
ctx->data = data;
ctx->size = size;
ctx->callback = callback;
ctx->user_data = user_data;
/* create detached thread */
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; /* success */
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.

  1. 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.
  2. 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.

ret = uaapplication_init(&g_app, TRACE_LEVEL_ERROR, TRACE_FAC_ALL, "settings.conf");
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "uaapplication_init failed.\n");
exit(EXIT_FAILURE);
}

The next step is the initialize the firmware IPC component.

/* Helper function for registering 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) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "failed to register 'firmware' IPC component.\n");
}
}

If there is an init function you also need a cleanup function, which we need to call at the end of the program.

/* Helper function for unregistering IPC component. */
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) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "failed to start firmware burn process.\n");
goto error;
}

To make this working we need to process the main loop, which in turn will run the IPC communication, timers and network.

while (ua_shutdown_should_shutdown() == 0) {
/* process regular application */
}

The IPC framework will call our callback, which then stops the main loop be calling ua_shutdown_request_shutdown.

/* This is the firmware IPC callback, that is called when the operation is finished. */
static void my_burn_complete(int _result, void *cb_data, int prio)
{
UA_UNUSED(cb_data);
UA_UNUSED(prio);
printf("burn_complete: %i\n", _result);
}

A complete working main.c looks like this:

/*****************************************************************************
* *
* Copyright (c) 2006-2023 Unified Automation GmbH. All rights reserved. *
* *
* Software License Agreement ("SLA") Version 2.8 *
* *
* Unless explicitly acquired and licensed from Licensor under another *
* license, the contents of this file are subject to the Software License *
* Agreement ("SLA") Version 2.8, or subsequent versions as allowed by the *
* SLA, and You may not copy or use this file in either source code or *
* executable form, except in compliance with the terms and conditions of *
* the SLA. *
* *
* All software distributed under the SLA is provided strictly on an "AS *
* IS" basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, *
* AND LICENSOR HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT *
* LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR *
* PURPOSE, QUIET ENJOYMENT, OR NON-INFRINGEMENT. See the SLA for specific *
* language governing rights and limitations under the SLA. *
* *
* The complete license agreement can be found here: *
* http://unifiedautomation.com/License/SLA/2.8/ *
* *
*****************************************************************************/
#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 generated proxy/stub files */
#include <firmware.h>
#include <firmware_proxy.h>
#include <firmware_stub.h>
struct uaapplication g_app;
struct appconfig g_appconfig;
static uint8_t g_firmware_ipc_handle;
/* This is the firmware IPC callback, that is called when the operation is finished. */
static void my_burn_complete(int _result, void *cb_data, int prio)
{
UA_UNUSED(cb_data);
UA_UNUSED(prio);
printf("burn_complete: %i\n", _result);
}
/* Helper function for registering 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) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "failed to register 'firmware' IPC component.\n");
}
}
/* Helper function for unregistering IPC component. */
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;
UA_UNUSED(argc);
UA_UNUSED(argv);
ret = uaapplication_init(&g_app, TRACE_LEVEL_ERROR, TRACE_FAC_ALL, "settings.conf");
if (ret != 0) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "uaapplication_init failed.\n");
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) {
TRACE_ERROR(TRACE_FAC_APPLICATION, "failed to start firmware burn process.\n");
goto error;
}
printf("Waiting for callback...\n");
while (ua_shutdown_should_shutdown() == 0) {
/* process regular application */
}
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)