High Performance OPC UA Server SDK  1.7.1.383
Using Extension Objects

Structures are stored in Variant values as extension objects. On the wire an ExtensionObject is encoded as TypeId, followed by the Length of the body in bytes and then the body itself, which is the encoded structure data itself. The structure body must be encoded as a series of structure fields, where each field is encoded according to the base types defined in Part6 of the OPC UA Specification. Also nested structures are allowed.

Example:

struct custom_struct {
bool open;
int32_t flow;
float temperature;
};

Encoding Table:

Field Bytes Value
Type Id 4 The encoding id of the Type
Encoding 1 0x1 for Binary encoded data
Length 4 9
Open 1 The value of 'open'
Flow 4 The value of 'flow'
Temperature 4 The value if 'temperature'

In the SDK the extension object can be in two different states, UA_EXTENSIONOBJECT_BINARY or UA_EXTENSIONOBJECT_ENCODEABLEOBJECT. Normally when the SDK decodes a UA Message it tries to decode Variants and its contained ExtensionObjects. When the extension objects's encoding_id is known, it gets decoded and the ExtensionObject contains a pointer to the decoded object (UA_EXTENSIONOBJECT_ENCODEABLEOBJECT). This requires that the types was known at compile time and a C struct was generated for it.

If the type is not known the struct is stored as byte blob in the extension object's body (UA_EXTENSIONOBJECT_BINARY).

See also ua_extensionobject for details on the ExtensionObject.

In this case you can decode the value yourself on application level. A typical use case for this scenario are PLCs where the types are created at runtime by downloading a new PLC program, so the SDK does not know the types at compile time. This also means that there exist no C structs where the decoder could decode to, but some other some other generic data model, which is application specific.

Creating an Extension Object

The SDK offers a simple API to create well-known extensobjects which are know at compile-time.

The workflow is a follows:

  1. Fill a new dynamically allocated C struct with data (this is the encodable object). Use ipc_malloc for memory allocation.
  2. Create the datatype NodeId which identifies the struct in the type system.
  3. Call ua_variant_attach_extensionobject to create a Variant/ExtensionObject.

The SDK API will take ownership of the memory of the given encodable object, to avoid memcpy calls, and takes care of creating the ExtensionObject with correct encoding_id and setting the Variant data type.

The following example demonstrate how to create an ExtensionObject which contains an ua_range.

int create_eurange(struct ua_variant *value, double low, double high)
{
struct ua_nodeid datatype;
struct ua_range *eurange = NULL;
int ret;
ua_nodeid_set_numeric(&datatype, 0, UA_ID_RANGE);
/* allocate memory for ua_range struct */
eurange = IPC_ALLOC(eurange);
if (eurange == NULL) return UA_EBADNOMEM;
/* set minimum/maximum of the range */
eurange->low = low;
eurange->high = high;
/* attach range to an ua_variant */
ret = ua_variant_attach_extensionobject(value, eurange, &datatype);
if (ret != 0) goto out;
return 0;
out:
/* free memory on error */
ipc_free(eurange);
return -1;
}

Processing an Extension Object

When processing Variant values you need to be able to extract the encodable objects from Variant/ExtensionObjects. This is even easier than creating extension objects.

int get_eurange(struct ua_variant *value, struct ua_range **eurange)
{
struct ua_nodeid datatype;
ua_nodeid_set_numeric(&datatype, 0, UA_ID_RANGE);
return ua_variant_detach_extensionobject_of_type(value, (void**)eurange, &datatype);
}

Create Extension Objects with Pre-Encoded Data on Application Level

For well-known types the encodable objects of Variant/ExtensionObjects get serialized when the network message gets serialized. But this works only for known types.

For other types you can encode your struct yourself on application level, and fill the extension object with a pre-encoded ua_bytestring value. When the network message gets serialized, this ua_variant encoder can write the structure to the network stream by using a simple ua_memcpy operation.

Don't worry, you can leverage the existing encoder infrastructure to implement the encoding of custom types.

The following example shows how to encode the custom struct example from the beginning of this section:

static int encode_custom_struct(struct ua_variant *value, const struct custom_struct *custom)
{
struct ua_encoder_context ctx;
struct ua_buffer buf;
struct ua_nodeid encodingid = UA_NODEID_NUMERIC_INITIALIZER(1235, 3);
int ret;
/* create bytestring which will receive the encoded data */
ret = ua_bytestring_create(&data, 9);
if (ret != 0) return ret;
/* create encoder context */
ua_encoder_context_init_bytestring(&ctx, &data, &buf);
/* encode data */
ret = ua_encode_boolean(&ctx, &custom->open);
if (ret != 0) goto error;
ret = ua_encode_int32(&ctx, &custom->flow);
if (ret != 0) goto error;
ret = ua_encode_float(&ctx, &custom->temperature);
if (ret != 0) goto error;
/* update data length with number of encoded bytes */
data.len = buf.pos;
/* store struct in variant as pre-encoded value */
ret = ua_variant_set_extensionobject_binary(value, &data, &encodingid);
if (ret != 0) goto error;
return 0;
error:
return ret;
}

Decoding Extension Objects on Application Level

ExtensionObjects that are not known by the SDK will be stored as ua_bytestring in the ua_extensionobject. You can decode this data in the same way as we have created it in the previous section on application level.

The following example shows how to decode the custom struct example from the beginning of this section:

static int decode_custom_struct(const struct ua_variant *value, struct custom_struct *custom)
{
const struct ua_extensionobject *eo;
struct ua_decoder_context ctx;
struct ua_buffer buf;
struct ua_nodeid encodingid = UA_NODEID_NUMERIC_INITIALIZER(1235, 3);
int ret;
/* extract extensionobject from variant */
if (ret != 0) return ret;
/* check if EO is binary encoded */
if (eo->encoding != UA_EXTENSIONOBJECT_BINARY) return UA_EBADINVALIDARGUMENT;
/* check if EO contains the type we expect */
if (ua_nodeid_compare(&eo->encoding_id, &encodingid) != 0) return UA_EBADINVALIDARGUMENT;
/* create decoder context */
ua_decoder_context_init_bytestring(&ctx, &eo->body.binary, &buf);
/* decode data */
ret = ua_decode_boolean(&ctx, &custom->open);
if (ret != 0) goto error;
ret = ua_decode_int32(&ctx, &custom->flow);
if (ret != 0) goto error;
ret = ua_decode_float(&ctx, &custom->temperature);
if (ret != 0) goto error;
return 0;
error:
return ret;
}