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(UDBus::Iterator&, void*)> parse = [](UDBus::Iterator&, void*) -> bool { return false; };
};
The parse
callback receives references to the parent
iterator and a void*
user pointer. It should return
true
on success and false
on failure.
To set the user pointer, use call the
UDBus::Message::setUserPointer()
method.
Inside the callback, you need to manually parse the variant argument. More information on how to do that 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{
.f = [](UDBus::Iterator& it, void* v) -> 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.
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.
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
.