POP Parsing DSL

The informations transmitted in the POP encoded messages don’t include the type of the transmitted values as these are defined at compilation time and known to both involved parties.

With the use of python and the need to decode message exchanges between arbitrary peers, information about the transmitted values data-types is not available and has to be provided somehow.

The implemented python based parser for POP messages introduces the concept of a types registry. Each time a message has to be decoded, the specific types of the payload are retrieved from the registry using the classid/methodid couple or the exception code contained in the POP payload header.

As the measuring of arbitrary programs introduces new classes, methods and data types, it does not suffice to provide a built-in set of known data types and the need to let the final pas user specify custom and complex types arises.

For this purpose a special python-based domain specific language as been defined. The DSL builds upon a few directives to be able to define primitive types (rarely needed), complex types and new classes and to register them to the registry in order to let the parser retrieve them when needed.

Refer to the Built-in parser types document for an overview of the already provided types and classes.

Defining classes and methods

Defining a new class and its corresponding methods is an easy task thanks to the declarative syntax offered by the POP Parsing DSL.

Each file which will finish up loaded by the types registry shares a common import:

from pas.parser.types import *

Once imported the different types, it is possible to use the pas.parser.types.cls(), pas.parser.types.func() and pas.parser.types.exc() functions to create new classes, methods and exceptions respectively.

Their API is the following:

pas.parser.types.cls(id, name, methods)[source]

Allows to define and automatically register a new POP class.

Parameters:
  • id (int) – The classid to which this class will be mapped.
  • name (str or unicode compatible object) – The name to use as string representation of this class. Normally its the original class name.
  • methods (list of pas.parser.types.func() objects) – The list of methods bound to an instance of this class.
pas.parser.types.func(id, name, args, retypes)[source]

Allows to define a new POP method bound to a specific class instance. The method itself will never know to which class it belongs; to bind a method to a class insert it in the methods argument at class declaration time.

Parameters:
  • id (int) – The method ID to which this method will be mapped.
  • name (str or unicode compatible object) – The name to use as string representation of this method. Normally its always the original method name.
  • args (list of POP Parser DSL types) – A list of arguments taken by this method. The provided types will be directly used to decode the payload. Any built-in or custom defined scalar or complex type is accepted.
  • retypes (list of POP Parser DSL types) –

    A list of types of the values returned by this method. The provided types will be directly used to decode the payload. Any built-in or custom defined scalar or complex type is accepted.

    Note that an [out] argument will be present in the POP response and shall thus be inserted into the retypes list.

pas.parser.types.exc(id, name, properties)[source]

Allows to define and automatically register a new POP exception type.

Parameters:
  • id (int) – The exception ID to which this exception will be mapped.
  • name (str or unicode compatible object) – The name to use as string representation of this exception. Normally its always the original exception name.
  • properties (list of POP Parser DSL types) – The list of properties bound to an instance of this exception.

A possible definition of the paroc_service_base class using the parser DSL is the following:

# Import the POP types and the cls and func helpers
from pas.parser.types import *

# Define the class and its methods
cls(0, 'paroc_service_base', [
    func(0,  'BindStatus',  [],       [int, string, string]),   # broker_receive.cc:183
    func(1,  'AddRef',      [],       [int]),                   # broker_receive.cc:218
    func(2,  'DecRef',      [],       [int]),                   # broker_receive.cc:238
    func(3,  'Encoding',    [string], [bool]),                  # broker_receive.cc:260
    func(4,  'Kill',        [],       []),                      # broker_receive.cc:287
    func(5,  'ObjectAlive', [],       [bool]),                  # broker_receive.cc:302
    func(6,  'ObjectAlive', [],       []),                      # broker_receive.cc:319
    func(14, 'Stop',        [string], [bool]),                  # paroc_service_base.ph:47
])

# Done. No further actions are required

By reading the preceding snippet, a few observations can be made:

  1. Not all methods are defined. In fact, the parser doesn’t care if the object is fully defined or not until it doesn’t receive a request or a response for an inexistent method ID.
  2. Methods can return multiple values. The first return value is always mapped to the C++ return value (if non-void), while the following values are mapped to the arguments which were passed as references.
  3. The bool type is often used in responses. The actual C++ return type is an int, but as it is interpreted only as a success flag a bool type has more semantic meaning in these particular cases.

Defining complex and composite types

In the previous section the process of defining a new class and its methods was presented, but the illustrated example was based on a relative simple class.

What has to be done, if for example, the POPCSearchNode class has to be defined (more specifically its callbackResult method)?

The following is the signature of the POPCSearchNode::callbackResult method:

