Revise and update the tutorial for redesigned sdbus-c++ parts

This commit is contained in:
Stanislav Angelovic
2019-03-27 17:34:41 +01:00
parent cbf2218301
commit d489eee9c0

View File

@ -7,25 +7,23 @@ Using sdbus-c++ library
2. [Integrating sdbus-c++ into your project](#integrating-sdbus-c-into-your-project)
3. [Header files and namespaces](#header-files-and-namespaces)
4. [Error signalling and propagation](#error-signalling-and-propagation)
5. [Multiple layers of sdbus-c++ API](#multiple-layers-of-sdbus-c-api)
6. [An example: Number concatenator](#an-example-number-concatenator)
7. [Implementing the Concatenator example using basic sdbus-c++ API layer](#implementing-the-concatenator-example-using-basic-sdbus-c-api-layer)
8. [Implementing the Concatenator example using convenience sdbus-c++ API layer](#implementing-the-concatenator-example-using-convenience-sdbus-c-api-layer)
9. [Implementing the Concatenator example using sdbus-c++-generated stubs](#implementing-the-concatenator-example-using-sdbus-c-generated-stubs)
10. [Asynchronous server-side methods](#asynchronous-server-side-methods)
11. [Asynchronous client-side methods](#asynchronous-client-side-methods)
12. [Using D-Bus properties](#using-d-bus-properties)
13. [Conclusion](#conclusion)
5. [Design of sdbus-c++](#design-of-sdbus-c)
6. [Multiple layers of sdbus-c++ API](#multiple-layers-of-sdbus-c-api)
7. [An example: Number concatenator](#an-example-number-concatenator)
8. [Implementing the Concatenator example using basic sdbus-c++ API layer](#implementing-the-concatenator-example-using-basic-sdbus-c-api-layer)
9. [Implementing the Concatenator example using convenience sdbus-c++ API layer](#implementing-the-concatenator-example-using-convenience-sdbus-c-api-layer)
10. [Implementing the Concatenator example using sdbus-c++-generated stubs](#implementing-the-concatenator-example-using-sdbus-c-generated-stubs)
11. [Asynchronous server-side methods](#asynchronous-server-side-methods)
12. [Asynchronous client-side methods](#asynchronous-client-side-methods)
13. [Using D-Bus properties](#using-d-bus-properties)
14. [Conclusion](#conclusion)
Introduction
------------
sdbus-c++ is a C++ wrapper library built on top of [sd-bus](http://0pointer.net/blog/the-new-sd-bus-api-of-systemd.html), a lightweight D-Bus client
library implemented within systemd project. It provides D-Bus functionality on a higher level of abstraction, trying to employ C++ type system
to shift as much work as possible from the developer to the compiler.
sdbus-c++ is a C++ wrapper library built on top of [sd-bus](http://0pointer.net/blog/the-new-sd-bus-api-of-systemd.html), a lightweight D-Bus client library implemented within systemd project. It provides D-Bus functionality on a higher level of abstraction, trying to employ C++ type system to shift as much work as possible from the developer to the compiler.
sdbus-c++ does not cover the entire sd-bus API, but provides tools for implementing the most common functionality - RPC
method calls, signals and properties. There is room for additions and improvements, as needed and when needed.
sdbus-c++ does not cover the entire sd-bus API, but provides tools for implementing the most common functionality - RPC method calls, signals and properties. There is room for additions and improvements, as needed and when needed.
Integrating sdbus-c++ into your project
---------------------------------------
@ -50,8 +48,7 @@ Note: sdbus-c++ library depends on C++17, since it uses C++17 `std::uncaught_exc
Header files and namespaces
---------------------------
All sdbus-c++ header files reside in the `sdbus-c++` subdirectory within the standard include directory. Users can either include
individual header files, like so:
All sdbus-c++ header files reside in the `sdbus-c++` subdirectory within the standard include directory. Users can either include individual header files, like so:
```cpp
#include <sdbus-c++/IConnection.h>
@ -70,37 +67,51 @@ Error signalling and propagation
--------------------------------
`sdbus::Error` exception is used to signal errors in sdbus-c++. There are two types of errors:
* D-Bus related errors, like call timeouts, failed socket allocation, etc.
* user errors, i.e. errors signalled and propagated from remote methods back to the caller.
The exception object carries the error name and error message with it.
* D-Bus related errors, like call timeouts, failed socket allocation, etc. These are raised by the D-Bus library or D-Bus daemon itself.
* user-defined errors, i.e. errors signalled and propagated from remote methods back to the caller. So these are issued by sdbus-c++ users.
sdbus-c++ design
----------------
`sdbus::Error` is a carrier for both types of errors, carrying the error name and error message with it.
Design of sdbus-c++
-------------------
The following diagram illustrates the major entities in sdbus-c++.
![class](sdbus-c++-class-diagram.png)
`IConnection` represents the concept of a D-Bus connection. You can connect to either the system bus or a session bus. Services can assign unique service names to those connections. A processing loop can be run on the connection.
`IConnection` represents the concept of a D-Bus connection. You can connect to either the system bus or a session bus. Services can assign unique service names to those connections. A processing loop should be run on the connection.
`IObject` represents the concept of an object that exposes its methods, signals and properties. Its responsibilities are:
* registering (possibly multiple) interfaces and methods, signals, properties on those interfaces,
* emitting signals.
* registering (possibly multiple) interfaces and methods, signals, properties on those interfaces,
* emitting signals.
`IObjectProxy` represents the concept of the proxy, which is a view of the `Object` from the client side. Its responsibilities are:
* invoking remote methods of the corresponding object,
* registering handlers for signals.
* invoking remote methods of the corresponding object, in both synchronous and asynchronous way,
* registering handlers for signals,
`Message` class represents a message, which is the fundamental DBus concept. There are three distinctive types of message that derive from the `Message` class:
* `MethodCall` (with serialized parameters),
* `MethodReply` (with serialized return values),
* or a `Signal` (with serialized parameters).
* `MethodCall` (with serialized parameters),
* `MethodReply` (with serialized return values),
* or a `Signal` (with serialized parameters).
### Thread safety in sdbus-c++
sdbus-c++ is thread-aware by design. But, in general, it's not thread-safe. At least not in all places. There are situations where sdbus-c++ provides and guarantees API-level thread safety by design. It is safe to do these operations from multiple threads at the same time:
* Making and destroying `Object`s and `ObjectProxy`s, even on a shared connection that is already running an event loop. Under *making* here is meant a complete atomic sequence of construction, registration of method/signal/property callbacks and export of the `Object`/`ObjectProxy` so it is ready to issue/receive messages. This sequence must be done in one thread.
* Creating and sending asynchronous method replies on an `Object` instance.
* Creating and emitting signals on an `Object` instance.
* Creating and sending method calls (both synchronously and asynchronously) on an `ObjectProxy` instance. (But it's generally better that our threads use their own exclusive instances of object proxy, to minimize shared state and contention.)
Multiple layers of sdbus-c++ API
-------------------------------
sdbus-c++ API comes in two layers:
* [the basic layer](#implementing-the-concatenator-example-using-basic-sdbus-c-api-layer), which is a simple wrapper layer on top of sd-bus, using mechanisms that are native to C++ (e.g. serialization/deserialization of data from messages),
* [the convenience layer](#implementing-the-concatenator-example-using-convenience-sdbus-c-api-layer), building on top of the basic layer, which aims at alleviating users from unnecessary details and enables them to write shorter, safer, and more expressive code.
@ -110,21 +121,22 @@ An example: Number concatenator
-------------------------------
Let's have an object `/org/sdbuscpp/concatenator` that implements the `org.sdbuscpp.concatenator` interface. The interface exposes the following:
* a `concatenate` method that takes a collection of integers and a separator string and returns a string that is the concatenation of all
integers from the collection using given separator,
* a `concatenated` signal that is emitted at the end of each successful concatenation.
* a `concatenate` method that takes a collection of integers and a separator string and returns a string that is the concatenation of all integers from the collection using given separator,
* a `concatenated` signal that is emitted at the end of each successful concatenation.
In the following sections, we will elaborate on the ways of implementing such an object on both the server and the client side.
Implementing the Concatenator example using basic sdbus-c++ API layer
---------------------------------------------------------------------
In the basic API layer, we already have abstractions for D-Bus connections, objects and object proxies, with which we can interact via
interfaces. However, we still work with the concept of messages. To issue a method call for example, we have to go through several steps:
we have to create a method call message first, serialize method arguments into the message, and send the message at last. We get the reply
message (if applicable) in return, so we have to deserialize the return values from it manually.
In the basic API layer, we already have abstractions for D-Bus connections, objects and object proxies, with which we can interact via their interface classes (`IConnection`, `IObject`, `IObjectProxy`), but, analogously to the underlying sd-bus C library, we still work on the level of D-Bus messages. We need to
Overloaded versions of C++ insertion/extraction operators are used for serialization/deserialization. That makes the client code much simpler.
* create them,
* serialize/deserialize arguments to/from them (thanks to many overloads of C++ insertion/extraction operators, this is very simple),
* send them over to the other side.
This is how a simple Concatenator service implemented upon the basic sdbus-c++ API could look like:
### Server side
@ -191,6 +203,12 @@ int main(int argc, char *argv[])
}
```
We establish a D-Bus sytem connection and request `org.sdbuscpp.concatenator` D-Bus name on it. This name will be used by D-Bus clients to find the service. We then create an object with path `/org/sdbuscpp/concatenator` on this connection. We register interfaces, its methods, signals that the object provides, and, through `finishRegistration()`, export the object (i.e., make it visible to clients) on the bus. Then we need to make sure to run the event loop upon the connection, which handles all incoming, outgoing and other requests.
The callback for any D-Bus object method on this level is any callable of signature `void(sdbus::MethodCall& call, sdbus::MethodReply& reply)`. The first parameter `call` is the incoming method call message. We need to deserialize input arguments from it. When we can invoke the logic and get the results. Then we serialize the results to the second parameter, the pre-constructed `reply` message. The reply is then sent automatically by sdbus-c++. We also fire a signal with the results. To do this, we need to create a signal message via object's `createSignal()`, serialize the results into it, and then send it out to subscribers by invoking object's `emitSignal()`.
Please note that we can create and destroy D-Bus objects on a connection dynamically, at any time during runtime, even while there is an active event loop upon the connection. So managing D-Bus objects' lifecycle (creating, exporting and destroying D-Bus objects) is completely thread-safe.
### Client side
```c++
@ -257,33 +275,57 @@ int main(int argc, char *argv[])
}
```
### Proxy and D-Bus connection
In simple cases, we don't need to create D-Bus connection explicitly for our proxies. Unless a connection is provided to an object proxy explicitly via factory parameter, the proxy will create a connection of his own, and it will be a system bus connection. This is the case in the example above. (This approach is not scalable and resource-saving if we have plenty of proxies; see section [Working with D-Bus connections](#working-with-d-bus-connections-in-sdbus-c) for elaboration.) So, in the example, we create a proxy for object `/org/sdbuscpp/concatenator` publicly available at bus `org.sdbuscpp.concatenator`. We register signal handlers, if any, and finish the registration, making the object proxy ready for use.
There are three ways of creating the object proxy -- three overloads of `sdbus::createObjectProxy`. They differ from each other as to how the proxy towards the connection will behave upon creation:
The callback for a D-Bus signal handler on this level is any callable of signature `void(sdbus::Signal& signal)`. The one and only parameter `signal` is the incoming signal message. We need to deserialize arguments from it, and then we can do our business logic with it.
* One that takes no connection as a parameter. This one is for convenience -- if you have a simple application and don't want to bother with connections, call this one. Internally, it will create a connection object, and it will be a *system* bus connection. The proxy will immediately create an internal thread and start a processing loop upon the clone of this connection in this thread as long as there is at least one signal registered, so the signals are correctly received and the callbacks are handled from within this internal thread. If there is no signal, i.e. the proxy just provides methods and/or properties, no connection clone is made, no thread is created and no processing loop is started -- you don't pay for what you don't use.
Subsequently, we invoke two RPC calls to object's `concatenate()` method. We create a method call message by invoking proxy's `createMethodCall()`. We serialize method input arguments into it, and make a synchronous call via proxy's `callMethod()`. As a return value we get the reply message as soon as it arrives. We deserialize return values from that message, and further use it in our program. The second `concatenate()` RPC call is done with invalid arguments, so we get a D-Bus error reply from the service, which as we can see is manifested via `sdbus::Error` exception being thrown.
* One that takes the connection as an **rvalue unique_ptr**. This one behaves the same as the above one, just that you must create the connection by yourself, and then `std::move` the ownership of it to the proxy. This comes with a flexibility that you can choose connection type (system, session).
Please note that we can create and destroy D-Bus object proxies dynamically, at any time during runtime, even when they share a common D-Bus connection and there is an active event loop upon the connection. So managing D-Bus object proxies' lifecycle (creating and destroying D-Bus object proxies) is completely thread-safe.
* One that takes the connection as an **lvalue reference**. This one behaves differently. You as a client are the owner of the connection, you take full control of it. The proxy just references the connection. This means the proxy does no async processing on it even when there are signals. It relies on you to manage the processing loop yourself (if you need it for signals).
### Working with D-Bus connections in sdbus-c++
The design of D-Bus connections in sdbus-c++ allows for certain flexibility and enables users to choose simplicity over scalability or scalability (at a finer granularity of user's choice) at the cost of slightly decreased simplicity.
How shall we use connections in relation to D-Bus objects and object proxies?
A D-Bus connection is represented by a `Connection` instance. Each connection needs an event loop being run upon it. So it needs a thread handling the event loop. This thread serves all incoming and outgoing messages and all communication towards D-Bus daemon.
One process can have multiple D-Bus connections, with assigned unique bus names or without, as long as those with assigned bus names do not share a common bus name.
A D-Bus connection can be created for and used exclusively by one D-Bus object (represented by one `Object` instance) or D-Bus object proxy (represented by one `ObjectProxy` instance), but can very well be used by and shared across multiple objects, multiple proxies or even both multiple objects and proxies at the same time. When shared, one must bear in mind that the access to the connection is mutually exclusive and is serialized. This means, for example, that if an object's callback is going to be invoked for an incoming remote method call and in another thread we use a proxy to call remote method in another process, the threads are contending and only one can go on while the other must wait and can only proceed after the first one has finished, because both are using a shared resource -- the connection.
The former case (1:1) is one extreme; it's usually simple, has zero resource contention, but hurts scalability (for example, 50 proxies in our program need 50 D-Bus connections and 50 event loop threads upon them). The latter case (1:N) is the other extreme -- all D-Bus objects and proxies share one single connection. This is the most scalable solution (since, for example, 5 or 200 objects/proxies use always one single connection), but may increase contention and hurt concurrency (since only one of all those objects/proxies can work with the connection at a time). And then there are limitless options between the two (for example, we can use one connection for all objects, and another connection for all proxies in our service...). sdbus-c++ gives its users freedom to choose whatever approach is more suitable to them in their application at fine granularity.
How can we use connections from the server and the client perspective?
* On the *server* side, we generally need to create D-Bus objects and publish their APIs. For that we first need a connection with a unique bus name. We need to create the D-Bus connection manually ourselves, request bus name on it, and manually launch its event loop (in a blocking way, through `enterProcessingLoop()`, or non-blocking async way, through `enterProcessingLoopAsync()`). At any time before or after running the event loop on the connection, we can create and "hook", as well as remove, objects and proxies upon that connection.
* On the *client* side, for our D-Bus object proxies, we have more options (corresponding to three overloads of the `createObjectProxy()` factory):
* We don't bother about any connection when creating a proxy. For each proxy instance sdbus-c++ also creates an internal connection instance to be used just by this proxy, and it will be a *system bus* connection. Additionally, an event loop thread for that connection is created and run internally.
This hurts scalability (see discussion above), but our code is simpler, and since each proxy has its own connection, there is zero contention.
* We create a connection explicitly by ourselves and `std::move` it to the object proxy factory. The proxy becomes an owner of this connection, and will run the event loop on that connection. This is the same as in the above bullet point, but with a flexibility that we can choose the bus type (system, session bus).
* We are always full owners of the connection. We create the connection, and a proxy only takes and keeps a reference to it. We take care of the event loop upon that connection (and we must ensure the connection exists as long as all its users exist).
Implementing the Concatenator example using convenience sdbus-c++ API layer
---------------------------------------------------------------------------
One of the major sdbus-c++ design goals is to make the sdbus-c++ API easy to use correctly, and hard to use incorrectly.
The convenience API layer abstracts the concept of underlying D-Bus messages away completely. It abstracts away D-Bus signatures. And it tries
to provide an interface that uses small, focused functions, with one or zero parameters, to form a chained function statement that reads like
a sentence to a human reading the code. To achieve that, sdbus-c++ utilizes the power of the C++ type system, so many issues are resolved at
compile time, and the run-time performance cost compared to the basic layer is close to zero.
The convenience API layer abstracts the concept of underlying D-Bus messages away completely. It abstracts away D-Bus signatures. The interface uses small, focused functions, with a few parameters only, to form a chained function statement that reads like a human language sentence. To achieve that, sdbus-c++ utilizes the power of the C++ type system, which deduces and resolves a lot of things at compile time, and the run-time performance cost compared to the basic layer is close to zero.
Thus, in the end of the day, the code written using the convenience API is:
- more expressive,
- closer to the abstraction level of the problem being solved,
- shorter,
- almost as fast (if not equally fast) as one written using the basic API layer.
Rather than *how*, the code written using this layer expresses *what* it does. Let's look at code samples to see if you agree :)
- more expressive,
- closer to the abstraction level of the problem being solved,
- shorter,
- almost as fast as one written using the basic API layer.
The code written using this layer expresses *what* it does, rather then *how*. Let's look at code samples.
### Server side
@ -395,24 +437,22 @@ int main(int argc, char *argv[])
}
```
Several lines of code have shrunk into one-liners when registering/calling methods or signals. D-Bus signatures and the serialization/deserialization
of arguments from the messages is generated at compile time, by introspecting signatures of provided callbacks or deducing types of provided arguments.
When registering methods, calling methods or emitting signals, multiple lines of code have shrunk into simple one-liners. Signatures of provided callbacks are introspected and types of provided arguments are deduced at compile time, so the D-Bus signatures as well as serialization and deserialization of arguments to and from D-Bus messages are generated for us completely by the compiler.
sdbus-c++ users shall prefer the convenience API to the lower level, basic API. When feasible, using generated adaptor and proxy stubs is even better. These stubs provide yet another, higher API level built on top of the convenience API. They are described in the following section.
Implementing the Concatenator example using sdbus-c++-generated stubs
---------------------------------------------------------------------
sdbus-c++ ships with the native stub generator tool called sdbuscpp-xml2cpp. The tool is very similar to dbusxx-xml2cpp tool that comes from
dbus-c++ project.
sdbus-c++ ships with the native stub generator tool called sdbuscpp-xml2cpp. The tool is very similar to dbusxx-xml2cpp tool that comes from dbus-c++ project.
The generator tool takes D-Bus XML IDL description of D-Bus interfaces on its input, and can be instructed to generate one or both of these:
an adaptor header file for use at server side, and a proxy header file for use at client side. Like this:
The generator tool takes D-Bus XML IDL description of D-Bus interfaces on its input, and can be instructed to generate one or both of these: an adaptor header file for use at server side, and a proxy header file for use at client side. Like this:
```bash
sdbuscpp-xml2cpp database-bindings.xml --adaptor=database-server-glue.h --proxy=database-client-glue.h
```
The adaptor header file contains classes that can be used to implement described interfaces. The proxy header file contains classes that can be used
to make calls to remote objects.
The adaptor header file contains classes that can be used to implement described interfaces. The proxy header file contains classes that can be used to make calls to remote objects.
### XML description of the Concatenator interface
@ -439,9 +479,7 @@ After running this through the stubs generator, we get the stub code that is des
### concatenator-server-glue.h
There is specific class for each interface in the XML IDL file. The class is de facto an interface which shall be implemented by inheriting from it.
The class' constructor takes care of registering all methods, signals and properties. For each D-Bus method there is a pure virtual member function.
These pure virtual functions must be implemented in the child class. For each signal, there is a public function member that emits this signal.
There is specific class for each interface in the XML IDL file. The class is de facto an interface which shall be implemented by inheriting from it. The class' constructor takes care of registering all methods, signals and properties. For each D-Bus method there is a pure virtual member function. These pure virtual functions must be implemented in the child class. For each signal, there is a public function member that emits this signal.
```cpp
/*
@ -491,9 +529,7 @@ private:
### concatenator-client-glue.h
Analogously to the adaptor classes described above, there is specific class for each interface in the XML IDL file. The class is de facto a proxy
to the concrete interface of a remote object. For each D-Bus signal there is a pure virtual member function whose body must be provided in a child
class. For each method, there is a public function member that calls the method remotely.
Analogously to the adaptor classes described above, there is specific class for each interface in the XML IDL file. The class is de facto a proxy to the concrete interface of a remote object. For each D-Bus signal there is a pure virtual member function whose body must be provided in a child class. For each method, there is a public function member that calls the method remotely.
```cpp
/*
@ -543,11 +579,7 @@ private:
### Providing server implementation based on generated adaptors
To implement a D-Bus object that implements all its D-Bus interfaces, we shall create a class representing the object that inherits from all
corresponding `*_adaptor` classes and implements all pure virtual member functions. Specifically, the object class shall inherit from the
`Interfaces` template class, the template arguments of which are individual adaptor classes. The `Interfaces` is just a convenience class that
hides a few boiler-plate details. For example, in its constructor, it creates an `Object` instance, it takes care of proper initialization of
all adaptor superclasses, and exports the object finally.
To implement a D-Bus object that implements all its D-Bus interfaces, we shall create a class representing the object that inherits from all corresponding `*_adaptor` classes and implements all pure virtual member functions. Specifically, the object class shall inherit from the `Interfaces` template class, the template arguments of which are individual adaptor classes. The `Interfaces` is just a convenience class that hides a few boiler-plate details. For example, in its constructor, it creates an `Object` instance, it takes care of proper initialization of all adaptor superclasses, and exports the object finally.
```cpp
#include <sdbus-c++/sdbus-c++.h>
@ -584,8 +616,7 @@ protected:
};
```
That's it. We now have an implementation of a D-Bus object implementing `org.sdbuscpp.Concatenator` interface. Let's now create a service
publishing the object.
That's it. We now have an implementation of a D-Bus object implementing `org.sdbuscpp.Concatenator` interface. Let's now create a service publishing the object.
```cpp
#include "Concatenator.h"
@ -605,15 +636,11 @@ int main(int argc, char *argv[])
}
```
It's that simple!
It's that simple :)
### Providing client implementation based on generated proxies
To implement a proxy for a remote D-Bus object, we shall create a class representing the object proxy that inherits from all corresponding
`*_proxy` classes and -- if applicable -- implements all pure virtual member functions. Specifically, the object proxy class shall inherit
from the `ProxyInterfaces` template class. As its template arguments we shall provide all proxy classes. The `ProxyInterfaces` is just a
convenience class that hides a few boiler-plate details. For example, in its constructor, it creates an `ObjectProxy` instance, and it takes
care of proper initialization of all proxy superclasses.
To implement a proxy for a remote D-Bus object, we shall create a class representing the object proxy that inherits from all corresponding `*_proxy` classes and -- if applicable -- implements all pure virtual member functions. Specifically, the object proxy class shall inherit from the `ProxyInterfaces` template class. As its template arguments we shall provide all proxy classes. The `ProxyInterfaces` is just a convenience class that hides a few boiler-plate details. For example, in its constructor, it creates an `ObjectProxy` instance, and it takes care of proper initialization of all proxy superclasses.
```cpp
#include <sdbus-c++/sdbus-c++.h>
@ -731,9 +758,11 @@ void concatenate(sdbus::MethodCall& call, sdbus::MethodResult result)
Notice these differences as compared to the synchronous version:
* Instead of `MethodReply` message given by reference, there is `MethodResult` as a second parameter of the callback, which will hold method results and can be written to from any thread.
* You shall pass the result holder (`MethodResult` instance) by moving it to the thread of execution, and eventually write method results (or method error) to it via its `returnResults()` or `returnError()` method, respectively.
* Unlike in sync methods, reporting errors cannot be done by throwing sdbus::Error, since the execution takes place out of context of the D-Bus dispatcher thread. Instead, just pass the error name and message to the `returnError` method of the result holder.
* Instead of `MethodReply` message given by reference, there is `MethodResult` as a second parameter of the callback, which will hold method results and can be written to from any thread.
* You shall pass the result holder (`MethodResult` instance) by moving it to the thread of execution, and eventually write method results (or method error) to it via its `returnResults()` or `returnError()` method, respectively.
* Unlike in sync methods, reporting errors cannot be done by throwing sdbus::Error, since the execution takes place out of context of the D-Bus dispatcher thread. Instead, just pass the error name and message to the `returnError` method of the result holder.
That's all.
@ -741,9 +770,9 @@ That's all.
Method callbacks in convenience sdbus-c++ API also need to take the result object as a parameter. The requirements are:
* The result holder is of type `sdbus::Result<Types...>`, where `Types...` is a list of method output argument types.
* The result object must be a first physical parameter of the callback taken by value.
* The callback itself is physically a void-returning function.
* The result holder is of type `sdbus::Result<Types...>`, where `Types...` is a list of method output argument types.
* The result object must be a first physical parameter of the callback taken by value.
* The callback itself is physically a void-returning function.
For example, we would have to change the concatenate callback signature from `std::string concatenate(const std::vector<int32_t>& numbers, const std::string& separator)` to `void concatenate(sdbus::Result<std::string> result, const std::vector<int32_t>& numbers, const std::string& separator)`.