Working with IDL

All IDL type support is contained within the subpackage cyclonedds.idl, which enables you to use it even in contexts where you do not need the full Eclipse Cyclone DDS: Python API ecosystem.

Note

There is no official OMG specification for mapping IDL to Python. The solutions here are therefore not standardized and are not compatible with other DDS implementations. However, they are based purely on the standard library type-hinting functionality as introduced in Python 3.5, so that any Python tooling that works with type-hints is also compatible with our implementation. To generate initializers and string representations use the dataclasses standard library module. This is applied outside of the other IDL machinery, therefore you can control immutability, equality checking, or even use a different dataclasses representation, for example runtype.

Working with the IDL compiler

Use the IDL compiler if you already have an IDL file that defines your types, or if you require interoperability with non-Python projects. The idlpy library is built as part of the python package.

After installing the CycloneDDS Python backend you can run idlc with the -l py flag to generate Python code:

idlc -l py your_file.idl

To nest the resulting Python module inside an existing package, you can specify the path from the intended root. If you have a package ‘wubble’ with a submodule ‘fruzzy’ and want the generated modules and types under there you can pass py-root-prefix:

idlc -l py -p py-root-prefix=wubble.fruzzy your_file.idl

IDL Datatypes in Python

The cyclonedds.idl package implements the IDL unions, structs, and their OMG XCDR-V1 encoding, in python. In most cases, the IDL compiler writes the code that references this package without the need to edit the objects. However, for python-only projects it is possible to write the objects manually (where cross-language interactions are not required). To manually write IDL objects, you can make your classes inherit from the following classes:

The following is a basic example of how to use dataclasses (For further information refer to the standard library documentation of dataclasses):

 1from dataclasses import dataclass
 2from cyclonedds.idl import IdlStruct
 3
 4@dataclass
 5class Point2D(IdlStruct):
 6   x: int
 7   y: int
 8
 9p1 = Point2D(20, -12)
10p2 = Point2D(x=12, y=-20)
11p1.x += 5

The dataclass decorator turns a class with just names and types into a dataclass. The IdlStruct parent class makes use of the type information defined in the dataclass to (de)serialize messages. All normal dataclasses functionality is preserved, therefore to define default factories use field from the dataclasses module, or add a __post_init__ method for more complicated construction scenarios.

Types

Not all Python types are encodable with OMG XCDR-V1. Therefore, there are limitations to what you can put in an IdlStruct class. The following is an exhaustive list of types:

Integers

The default Python int type maps to an OMG XCDR-V1 64-bit integer. The types module has all the other integers types that are supported in python.

1from dataclasses import dataclass
2from cyclonedds.idl import IdlStruct
3from cyclonedds.idl.types import int8, uint8, int16, uint16, int32, uint32, int64, uint64
4
5@dataclass
6class SmallPoint2D(IdlStruct):
7   x: int8
8   y: int8

Note

These special types are just normal int<python:int>`s at runtime. They are only used to indicate the serialization functionality what type to use on the network. If you store a number that is not supported by that integer type you will get an error during encoding. The ``int128` and uint128 are not supported.

Floats

The Python float type maps to a 64-bit float, which is a double in C-style languages. The types module has a float32 and float64 type, float128 is not supported.

Strings

The Python str type maps directly to the XCDR string. It is encoded with utf-8. Inside types there is the bounded_str type for a string with maximum length.

1from dataclasses import dataclass
2from cyclonedds.idl import IdlStruct
3from cyclonedds.idl.types import bounded_str
4
5@dataclass
6class Textual(IdlStruct):
7   x: str
8   y: bounded_str[20]

Lists

The Python list is a versatile type. In normal python, a list is able to contain other types, but to be able to encode it, all of the contents must be the same type, and this type must be known beforehand. This can be achieved by using the sequence type.

1from dataclasses import dataclass
2from cyclonedds.idl import IdlStruct
3from cyclonedds.idl.types import sequence
4
5@dataclass
6class Names(IdlStruct):
7   names: sequence[str]
8
9n = Names(names=["foo", "bar", "baz"])

In XCDR, this results in an ‘unbounded sequence’, which in most cases should be acceptable. However, use annotations to change to either:

  • A ‘bounded sequence’. For example, to limit the maximum allowed number of items.

  • An ‘array’. For example, if the length of the list is always the same.

