Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ tests/run-test-on-device.sh
.settings
*.log

# vscode
.vscode/

#autotools
sdbus-cpp.pc
*Makefile
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ set(SDBUSCPP_HDR_SRCS
${SDBUSCPP_SOURCE_DIR}/ISdBus.h)

set(SDBUSCPP_PUBLIC_HDRS
${SDBUSCPP_INCLUDE_DIR}/Awaitable.h
${SDBUSCPP_INCLUDE_DIR}/ConvenienceApiClasses.h
${SDBUSCPP_INCLUDE_DIR}/ConvenienceApiClasses.inl
${SDBUSCPP_INCLUDE_DIR}/VTableItems.h
Expand Down
73 changes: 63 additions & 10 deletions docs/using-sdbus-c++.md
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@ private:

Analogously to the adaptor classes described above, there is one proxy class generated for one interface in the XML IDL file. The class is de facto a proxy to the concrete single 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.

Generated proxy classes are not copyable and not moveable by design. One can create them on the heap and manage them in e.g. a `std::unique_ptr` if move semantics is needed (for example, when they are stored in a container).
Generated proxy classes are not copyable and not moveable by design. One can create them on the heap and manage them in e.g. a `std::unique_ptr` if move semantics is needed (for example, when they are stored in a container).

```cpp
/*
Expand Down Expand Up @@ -1130,11 +1130,11 @@ For a real example of a server-side asynchronous D-Bus method, please look at sd
Asynchronous client-side methods
--------------------------------

sdbus-c++ also supports asynchronous approach at the client (the proxy) side. With this approach, we can issue a D-Bus method call without blocking current thread's execution while waiting for the reply. We go on doing other things, and when the reply comes, either a given callback handler will be invoked within the context of the event loop thread, or a future object returned by the async call will be set the returned value.6
sdbus-c++ also supports asynchronous approach at the client (the proxy) side. With this approach, we can issue a D-Bus method call without blocking current thread's execution while waiting for the reply. We go on doing other things, and when the reply comes, either a given callback handler will be invoked within the context of the event loop thread, a future object returned by the async call will be set the returned value, or (with C++20) an awaitable can be `co_await`ed in a coroutine.

### Lower-level API

Considering the Concatenator example based on lower-level API, if we wanted to call `concatenate` in an async way, we have two options: We either pass a callback to the proxy when issuing the call, and that callback gets invoked when the reply arrives:
Considering the Concatenator example based on lower-level API, if we wanted to call `concatenate` in an async way, we have several options. We can pass a callback to the proxy when issuing the call, and that callback gets invoked when the reply arrives:

```c++
int main(int argc, char *argv[])
Expand Down Expand Up @@ -1204,6 +1204,18 @@ Another option is to use `std::future`-based overload of the `IProxy::callMethod
}
```

A third option, available with C++20, is to use `sdbus::with_awaitable` to get an `Awaitable<MethodReply>` that can be `co_await`ed in a coroutine:

```c++
// In a coroutine context:
auto method = concatenatorProxy->createMethodCall(interfaceName, concatenate);
method << numbers << separator;
auto reply = co_await concatenatorProxy->callMethod(method, sdbus::with_awaitable);
std::string result;
reply >> result;
// If an error occurs, sdbus::Error is thrown when co_await completes
```

### Convenience API

On the convenience API level, the call statement starts with `callMethodAsync()`, and one option is to finish the statement with `uponReplyInvoke()` that takes a callback handler. The callback is a void-returning function that takes at least one argument: `std::optional<sdbus::Error>`. All subsequent arguments shall exactly reflect the D-Bus method output arguments. A concatenator example:
Expand Down Expand Up @@ -1264,6 +1276,18 @@ The future object will contain void for a void-returning D-Bus method, a single
...
```

A third option, available with C++20, is to finish the async call statement with `getResultAsAwaitable<ReturnTypes...>()`, which returns an `Awaitable<T>` that can be `co_await`ed in a coroutine. The template arguments are the D-Bus method return types (empty for void-returning methods). The awaitable returns `void`, a single value, or a `std::tuple` for multiple return values:

```c++
// In a coroutine context:
auto result = co_await concatenatorProxy->callMethodAsync("concatenate")
.onInterface(interfaceName)
.withArguments(numbers, separator)
.getResultAsAwaitable<std::string>();
std::cout << "Got concatenate result: " << result << std::endl;
// If an error occurs, sdbus::Error is thrown when co_await completes
```

### Marking client-side async methods in the IDL

sdbus-c++-xml2cpp can generate C++ code for client-side async methods. We just need to annotate the method with `org.freedesktop.DBus.Method.Async`. The annotation element value must be either `client` (async on the client-side only) or `client-server` (async method on both client- and server-side):
Expand All @@ -1286,19 +1310,39 @@ sdbus-c++-xml2cpp can generate C++ code for client-side async methods. We just n
</node>
```

An asynchronous method can be generated as a callback-based method or `std::future`-based method. This can optionally be customized through an additional `org.freedesktop.DBus.Method.Async.ClientImpl` annotation. Its supported values are `callback` and `std::future`. The default behavior is callback-based method.
An asynchronous method can be generated as a callback-based method, `std::future`-based method, or C++20 awaitable-based method. This can optionally be customized through an additional `org.freedesktop.DBus.Method.Async.ClientImpl` annotation. Its supported values are `callback`, `future` and `awaitable`. The default behavior is callback-based method.

#### Generating callback-based async methods

For each client-side async method, a corresponding `on<MethodName>Reply` pure virtual function, where `<MethodName>` is the capitalized D-Bus method name, is generated in the generated proxy class. This function is the callback invoked when the D-Bus method reply arrives, and must be provided a body by overriding it in the implementation class.

So in the specific example above, the tool will generate a `Concatenator_proxy` class similar to one shown in a [dedicated section above](#concatenator-client-glueh), with the difference that it will also generate an additional `virtual void onConcatenateReply(std::optional<sdbus::Error> error, const std::string& concatenatedString);` method, which we shall override in the derived `ConcatenatorProxy`.

#### Generating std:future-based async methods
#### Generating std::future-based async methods

In this case, a `std::future` is returned by the method, which will later, when the reply arrives, get set to contain the return value. Or if the call returns an error, `sdbus::Error` will be thrown by `std::future::get()`.

For a real example of a client-side asynchronous D-Bus methods, please look at sdbus-c++ [stress tests](/tests/stresstests).
#### Generating awaitable-based async methods

> **_Note_:** This requires C++20 support. The generated code uses `sdbus::Awaitable<T>` which requires compiling with C++20 or newer.

When using `awaitable` as the `ClientImpl` annotation value, the generated method returns an `sdbus::Awaitable<T>` that can be used with C++20 coroutines. The return type `T` is `void` for void-returning D-Bus methods, a single type for single-value methods, or `std::tuple<Types...>` for multi-value methods.

Example annotation:

```xml
<method name="concatenate">
<annotation name="org.freedesktop.DBus.Method.Async" value="client" />
<annotation name="org.freedesktop.DBus.Method.Async.ClientImpl" value="awaitable" />
<arg type="ai" name="numbers" direction="in" />
<arg type="s" name="separator" direction="in" />
<arg type="s" name="concatenatedString" direction="out" />
</method>
```

This generates a method that can be `co_await`ed: `std::string result = co_await proxy.concatenate({1, 2, 3}, ":");`

For a real example of a client-side asynchronous D-Bus methods, please look at sdbus-c++ [stress tests](/tests/stresstests) and [integration tests](/tests/integrationtests).

## Method call timeout

Expand Down Expand Up @@ -1338,7 +1382,7 @@ We read property value easily through `IProxy::getProperty()` method:
uint32_t status = proxy->getProperty("status").onInterface("org.sdbuscpp.Concatenator");
```

Getting a property in asynchronous manner is also possible, in both callback-based and future-based way, by calling `IProxy::getPropertyAsync()` method:
Getting a property in asynchronous manner is also possible, in callback-based, future-based, or (with C++20) awaitable way, by calling `IProxy::getPropertyAsync()` method:

```c++
// Callback-based method:
Expand All @@ -1347,10 +1391,15 @@ auto callback = [](std::optional<sdbus::Error> /*error*/, sdbus::Variant value)
std::cout << "Got property value: " << value.get<uint32_t>() << std::endl;
};
uint32_t status = proxy->getPropertyAsync("status").onInterface("org.sdbuscpp.Concatenator").uponReplyInvoke(std::move(callback));

// Future-based method:
std::future<sdbus::Variant> statusFuture = object.getPropertyAsync("status").onInterface("org.sdbuscpp.Concatenator").getResultAsFuture();
...
std::cout << "Got property value: " << statusFuture.get().get<uint32_t>() << std::endl;

// Awaitable method (C++20):
auto value = co_await proxy->getPropertyAsync("status").onInterface("org.sdbuscpp.Concatenator").getResultAsAwaitable();
std::cout << "Got property value: " << value.get<uint32_t>() << std::endl;
```

More information on an `error` callback handler parameter, on behavior of `future` in erroneous situations, can be found in section [Asynchronous client-side methods](#asynchronous-client-side-methods).
Expand All @@ -1364,14 +1413,18 @@ uint32_t status = ...;
proxy->setProperty("status").onInterface("org.sdbuscpp.Concatenator").toValue(status);
```

Setting a property in asynchronous manner is also possible, in both callback-based and future-based way, by calling `IProxy::setPropertyAsync()` method:
Setting a property in asynchronous manner is also possible, in callback-based, future-based, or awaitable way, by calling `IProxy::setPropertyAsync()` method:

```c++
// Callback-based method:
auto callback = [](std::optional<sdbus::Error> error { /*... Error handling in case error contains a value...*/ };
uint32_t status = proxy->setPropertyAsync("status").onInterface("org.sdbuscpp.Concatenator").toValue(status).uponReplyInvoke(std::move(callback));

// Future-based method:
std::future<void> statusFuture = object.setPropertyAsync("status").onInterface("org.sdbuscpp.Concatenator").getResultAsFuture();

// Awaitable method (C++20):
co_await proxy->setPropertyAsync("status").onInterface("org.sdbuscpp.Concatenator").toValue(status).getResultAsAwaitable();
```

More information on `error` callback handler parameter, on behavior of `future` in erroneous situations, can be found in section [Asynchronous client-side methods](#asynchronous-client-side-methods).
Expand Down Expand Up @@ -1468,7 +1521,7 @@ When implementing the adaptor, we simply need to provide the body for the `statu

We can mark the property so that the generator generates either asynchronous variant of getter method, or asynchronous variant of setter method, or both. Annotations names are `org.freedesktop.DBus.Property.Get.Async`, or `org.freedesktop.DBus.Property.Set.Async`, respectively. Their values must be set to `client`.

In addition, we can choose through annotations `org.freedesktop.DBus.Property.Get.Async.ClientImpl`, or `org.freedesktop.DBus.Property.Set.Async.ClientImpl`, respectively, whether a callback-based or future-based variant will be generated. The concept is analogous to the one for asynchronous D-Bus methods described above in this document.
In addition, we can choose through annotations `org.freedesktop.DBus.Property.Get.Async.ClientImpl`, or `org.freedesktop.DBus.Property.Set.Async.ClientImpl`, respectively, whether a callback-based, future-based, or awaitable variant will be generated. Supported values are `callback`, `future`, and `awaitable`. The concept is analogous to the one for asynchronous D-Bus methods described above in this document.

The callback-based method will generate a pure virtual function `On<PropertyName>Property[Get|Set]Reply()`, which must be overridden by the derived class.

Expand Down Expand Up @@ -1679,7 +1732,7 @@ The macro must be placed in the global namespace. The first argument is the stru

This is described in detail in the following sections.

> **_Note_:** The macro supports **max 16 struct members**. If you need more, feel free to open an issue, or implement the teaching code yourself :o)
> **_Note_:** The macro supports **max 16 struct members**. If you need more, feel free to open an issue, or implement the teaching code yourself :o)

> **_Another note_:** You may have noticed one of `my::Struct` members is `std::list`. Thanks to the custom support for `std::list` implemented higher above, it's now automatically accepted by sdbus-c++ as a D-Bus array representation.

Expand Down
158 changes: 158 additions & 0 deletions include/sdbus-c++/Awaitable.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* (C) 2016 - 2021 KISTLER INSTRUMENTE AG, Winterthur, Switzerland
* (C) 2016 - 2026 Stanislav Angelovic <stanislav.angelovic@protonmail.com>
* (C) 2026 - Alex Cani <alexcani109@gmail.com>
*
* @file Awaitable.h
*
* Created on: Feb 28, 2026
* Project: sdbus-c++
* Description: High-level D-Bus IPC C++ library based on sd-bus
*
* This file is part of sdbus-c++.
*
* sdbus-c++ is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* sdbus-c++ is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with sdbus-c++. If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef SDBUS_CXX_AWAITABLE_H_
#define SDBUS_CXX_AWAITABLE_H_

#include <atomic>
#include <cassert>
#if __has_include(<coroutine>)
#include <coroutine>
#endif
#include <cstdint>
#include <exception>
#include <memory>
#include <type_traits>
#include <variant>

namespace sdbus {

// Forward declarations
class AsyncMethodInvoker;
namespace internal {
class Proxy;
} // namespace internal

/********************************************//**
* @enum AwaitableState
*
* Represents the lifecycle state of an asynchronous
* operation in the coroutine awaitable protocol.
* Used for atomic coordination between the coroutine
* and the D-Bus callback thread.
*
***********************************************/
enum class AwaitableState : uint8_t
{
NotReady, // Initial state: callback hasn't fired yet
Waiting, // Coroutine is suspended and waiting for callback
Completed // Callback completed, result is ready
};

// Shared data
template <typename T>
struct AwaitableData
{
using result_type = std::conditional_t<std::is_void_v<T>, std::monostate, T>;
std::variant<result_type, std::exception_ptr> result;
std::atomic<AwaitableState> status{AwaitableState::NotReady};
#ifdef __cpp_lib_coroutine
// Keep the handle as the last member to mainting ABI compatibility
// with clients without coroutine support.
std::coroutine_handle<> handle;
#endif // __cpp_lib_coroutine

void resumeCoroutine()
{
#ifdef __cpp_lib_coroutine
handle.resume();
#endif // __cpp_lib_coroutine
}
};

/********************************************//**
* @class Awaitable
*
* A C++20 coroutine awaitable that represents an asynchronous
* operation. Allows suspending a coroutine until a D-Bus method
* call completes, then resuming with the result or exception.
*
* This is not a full-fledged coroutine type, but a simple awaitable
* that can be used with `co_await` to retrieve results of async D-Bus calls.
* This is independent of any specific coroutine framework or scheduler,
* as it relies on the D-Bus callback mechanism to resume the coroutine.
*
* You most likely don't need to use this class directly. Instead, use the
* respective low-level or high-level API functions that return an Awaitable
* instance, such as IProxy::callMethodAsync with with_awaitable_t tag,
* or the .getResultAsAwaitable() methods of the high-level API.
*
* The class represents nothing, i.e. is a simple placeholder class, if the API
* is used as C++17 or with a standard library not supporting coroutines.
*
***********************************************/
template <typename T>
class Awaitable
{
#ifdef __cpp_lib_coroutine
public:
// Called when the coroutine is co_await'ed. Returns true if the coroutine should be suspended.
[[nodiscard]] bool await_ready() const noexcept
{
return data_->status.load(std::memory_order_acquire) == AwaitableState::Completed;
}

// Called when the coroutine is suspended, returning false here will immediately
// resume the coroutine.
bool await_suspend(std::coroutine_handle<> handle) noexcept
{
data_->handle = handle;

// Attempt transition from NotReady to Waiting.
AwaitableState expected = AwaitableState::NotReady;
return data_->status.compare_exchange_strong(expected, AwaitableState::Waiting, std::memory_order_acq_rel);
}

// Called when the coroutine is resumed. Returns the result or throws the exception.
[[nodiscard]] T await_resume() const
{
if (auto* exception = std::get_if<std::exception_ptr>(&data_->result); exception != nullptr)
std::rethrow_exception(*exception);

if constexpr (std::is_void_v<T>)
return;
else
return std::get<T>(std::move(data_->result));
}
#endif // __cpp_lib_coroutine

private:
friend internal::Proxy;
friend AsyncMethodInvoker;

explicit Awaitable(std::shared_ptr<AwaitableData<T>> data)
: data_(std::move(data))
{
assert(data_ != nullptr);
}

std::shared_ptr<AwaitableData<T>> data_;
};

} // namespace sdbus

#endif // SDBUS_CXX_AWAITABLE_H_
Loading
Loading