Working with idl

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

All idl type support is contained within the subpackage cyclonedds.idl, allowing you to use it even in contexts where you do not need the full CycloneDDS Python ecosystem.

Working with the IDL compiler

You use the IDL compiler if you already have an idl file to define your types or if you require interoperability with non-Python projects. The idlpy library is built as part of the python package leveraging the scikit-build cmake integration. We will soon integrate a locator mechanism into idlc to retrieve the library location, since the idlpy library will be part of the python package installation and not on the LD_LIBRARY_PATH or equivalent. For now you can employ the same tactic as idlc will do eventually and use a hidden method:

idlc -l $(python -c "import cyclonedds.__idlc__") your_file.idl

IDL Datatypes in Python

The cyclonedds.idl package implements defining IDL unions and structs and their OMG XCDR-V1 encoding in pure python. There should almost never be a need to delve into the details of this package when using DDS. In most cases the IDL compiler will write the code that uses this package. However, it is possible to write the objects manually. This is of course only useful if you do not plan to interact with other language clients, but makes perfect sense in a python-only project. If you are manually writing IDL objects your most important tool is IdlStruct.

The following basic example will be very familiar if you have used dataclasses before. We will go over it here again briefly, for more detail go to the standard library documentation of dataclasses.

 1from dataclasses import dataclass
 2from cyclonedds.idl import idl
 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

As you can see the @idl turns a class with just names and types into a dataclass. The __init__ method is automatically generated for easy object construction. All normal dataclasses functionality is preserved, so you can still use field from the dataclasses module to define default factories or add a __post_init__ method for more complicated construction scenarios.

Types

Not all types that are possible to write in Python are encodable with OMG XCDR-V1. This means that you are slightly limited in what you can put in an @idl class. An exhaustive list follows:

Integers

The default python int type maps to a OMG XCDR-V1 64 bit integer. For most applications that should suffice, but the types module has all the other integers types 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 that these special types are just normal 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 would be 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. Under the hood 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 would be able to contain any 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 will result in an ‘unbounded sequence’, which should be fine in most cases. However, you can switch over to a ‘bounded sequence’ or ‘array’ using annotations. This can be useful to either limit the maximum allowed number of items (bounded sequence) or if the length of the list is always the same (array).

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

Currently dictionaries are not supported by the Cyclone IDL compiler. However, if your project is pure python there is no problem in using them. Unlike a raw python dict both the key and the value need to have a constant type. This is expressed using 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 not like the Unions defined in the typing module. IDL unions are discriminated, meaning they have a value that indicates which of the possibilities is active.

You can write discriminated unions using the @union decorator and the case and default helper types. You again write a 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

You can also reference other classes as member type. These other classes should be IdlStruct or IdlUnion classes and again 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

If you are using a DDS system you should not need this, serialization and deserialization happens automatically within the backend. However, for debug purposes or outside a DDS context it might be useful to look at the serialized data or create python objects from raw bytes. By inheriting from IdlStruct or IdlUnion the classes you define automatically gain instance.serialize() -> bytes and a cls.deserialize(data: bytes) -> cls functions. Serialize is a member function that will return bytes with the serialized object. Deserialize is a classmethod that takes the bytes and returns the resultant object. You can also inspect the python builtin cls.__annotations__ for the member types and the cls.__idl_annotations__ and cls.__idl_field_annotations__ for idl information.

 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. As an aside, the IDL #pragma keylist is a class annotation in python, but functions in the exact 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