The library provides 2 ways of parsing a message:

  1. Manual - using message iterators
  2. Automatic - using a predefined schema

Automatic parsing

In most cases, a method call will have a predefined schema for the data that we want to accept. When there is such an arrangement, the library provides a type-safe API for automatically validating a method call with the provided schema and extracting the data correctly.

Defining a schema

Simple schema

A schema is defined using the UDBus::Type<typename T, typename ...More> structure and initialising it with references to the variables that you want your application to store the resulting data in.

For example, here's how you define a simple structure:

int a = 0;
const char* b = "Hello, World!";

auto data = UDBus::Type
{
    a,
    b
};

Structures

If you need to use structures as part of the method call arguments, you can use the UDBus::Struct<typename T, typename ...More> struct, like this:

int a = 0;
const char* b = "Hello, World!";

auto s = UDBus::Struct
{
    a,
    b
};

auto data = UDBus::Type
{
    a,
    b,
    s
};

Due to restrictions of the C++ language and our API, we're required to take lvalue references. This forces the above code style, which is not the prettiest.

To make the code more structured, you can use the UDBus::makeStruct function that will temporarily allocate a struct, which will then be automatically deallocated when the type containing it is freed.

Example:

int a = 0;
const char* b = "Hello, World!";

auto data = UDBus::Type
{
    a,
    b,
    UDBus::makeStruct(UDBus::Struct
    {
        a,
        b
    })
};

Tip

The UDBus::Struct type is actually a child of the UDBus::Type type. You may use this to your advantage in some scenarios.

Variants

To define a variant type, use the UDBus::Variant struct. It looks like this:

struct Variant
{
        std::function<bool(Message&, Iterator&, void**, void*)> parse = [](Message&, Iterator&, void**, void*) -> bool { return true; };
        void* data = nullptr;
};

The parse callback is called when the variant is encountered and allows you to apply both an automatic and manual parsing strategy.

It gets the following arguments:

  1. A reference to the current message
  2. A reference to the current parent iterator
  3. A pointer to the data member of the Variant struct. You can use this to store any data produced by the parse callback.
  4. A user pointer which can be set using UDBus::Message::setUserPointer()

Automatic parsing can be done by calling UDBus::Message::handleMessage() method with a reference to an instance of UDBus::Type and a pointer to the current parent iterator. For example:

UDBus::Variant
{
    .parse = [](UDBus::Message& message, UDBus::Iterator& it, void** v, void*) -> bool
    {
        auto* variantStruct = new VariantStructure{};
     
        auto integer = UDBus::Type
        {
            variantStruct->integer,
            UDBus::bump()
        };
   
        auto string = UDBus::Type
        {
            variantStruct->string,
            UDBus::bump()
        };
   
        UDBus::Type structure
        {
            UDBus::makeStruct(UDBus::Struct{
                variantStruct->structure.a,
                variantStruct->structure.b,
                variantStruct->structure.f
            }),
            UDBus::bump()
        };
   
        if (message.handleMessage(integer, &it) == UDBus::RESULT_SUCCESS)
            variantStruct->type = VARIANT_TYPE_INT;
        else if (message.handleMessage(string, &it) == UDBus::RESULT_SUCCESS)
            variantStruct->type = VARIANT_TYPE_STRING;
        else if (message.handleMessage(structure, &it) == UDBus::RESULT_SUCCESS)
            variantStruct->type = VARIANT_TYPE_STRUCT;
        else
            variantStruct->type = VARIANT_TYPE_NONE;
   
        UDBUS_FREE_TYPE(integer);
        UDBUS_FREE_TYPE(string);
        UDBUS_FREE_TYPE(structure);
   
        *v = variantStruct;
        return true;
    }
}

Code like this can be used to conditionally deserialise into multiple variant templates easily and efficientlly.

Information on doing manual parsing can be found here.

Arrays and dictionaries

Arrays and dictionaries are parsed automatically, only if and when using the following standard containers:

  1. std::vector
  2. std::map
  3. std::unordered_map

