The library provides 2 ways of parsing a message:
- Manual - using message iterators
- 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::makeStruct(UDBus::Struct
UDBus{
,
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:
- A reference to the current message
- A reference to the current parent iterator
- A pointer to the
data
member of theVariant
struct. You can use this to store any data produced by theparse
callback. - 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:
::Variant
UDBus{
.parse = [](UDBus::Message& message, UDBus::Iterator& it, void** v, void*) -> bool
{
auto* variantStruct = new VariantStructure{};
auto integer = UDBus::Type
{
->integer,
variantStruct::bump()
UDBus};
auto string = UDBus::Type
{
->string,
variantStruct::bump()
UDBus};
::Type structure
UDBus{
::makeStruct(UDBus::Struct{
UDBus->structure.a,
variantStruct->structure.b,
variantStruct->structure.f
variantStruct}),
::bump()
UDBus};
if (message.handleMessage(integer, &it) == UDBus::RESULT_SUCCESS)
->type = VARIANT_TYPE_INT;
variantStructelse if (message.handleMessage(string, &it) == UDBus::RESULT_SUCCESS)
->type = VARIANT_TYPE_STRING;
variantStructelse if (message.handleMessage(structure, &it) == UDBus::RESULT_SUCCESS)
->type = VARIANT_TYPE_STRUCT;
variantStructelse
->type = VARIANT_TYPE_NONE;
variantStruct
(integer);
UDBUS_FREE_TYPE(string);
UDBUS_FREE_TYPE(structure);
UDBUS_FREE_TYPE
*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:
std::vector
std::map
std::unordered_map
However, you also have the option of defining custom containers using the following macros:
UDBUS_USING_CUSTOM_DICT_TYPES
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
{
::associateWithVariant(v, UDBus::makeVariant
UDBus(
::Variant
UDBus{
.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::ignore(),
UDBus,
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::bump(),
UDBus};
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)
{
::handleMethodCall("interface", "method", data);
UDBus}
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>
(Type<T, T2...>& t, UDBus::Iterator* iterator = nullptr) noexcept; MessageGetResult handleMessage
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:
::Iterator it, arrayIt;
UDBus.setGet(reply, arrayIt, true);
it
if (it.get_arg_type() == DBUS_TYPE_ARRAY)
{
.recurse();
it
while (arrayIt.get_arg_type() == DBUS_TYPE_STRUCT)
{
::Iterator structIt;
UDBus.setGet(reply, structIt, false);
arrayIt.recurse();
arrayIt
char* id;
char* userName;
.get_basic(&id);
structIt.next();
structIt.next();
structIt.get_basic(&userName);
structIt
if (username == userName)
{
= id;
sessionID break;
}
.next();
arrayIt}
}
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 structure1double 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 typeunion
{
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()
{
::Connection connection{};
UDBus
::Error error{};
UDBus.bus_get(DBUS_BUS_SESSION, error);
connectionif (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
{
.integer,
s.boolean,
s.string,
s::makeStruct(UDBus::Struct{
UDBus::makeStruct(UDBus::Struct{
UDBus.nestedStructure.structure1.vector,
s::associateWithVariant(s.nestedStructure.structure1.map, UDBus::makeVariant(
UDBus::Variant{
UDBus.parse = [](UDBus::Message& message, UDBus::Iterator& it, void** v, void*) -> bool
{
auto* variantStruct = new VariantStructure{};
auto integer = UDBus::Type
{
->integer,
variantStruct::bump()
UDBus};
auto string = UDBus::Type
{
->string,
variantStruct::bump()
UDBus};
::Type structure
UDBus{
::makeStruct(UDBus::Struct{
UDBus->structure.a,
variantStruct->structure.b,
variantStruct->structure.f
variantStruct}),
::bump()
UDBus};
if (message.handleMessage(integer, &it) == UDBus::RESULT_SUCCESS)
->type = VARIANT_TYPE_INT;
variantStructelse if (message.handleMessage(string, &it) == UDBus::RESULT_SUCCESS)
->type = VARIANT_TYPE_STRING;
variantStructelse if (message.handleMessage(structure, &it) == UDBus::RESULT_SUCCESS)
->type = VARIANT_TYPE_STRUCT;
variantStructelse
->type = VARIANT_TYPE_NONE;
variantStruct
(integer);
UDBUS_FREE_TYPE(string);
UDBUS_FREE_TYPE(structure);
UDBUS_FREE_TYPE
*v = variantStruct;
return true;
}
}))
}),
.nestedStructure.f,
s.nestedStructure.i
s})
};
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)
{
(s);
printStruct(schema);
UDBUS_FREE_TYPEreturn 0;
}
.unref();
message}
}
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)"