1from dataclasses import dataclass
2from cyclonedds.idl import IdlStruct
3from cyclonedds.idl.types import sequence, array
4
5@dataclass
6class Numbers(IdlStruct):
7   ThreeNumbers: array[int, 3]
8   MaxFourNumbers: sequence[int, 4]

Dictionaries

Note

Currently, dictionaries are not supported by the IDL compiler. However, if your project is pure python there is no problem in using them.

Unlike the built-in Python dict both the key and the value must have a constant type. To define a dictionary, use the Dict from the typing module.

1from typing import Dict
2from dataclasses import dataclass
3from cyclonedds.idl import IdlStruct
4
5@dataclasses
6class ColourMap(IdlStruct):
7   mapping: Dict[str, str]
8
9c = ColourMap({"red": "#ff0000", "blue": "#0000ff"})

Unions

Unions in IDL are different to the unions defined in the typing module. IDL unions are discriminated, which means that they have a value that indicates which of the possibilities is active.

To write discriminated unions, use the following:

  • @union decorator

  • case helper type.

  • default helper type.

Write the class in a dataclass style, except only one of the values can be active at a time. The @union decorator takes one type as argument, which determines the type of what is differentiating the cases.

 1from enum import Enum, auto
 2from dataclasses import dataclass
 3from cyclonedds.idl import IdlUnion, IdlStruct
 4from cyclonedds.idl.types import uint8, union, case, default, MaxLen
 5
 6
 7class Direction(Enum):
 8   North = auto()
 9   East = auto()
10   South = auto()
11   West = auto()
12
13
14class WalkInstruction(IdlUnion, discriminator=Direction):
15   steps_n: case[Direction.North, int]
16   steps_e: case[Direction.East, int]
17   steps_s: case[Direction.South, int]
18   steps_w: case[Direction.West, int]
19   jumps: default[int]
20
21@dataclass
22class TreasureMap(IdlStruct):
23   description: str
24   steps: sequence[WalkInstruction, 20]
25
26
27map = TreasureMap(
28   description="Find my Coins, Diamonds and other Riches!\nSigned\nCaptain Corsaro",
29   steps=[
30      WalkInstruction(steps_n=5),
31      WalkInstruction(steps_e=3),
32      WalkInstruction(jumps=1),
33      WalkInstruction(steps_s=9)
34   ]
35)
36
37print (map.steps[0].discriminator)  # You can always access the discriminator, which in this case would print 'Direction.North'

Objects

To reference other classes as member a type, use IdlStruct or IdlUnion classes that only contain serializable members.

 1from dataclasses import dataclass
 2from cyclonedds.idl import IdlStruct
 3from cyclonedds.idl.types import sequence
 4
 5@dataclass
 6class Point2D(IdlStruct):
 7   x: int
 8   y: int
 9
10@dataclass
11class Cloud(IdlStruct):
12   points: sequence[Point]

Serialization

Serialization and deserialization automatically occur within the backend. For debug purposes, or outside a DDS context it can be useful to look at the serialized data, or create Python objects from raw bytes. By inheriting from IdlStruct or IdlUnion, the defined classes automatically gain instance.serialize() -> bytes and cls.deserialize(data: bytes) -> cls functions.

  • Serialize is a member function that returns bytes with the serialized object.

  • Deserialize is a classmethod that takes the bytes and returns the resultant object.

To inspect the member types, use the built-in Python cls.__annotations__, and for for idl information, use the cls.__idl_annotations__ and cls.__idl_field_annotations__.

 1from dataclasses import dataclass
 2from cyclonedds.idl import IdlStruct
 3
 4@dataclass
 5class Point2D(IdlStruct):
 6   x: int
 7   y: int
 8
 9p = Point2D(10, 10)
10data = p.serialize()
11q = Point2D.deserialize(data)
12
13assert p == q

Idl Annotations

In IDL you can annotate structs and members with several different annotations, for example @key. In Python we have decorators, but they only apply to classes not to fields. This is the reason why the syntax in Python for a class or field annotation differ slightly. Note: The IDL #pragma keylist is a class annotation in python, but functions in exactly the same way.

 1from dataclasses import dataclass
 2from cyclonedds.idl import IdlStruct
 3from cyclonedds.idl.annotations import key, keylist
 4
 5@dataclass
 6class Type1(IdlStruct):
 7   id: int
 8   key("id")
 9   value: str
10
11@dataclass
12@keylist(["id"])
13class Type2(IdlStruct):
14   id: int
15   value: str