However, you also have the option of defining custom containers using the following macros:

  1. UDBUS_USING_CUSTOM_DICT_TYPES
  2. UDBUS_USING_CUSTOM_ARRAY_TYPES

When these macros are not defined, the following 2 functions are part of the UDBus namespace:

template<typename T>
constexpr bool is_map_type() noexcept
{
    if constexpr (is_specialisation_of<std::unordered_map, T>{} || is_specialisation_of<std::map, T>{})
        return true;
    return false;
}

template<typename T>
constexpr bool is_array_type() noexcept
{
    if constexpr (is_specialisation_of<std::vector, T>{})
        return true;
    return false;
}

When the macros are defined, you should redefine these functions in the UDBus namespace, and you should change your code so that it returns true on the appropriate types.

Caution

A complex array, i.e. an array of type similar to std::vector<UDBus::Struct<...>>, has its data internally heap-allocated, so after usage you need to call the static UDBus::Type<...>::destroyComplex function in order to not cause a memory leak.

Caution

A complex dictionary, i.e. a dictionary with a complex value with a type similar to std::map<int, UDBus::Struct<...>>, has its data internally heap-allocated, so after usage you need to call the static UDBus::Type<...>::destroyComplex function in order to not cause a memory leak.

Tip

To call UDBus::Type<...>::destroyComplex, you can use the . syntax. For example, if we have UDBus::Type<...> t; we can use t.destroyComplex(array);. Alternatively, you can use the :: syntax to silence linter warnings, caused by using the . syntax on a static member like this: decltype(t)::destroyComplex(array).

Arrays and dictionaries of variants

To be able to parse an array or dictionary of variants, you need to provide a template. To do that, use the UDBus::associateWithVariant function:

std::unordered_map<int, UDBus::Variant> v;

auto data = UDBus::Type
{
    UDBus::associateWithVariant(v, UDBus::makeVariant
    (
        UDBus::Variant
        {
            .parse = [](UDBus::Message& message, UDBus::Iterator& it, void** v, void*) -> bool
            {
                std::cout << "Array with variant" << std::endl;
                return true;
            }
        }
    ))
};

Object paths

Object paths are represented as strings of type char* or const char*. The library does no validation for these types.

Skipping certain types

If you want to skip getting the value from a certain type, you can call the UDBus::ignore() function when constructing the instance of the Type<...> struct.

For example, if receiving a message with a signature of iis, you can skip the second integer like this:

auto type = UDBus::Type
{
    a,
    UDBus::ignore(),
    b,
};

Note

The number of fields of the schema and the data received over the dbus bus should still match when UDBus::ignore() is called as the last variable of the Type<...> instance.

Bumping the size of a type

Types should always have more than 2 types in the variadic list, however you may have a situation where you want to parse only 1 item. To do that, you can use the UDBus::bump utility function to bump the size of the Type<...> structure.

For example:

auto type = UDBus::Type
{
    a,
    UDBus::bump(),
};

Parsing calls to a server

In the main loop of your application, you need to call the UDBus::handleAutomatic with the names of the interface and method, as well as the schema:

while(true)
{
    UDBus::handleMethodCall("interface", "method", data);
}

Once you're done with using the schema, you can free it using the UDBus::Type::destroy static method. We also provide the shorthand macro UDBUS_FREE_TYPE(schema) that makes your code prettier.

Caution

There is currently no way to cleanly free all data using the destructor of the type structure. Deallocation has to be done manually!

Tip

To call UDBus::Type<...>::destroy, you can use the . syntax. For example, if we have UDBus::Type<...> t; we can use t.destroy(t);. Alternatively, you can use the :: syntax to silence linter warnings, caused by using the . syntax on a static member like this: decltype(t)::destroy(t).

Parsing a generic message

You can also parse a generic message in the same way using the UDBus::Message::handleMessage member function. It looks like this:

template<typename T, typename... T2>
MessageGetResult handleMessage(Type<T, T2...>& t, UDBus::Iterator* iterator = nullptr) noexcept;

The first argument is the required data schema. The second argument is an optional pointer to an iterator. Provide a valid pointer if you're parsing a part of an existing message.

