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:
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 thebytes
and returns the resultant object.
To inspect the member types, use the built-in Python
cls.__annotations__
, and for for idl information, use thecls.__idl_annotations__
andcls.__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