Skip to content

Commit

Permalink
Introduce support for asynchronous server-side methods (#12)
Browse files Browse the repository at this point in the history
* Add preliminary changes for async server methods

* Refactor the Message concept and break it into distinctive types

* Continue working on async server methods (high-level API mainly)

* Continue developing support for async server

* Finishing async server methods

* Finishing async server methods (fixing tests & cleaning up)

* A little code cleaning

* Add unit tests for type traits of free functions

* Support for generating async server methods in stub headers

* Update ChangeLog for v0.3.0

* Update the tutorial with how to use async server-side methods

* Update the TOC in sdbus-c++ tutorial

* Update numbering in TOC

* Remove unnecessary code

* Final cleanups
  • Loading branch information
Stanislav Angelovič authored and lukasdurfina committed Jul 2, 2018
1 parent b041f76 commit d8fd053
Show file tree
Hide file tree
Showing 33 changed files with 865 additions and 222 deletions.
8 changes: 8 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@ v0.2.5

v0.2.6
- Fixed memory leak in Message copy operations

v0.3.0
- Introduced support for asynchronous server-side methods
- Refactored the concept of Message into distinctive concrete message types
- As this release comes with breaking API changes:
* if you're using lower-level API, please rename 'Message' to whatever concrete message type (MethodCall, MethodReply, Signal) is needed there,
* if you're using higher-level API, please re-compile and re-link your application against sdbus-c++,
* if you're using generated stub headers, please re-compile and re-link your application against sdbus-c++.
4 changes: 2 additions & 2 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ AC_PREREQ(2.61)
# odd micro numbers indicate in-progress development
# even micro numbers indicate released versions
m4_define(sdbus_cpp_version_major, 0)
m4_define(sdbus_cpp_version_minor, 2)
m4_define(sdbus_cpp_version_micro, 6)
m4_define(sdbus_cpp_version_minor, 3)
m4_define(sdbus_cpp_version_micro, 0)

m4_define([sdbus_cpp_version],
[sdbus_cpp_version_major.sdbus_cpp_version_minor.sdbus_cpp_version_micro])
Expand Down
127 changes: 113 additions & 14 deletions doc/using-sdbus-c++.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ Using sdbus-c++ library
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. [Using D-Bus properties](#using-d-bus-properties)
11. [Conclusion](#conclusion)
10. [Asynchronous server-side methods](#asynchronous-server-side-methods)
11. [Using D-Bus properties](#using-d-bus-properties)
12. [Conclusion](#conclusion)

Introduction
------------
Expand Down Expand Up @@ -92,10 +93,10 @@ The following diagram illustrates the major entities in sdbus-c++.
* invoking remote methods of the corresponding object,
* registering handlers for signals.

`Message` class represents a message, which is the fundamental DBus concept. The message can be
* a method call (with serialized parameters),
* a method reply (with serialized return values),
* or a signal (with serialized parameters).
`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).

Multiple layers of sdbus-c++ API
-------------------------------
Expand Down Expand Up @@ -138,15 +139,15 @@ Overloaded versions of C++ insertion/extraction operators are used for serializa
// to emit signals.
sdbus::IObject* g_concatenator{};

void concatenate(sdbus::Message& msg, sdbus::Message& reply)
void concatenate(sdbus::MethodCall& call, sdbus::MethodReply& reply)
{
// Deserialize the collection of numbers from the message
std::vector<int> numbers;
msg >> numbers;
call >> numbers;

// Deserialize separator from the message
std::string separator;
msg >> separator;
call >> separator;

// Return error if there are no numbers in the collection
if (numbers.empty())
Expand All @@ -163,9 +164,9 @@ void concatenate(sdbus::Message& msg, sdbus::Message& reply)

// Emit 'concatenated' signal
const char* interfaceName = "org.sdbuscpp.Concatenator";
auto signalMsg = g_concatenator->createSignal(interfaceName, "concatenated");
signalMsg << result;
g_concatenator->emitSignal(signalMsg);
auto signal = g_concatenator->createSignal(interfaceName, "concatenated");
signal << result;
g_concatenator->emitSignal(signal);
}

int main(int argc, char *argv[])
Expand Down Expand Up @@ -200,10 +201,10 @@ int main(int argc, char *argv[])
#include <iostream>
#include <unistd.h>
void onConcatenated(sdbus::Message& signalMsg)
void onConcatenated(sdbus::Signal& signal)
{
std::string concatenatedString;
msg >> concatenatedString;
signal >> concatenatedString;
std::cout << "Received signal with concatenated string " << concatenatedString << std::endl;
}
Expand Down Expand Up @@ -666,6 +667,104 @@ int main(int argc, char *argv[])
}
```
Asynchronous server-side methods
--------------------------------
So far in our tutorial, we have only considered simple server methods that are executed in a synchronous way. Sometimes the method call may take longer, however, and we don't want to block (potentially starve) other clients (whose requests may take relative short time). The solution is to execute the D-Bus methods asynchronously. How physically is that done is up to the server design (e.g. thread pool), but sdbus-c++ provides API supporting async methods.
### Lower-level API
Considering the Concatenator example based on lower-level API, if we wanted to write `concatenate` method in an asynchronous way, you only have to adapt method signature and its body (registering the method and all the other stuff stays the same):
```c++
void concatenate(sdbus::MethodCall& call, sdbus::MethodResult result)
{
// Deserialize the collection of numbers from the message
std::vector<int> numbers;
call >> numbers;
// Deserialize separator from the message
std::string separator;
call >> separator;
// Launch a thread for async execution...
std::thread([numbers, separator, result = std::move(result)]()
{
// Return error if there are no numbers in the collection
if (numbers.empty())
{
// This will send the error reply message back to the client
result.returnError("org.sdbuscpp.Concatenator.Error", "No numbers provided");
return;
}
std::string concatenatedStr;
for (auto number : numbers)
{
concatenatedStr += (result.empty() ? std::string() : separator) + std::to_string(number);
}
// This will send the reply message back to the client
result.returnResults(concatenatedStr);
// Note: emitting signals from other than D-Bus dispatcher thread is not supported yet...
/*
// Emit 'concatenated' signal
const char* interfaceName = "org.sdbuscpp.Concatenator";
auto signal = g_concatenator->createSignal(interfaceName, "concatenated");
signal << result;
g_concatenator->emitSignal(signal);
*/
}).detach();
}
```

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.

That's all.

Note: We can use the concept of asynchronous D-Bus methods in both the synchronous and asynchronous way. Whether we return the results directly in the callback in the synchronous way, or we pass the arguments and the result holder to a different thread, and compute and set the results in there, is irrelevant to sdbus-c++. This has the benefit that we can decide at run-time, per each method call, whether we execute it synchronously or (perhaps in case of complex operation) execute it asynchronously to e.g. a thread pool.

### Convenience API

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.

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)`.

`sdbus::Result` class template has effectively the same API as `sdbus::MethodResult` class from above example (it inherits from MethodResult), so you use it in the very same way to send the results or an error back to the client.

Nothing else has to be changed. The registration of the method callback (`implementedAs`) and all the mechanics around remains completely the same.

### Marking async methods in the IDL

sdbus-c++ stub generator can generate stub code for server-side async methods. We just need to annotate the method with the `annotate` element having the "org.freedesktop.DBus.Method.Async" name, like so:

```xml
<?xml version="1.0" encoding="UTF-8"?>

<node name="/org/sdbuscpp/concatenator">
<interface name="org.sdbuscpp.Concatenator">
<method name="concatenate">
<annotation name="org.freedesktop.DBus.Method.Async" value="server" />
<arg type="ai" name="numbers" direction="in" />
<arg type="s" name="separator" direction="in" />
<arg type="s" name="concatenatedString" direction="out" />
</method>
<signal name="concatenated">
<arg type="s" name="concatenatedString" />
</signal>
</interface>
</node>
```

Using D-Bus properties
----------------------

Expand Down
1 change: 1 addition & 0 deletions include/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ libsdbuscpp_HEADERS = \
$(HEADER_DIR)/IObject.h \
$(HEADER_DIR)/IObjectProxy.h \
$(HEADER_DIR)/Message.h \
$(HEADER_DIR)/MethodResult.h \
$(HEADER_DIR)/Types.h \
$(HEADER_DIR)/TypeTraits.h \
$(HEADER_DIR)/sdbus-c++.h
Expand Down
11 changes: 8 additions & 3 deletions include/sdbus-c++/ConvenienceClasses.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
#define SDBUS_CXX_CONVENIENCECLASSES_H_

#include <sdbus-c++/Message.h>
#include <sdbus-c++/TypeTraits.h>
#include <string>
#include <type_traits>

// Forward declarations
namespace sdbus {
Expand All @@ -43,7 +45,10 @@ namespace sdbus {
public:
MethodRegistrator(IObject& object, const std::string& methodName);
MethodRegistrator& onInterface(const std::string& interfaceName);
template <typename _Function> void implementedAs(_Function&& callback);
template <typename _Function>
std::enable_if_t<!is_async_method_v<_Function>> implementedAs(_Function&& callback);
template <typename _Function>
std::enable_if_t<is_async_method_v<_Function>> implementedAs(_Function&& callback);

private:
IObject& object_;
Expand Down Expand Up @@ -103,7 +108,7 @@ namespace sdbus {
private:
IObject& object_;
const std::string& signalName_;
Message signal_;
Signal signal_;
int exceptions_{}; // Number of active exceptions when SignalEmitter is constructed
};

Expand All @@ -122,7 +127,7 @@ namespace sdbus {
private:
IObjectProxy& objectProxy_;
const std::string& methodName_;
Message method_;
MethodCall method_;
int exceptions_{}; // Number of active exceptions when MethodInvoker is constructed
bool methodCalled_{};
};
Expand Down
30 changes: 27 additions & 3 deletions include/sdbus-c++/ConvenienceClasses.inl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <sdbus-c++/IObject.h>
#include <sdbus-c++/IObjectProxy.h>
#include <sdbus-c++/Message.h>
#include <sdbus-c++/MethodResult.h>
#include <sdbus-c++/Types.h>
#include <sdbus-c++/TypeTraits.h>
#include <sdbus-c++/Error.h>
Expand All @@ -52,15 +53,15 @@ namespace sdbus {
}

template <typename _Function>
inline void MethodRegistrator::implementedAs(_Function&& callback)
inline std::enable_if_t<!is_async_method_v<_Function>> MethodRegistrator::implementedAs(_Function&& callback)
{
SDBUS_THROW_ERROR_IF(interfaceName_.empty(), "DBus interface not specified when registering a DBus method", EINVAL);

object_.registerMethod( interfaceName_
, methodName_
, signature_of_function_input_arguments<_Function>::str()
, signature_of_function_output_arguments<_Function>::str()
, [callback = std::forward<_Function>(callback)](Message& msg, Message& reply)
, [callback = std::forward<_Function>(callback)](MethodCall& msg, MethodReply& reply)
{
// Create a tuple of callback input arguments' types, which will be used
// as a storage for the argument values deserialized from the message.
Expand All @@ -80,6 +81,29 @@ namespace sdbus {
});
}

template <typename _Function>
inline std::enable_if_t<is_async_method_v<_Function>> MethodRegistrator::implementedAs(_Function&& callback)
{
SDBUS_THROW_ERROR_IF(interfaceName_.empty(), "DBus interface not specified when registering a DBus method", EINVAL);

object_.registerMethod( interfaceName_
, methodName_
, signature_of_function_input_arguments<_Function>::str()
, signature_of_function_output_arguments<_Function>::str() //signature_of<last_function_argument_t<_Function>>::str() // because last argument contains output types
, [callback = std::forward<_Function>(callback)](MethodCall& msg, MethodResult result)
{
// Create a tuple of callback input arguments' types, which will be used
// as a storage for the argument values deserialized from the message.
tuple_of_function_input_arg_types_t<_Function> inputArgs;

// Deserialize input arguments from the message into the tuple,
// plus store the result object as a last item of the tuple.
msg >> inputArgs;

// Invoke callback with input arguments from the tuple.
apply(callback, std::move(result), inputArgs); // TODO: Use std::apply when switching to full C++17 support
});
}

// Moved into the library to isolate from C++17 dependency
/*
Expand Down Expand Up @@ -339,7 +363,7 @@ namespace sdbus {

objectProxy_.registerSignalHandler( interfaceName_
, signalName_
, [callback = std::forward<_Function>(callback)](Message& signal)
, [callback = std::forward<_Function>(callback)](Signal& signal)
{
// Create a tuple of callback input arguments' types, which will be used
// as a storage for the argument values deserialized from the signal message.
Expand Down
28 changes: 25 additions & 3 deletions include/sdbus-c++/IObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

// Forward declarations
namespace sdbus {
class Message;
class Signal;
class IConnection;
}

Expand Down Expand Up @@ -70,6 +70,28 @@ namespace sdbus {
, const std::string& outputSignature
, method_callback methodCallback ) = 0;

/*!
* @brief Registers method that the object will provide on D-Bus
*
* @param[in] interfaceName Name of an interface that the method will belong to
* @param[in] methodName Name of the method
* @param[in] inputSignature D-Bus signature of method input parameters
* @param[in] outputSignature D-Bus signature of method output parameters
* @param[in] asyncMethodCallback Callback that implements the body of the method
*
* This overload register a method callback that will have a freedom to execute
* its body in asynchronous contexts, and send the results from those contexts.
* This can help in e.g. long operations, which then don't block the D-Bus processing
* loop thread.
*
* @throws sdbus::Error in case of failure
*/
virtual void registerMethod( const std::string& interfaceName
, const std::string& methodName
, const std::string& inputSignature
, const std::string& outputSignature
, async_method_callback asyncMethodCallback ) = 0;

/*!
* @brief Registers signal that the object will emit on D-Bus
*
Expand Down Expand Up @@ -138,7 +160,7 @@ namespace sdbus {
*
* @throws sdbus::Error in case of failure
*/
virtual Message createSignal(const std::string& interfaceName, const std::string& signalName) = 0;
virtual Signal createSignal(const std::string& interfaceName, const std::string& signalName) = 0;

/*!
* @brief Emits signal on D-Bus
Expand All @@ -149,7 +171,7 @@ namespace sdbus {
*
* @throws sdbus::Error in case of failure
*/
virtual void emitSignal(const sdbus::Message& message) = 0;
virtual void emitSignal(const sdbus::Signal& message) = 0;

/*!
* @brief Registers method that the object will provide on D-Bus
Expand Down
7 changes: 4 additions & 3 deletions include/sdbus-c++/IObjectProxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@

// Forward declarations
namespace sdbus {
class Message;
class MethodCall;
class MethodReply;
class IConnection;
}

Expand Down Expand Up @@ -65,7 +66,7 @@ namespace sdbus {
*
* @throws sdbus::Error in case of failure
*/
virtual Message createMethodCall(const std::string& interfaceName, const std::string& methodName) = 0;
virtual MethodCall createMethodCall(const std::string& interfaceName, const std::string& methodName) = 0;

/*!
* @brief Calls method on the proxied D-Bus object
Expand All @@ -76,7 +77,7 @@ namespace sdbus {
*
* @throws sdbus::Error in case of failure
*/
virtual Message callMethod(const sdbus::Message& message) = 0;
virtual MethodReply callMethod(const sdbus::MethodCall& message) = 0;

/*!
* @brief Registers a handler for the desired signal emitted by the proxied D-Bus object
Expand Down
Loading

0 comments on commit d8fd053

Please sign in to comment.