Caution

The iterator pointer should be null only when you intend to wipe the current message's entire parsing state for the purpose of parsing an entire message from the beginning.

So for example, if you're parsing a variant type as part of an existing message you should always provide the current iterator you get from > the parse callback as an argument to the handleMessage function. Otherwise bad things will happen.

Error handling

The UDBus::Message::handleMethodCall function returns an enum of type UDBus::MessageGetResult, which looks like this:

enum MessageGetResult
{
    RESULT_SUCCESS,
    RESULT_NOT_CALLED,
    RESULT_MORE_FIELDS_THAN_REQUIRED,
    RESULT_LESS_FIELDS_THAN_REQUIRED,
    RESULT_INVALID_BASIC_TYPE,
    RESULT_INVALID_STRUCT_TYPE,
    RESULT_INVALID_ARRAY_TYPE,
    RESULT_INVALID_DICTIONARY_TYPE,
    RESULT_INVALID_DICTIONARY_KEY,
    RESULT_INVALID_VARIANT_TYPE,
    RESULT_INVALID_VARIANT_PARSING,
};

You can check against it to see if any errors occurred during the parsing stage.

Note

Note that RESULT_NOT_CALLED is not an error. It is just there to signal that the method or signal has not been called.

Manual parsing

Manual parsing is done using the UDBus::Iterator class. When parsing a message, the get* methods are used. Example of parsing an array of structures:

UDBus::Iterator it, arrayIt;
it.setGet(reply, arrayIt, true);
        
if (it.get_arg_type() == DBUS_TYPE_ARRAY)
{
    it.recurse();
            
    while (arrayIt.get_arg_type() == DBUS_TYPE_STRUCT)
    {
        UDBus::Iterator structIt;
        arrayIt.setGet(reply, structIt, false);
        arrayIt.recurse();
                
        char* id;
        char* userName;
                
        structIt.get_basic(&id);
        structIt.next();
        structIt.next();
        structIt.get_basic(&userName);
                
        if (username == userName)
        {
            sessionID = id;
            break;
        }
         arrayIt.next();
    }
}

Caution

Calling setGet with the last argument being true should only be done when you're parsing the whole data tree. If you're parsing manually with a provided parent iterator(mostly when parsing Variant types), you SHOULD ALWAYS call it with false.

Example application

Here is the source code to an example demo application:

#include "UntitledDBusUtils/DBusUtils.hpp"
#include <iostream>

struct NestedStructure2
{
    std::vector<int> vector{};
    std::map<const char*, UDBus::Variant> map{};
};

struct NestedStructure
{
    NestedStructure2 structure1{};
    double f{};
    int i{};
};

struct TestingStructure
{
    int integer{};
    udbus_bool_t boolean{};
    const char* string{};
    NestedStructure nestedStructure{};
};

enum VariantType
{
    VARIANT_TYPE_INT,
    VARIANT_TYPE_STRING,
    VARIANT_TYPE_STRUCT,
    VARIANT_TYPE_NONE
};

struct VariantStructure
{
    VariantType type{};
    union
    {
        int integer;
        const char* string;
        struct
        {
            int a{};
            udbus_bool_t b{};
            double f{};
        } structure{};
    };
};

// Quick and dirty printing for debug
void printStruct(const TestingStructure& s)
{
    std::cout << "Integer: " << s.integer << std::endl;
    std::cout << "String: " << s.string << std::endl;
    std::cout << "Boolean: " << std::boolalpha << s.boolean << std::endl;

    // Nested Structure 1
    std::cout << "NestedStructure {" << std::endl;

    std::cout << "    Integer: " << s.nestedStructure.i << std::endl;
    std::cout << "    Float: " << s.nestedStructure.f << std::endl;

    // Nested Structure 2
    std::cout << "    NestedStructure2 {" << std::endl;
    std::cout << "        Array: [ ";

    std::string_view sep;
    for (const auto a : s.nestedStructure.structure1.vector) {
        std::cout << sep << a;
        sep = ", ";
    }
    std::cout << " ]" << std::endl;

    std::cout << "        Dictionary {" << std::endl;
    for (auto& a : s.nestedStructure.structure1.map)
    {
        std::cout << "            " << a.first << ": ";
        auto* vs = static_cast<VariantStructure*>(a.second.data);
        if (vs->type == VARIANT_TYPE_INT)
            std::cout << vs->integer << std::endl;
        else if (vs->type == VARIANT_TYPE_STRING)
            std::cout << vs->string << std::endl;
        else if (vs->type == VARIANT_TYPE_STRUCT)
            std::cout << "{ " << vs->structure.a << ", " << vs->structure.b << ", " << vs->structure.f << " }" << std::endl;
        delete vs;
    }
    std::cout << "        }" << std::endl;
    std::cout << "    }" << std::endl;
    std::cout << "}" << std::endl;
}

