High Performance OPC UA Server SDK  1.1.1.177
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:

firmware.h:
#ifndef __FIRMWARE_H__
#define __FIRMWARE_H__
int fictive_firmware_burn(const char *data, size_t size, void (*callback)(int error), void *user_data);
#endif /* __FIRMWARE_H__ */

To be able to call this via IPC we create the following IDL:

interface firmware
{
include "firmware.h";
[async]
int burn([in] char *data, [in] size_t len);
}

The keyword async tells the ICC to generate an asynchronous function stub. The include statement specifies a header file 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 "firmware.h" /* fictional firmware library */
/* 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 */
if (error == 0)
result = 0;
else
result = UA_EBAD;
/* call the IPC callback */
ctx->cb(result, ctx->user_data, ctx->prio);
mem_free(ctx);
}
/* Asynchronous IPC stub function implementation */
void begin_burn_impl(char *data, size_t len, burn_complete cb, void *cb_data, int prio)
{
int ret;
/* create context for async firmware operation */
ctx = mem_malloc(sizeof(*ctx));
if (ctx == NULL) {
/* return error in IPC callback */
cb(UA_EBADNOMEM, user_data, prio);
return;
}
/* store IPC parameters in context */
ctx->cb = cb;
ctx->user_data = user_data;
ctx->prio = prio;
/* start firmware burn operation using a fictional firmware library function */
ret = fictive_firmware_burn(data, size, firmware_complete, ctx);
if (ret != 0) {
/* return error in IPC callback */
cb(UA_EBAD, user_data, prio);
mem_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-2016 Unified Automation GmbH. All rights reserved. *
* *
* Software License Agreement ("SLA") Version 2.6 *
* *
* 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.6, 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.6/ *
* *
*****************************************************************************/
#include <stdlib.h>
#include <pthread.h>
/* blocking firmware function */
extern int burn(const char *data, size_t size);
/* Thread context */
struct burn_context {
pthread_t thread;
const char *data;
size_t size;
void (*callback)(int error);
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);
/* cleanup memory */
free(ctx);
}
/* 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 error), 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;
}

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 ${CMAKE_CURRENT_SOURCE_DIR}/../../src/cmake ${CMAKE_MODULE_PATH})
# include directories
include_directories(../../include)
# 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
firmware.c
burn.c
${FIRMWARE_PROXY_SOURCES}
${FIRMWARE_STUB_SOURCES})
target_link_libraries(firmware pthread)