conc async void callbackResult(Response resp);

As you might have noticed, it takes a Response object as argument, so a possible definition using the parser DSL syntax would be:

cls(1001, 'POPCSearchNode', [
    # ...other methods here...
    func(38, 'callbackResult', [Response], []),
])

But how is the Response object defined and how does the parser know how to decode it? The Response object is what in the python parser domain is called a compound type and fortunately its definition is much easier than you might think; we know that a Response object is composed by a string, a NodeInfo object and an ExplorationList object, so its definition becomes:

Response = compound('Response',
    ('uid', string),
    ('nodeInfo', NodeInfo),
    ('explorationList', ExplorationList),
)

This example introduced two new concepts:

  1. The compound function, which allows to create composite objects based on a list of (name, type) tuples;
  2. Nested compound objects definition. A compound object can further contain compound objects (the preceding snippet contains NodeInfo and ExplorationList as subtypes).

If we look deeper in detail, we’ll see that the ExplorationList object isn’t actually a compound object, but rather a list of compound objects.

Fortunately, the task of defining a list is also easy using the shortcuts offered by the DSL:

ExplorationList = array(
    compound('ListNode',
        ('nodeId', string),
        ('visited', array(string)),
    )
)

As you can see, defining new complex types is an easy task once the syntax and the different composers are known. Below you can find the API of the different composers that come built-in with the python POP parser:

pas.parser.types.compound(name, *parts)[source]

Creates a new compound type consisting of different parts. The parts are specified by the parts argument and read one adter the other from the POP payload.

Parameters:
  • name – The name to give to the new type, used when pretty printing the value.
  • parts (list of (name, type) tuples) – The actual definition of the compound type.

Use it like this:

NewType = compound('NewTypeName',
    ('member_1', string),
    ('member_2', int),
    ('member_3', float)
    # ...
)
pas.parser.types.dict(key, value)[source]

Creates a new complex type mapping keys to values. All keys will share the same value type and so will all values.

Parameters:
  • key – The type to use to decode the key.
  • value – The type to use to decode the value.

Use it like this:

# SomeCompoundType.dict_member will hold a mapping of strings to integers

SomeCompoundType = compound('SomeCompountTypeName',
    # ...other members...
    ('dict_member', dict(string, float)),
)
pas.parser.types.array(type)[source]

Creates a new complex type representing a length prefixed array of homogeneous items.

Parameters:type – The type of each item in the array.

Use it like this:

# SomeCompoundType.array_member will hold a variable length array of ints

SomeCompoundType = compound('SomeCompountTypeName',
    # ...other members...
    ('array_member', array(int)),
)
pas.parser.types.optional(type, unpack_bool=<function popbool at 0x22d6e60>)[source]

Special composer type which allows to declare a structure member as optional. An optional member is member prefixed by a bool flag; the flag is first read, if it yields true, it means that the value was encoded and it can be read, if it yields false, it means that no value was encoded and the member is skipped.

Parameters:
  • type – The type of the optional value.
  • unpack_bool (callable) – The type to use to decode the boolean flag. Defaults to popbool, but can be changed to bool if the encoding is done in an RPC compliant way.

Use it like this:

# SomeCompoundType.optional_member will hold a string if it was encoded
# or None if it was not encoded

SomeCompoundType = compound('SomeCompountTypeName',
    # ...other members...
    ('optional_member', string),
)

Defining scalars

In the previous sections we have seen how to create new classes with bound methods and how to define new argument or return types for those by combining scalars into more complex structure in different ways.

The last building block needed to fully grasp the parsing details is the decoding of scalars. It is here that the real work happens, as complex or compound types only arrange scalars in different orders to decode a full structure, but don’t tell them how to decode the single values.

New scalar types are not needed as much as new complex types, but it can serve to better understand the whole parsing process as well. Furthermore it allows to define how to decode POP-C++ specific types (e.g. the popbool primitive) which don’t comply with the standards.

A scalar type, as well as all types returned from the different composers seen above, is simply a callable which accepts an xdrlib.Unpacker object and returns a decoded python object. Refer to the Unpacker documentation for further information about the already supported data types.

As an example, the implementation of the popbool type is shown below:

import __builtin__

# Define a callable which taks a single argument
def popbool(stream):
   # Read an integer and check that the highest byte is set
   result = stream.unpack_int() & 0xff000000

   # Coearce the value to a boolean (use the __builtin__ pacakge as the
   # previous bool definition overwrote the built-in definition)
   return __builtin__.bool(result)

Table Of Contents

Previous topic

Custom subcommands

Next topic

Running pas with real machines

This Page