int deserialise()
{
    UDBus::Connection connection{};
    
    UDBus::Error error{};
    connection.bus_get(DBUS_BUS_SESSION, error);
    if (error.is_set())
    {
        std::cout << "Couldn't connect to the session bus" << std::endl;
        return 1;
    }

    const auto result = connection.request_name("org.test.Test", DBUS_NAME_FLAG_REPLACE_EXISTING, error);
    if (result != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER)
    {
        std::cout << "Not primary owner of the name" << std::endl;
        return 2;
    }

    TestingStructure s{};

    auto schema = UDBus::Type
    {
        s.integer,
        s.boolean,
        s.string,
        UDBus::makeStruct(UDBus::Struct{
            UDBus::makeStruct(UDBus::Struct{
                s.nestedStructure.structure1.vector,
                UDBus::associateWithVariant(s.nestedStructure.structure1.map, UDBus::makeVariant(
                UDBus::Variant{
                    .parse = [](UDBus::Message& message, UDBus::Iterator& it, void** v, void*) -> bool
                    {
                        auto* variantStruct = new VariantStructure{};

                        auto integer = UDBus::Type
                        {
                            variantStruct->integer,
                            UDBus::bump()
                        };

                        auto string = UDBus::Type
                        {
                            variantStruct->string,
                            UDBus::bump()
                        };

                        UDBus::Type structure
                        {
                            UDBus::makeStruct(UDBus::Struct{
                                variantStruct->structure.a,
                                variantStruct->structure.b,
                                variantStruct->structure.f
                            }),
                            UDBus::bump()
                        };

                        if (message.handleMessage(integer, &it) == UDBus::RESULT_SUCCESS)
                            variantStruct->type = VARIANT_TYPE_INT;
                        else if (message.handleMessage(string, &it) == UDBus::RESULT_SUCCESS)
                            variantStruct->type = VARIANT_TYPE_STRING;
                        else if (message.handleMessage(structure, &it) == UDBus::RESULT_SUCCESS)
                            variantStruct->type = VARIANT_TYPE_STRUCT;
                        else
                            variantStruct->type = VARIANT_TYPE_NONE;

                        UDBUS_FREE_TYPE(integer);
                        UDBUS_FREE_TYPE(string);
                        UDBUS_FREE_TYPE(structure);

                        *v = variantStruct;
                        return true;
                    }
                }))     
            }),
            s.nestedStructure.f,
            s.nestedStructure.i
        })
    };
    
    while (true)
    {
        (void)connection.read_write(0);
        auto message = connection.pop_message();

        if (!message.is_valid())
            continue;

        if (message.handleMethodCall("org.test.Test", "Test", schema) != UDBus::RESULT_NOT_CALLED)
        {
            printStruct(s);
            UDBUS_FREE_TYPE(schema);
            return 0;
        }

        message.unref();
    }
}

int main()
{
    return deserialise();
}

When you run this application you can test it by sending a message using the following command:

user $ gdbus call --session --dest org.test.Test --object-path /org/test/Test --method org.test.Test.Test --timeout=1 1 true "Hello, World!" "(([1,2,3,4],{'Key 1':<1>,'Key 2':<'Value 2'>,'Key 3':<(1, true, 0.5)>}),0.75,5)"