Introduction
zbus is a Rust crate for D-Bus. If you are not familiar with D-Bus, you should read what is D-Bus? first1. In short, zbus allows you to communicate from one program to another, using the D-Bus protocol. In other words, it’s an inter-process communication (IPC) solution. It is a very popular IPC solution on Linux and many Linux services (e.g systemd, NetworkManager) and desktop environments (e.g GNOME and KDE), rely on D-Bus for their IPC needs. There are many tools and implementations available, making it easy to interact with D-Bus programs from different languages and environments.
zbus is a 100% Rust-native implementation of the D-Bus protocol. It provides both an API to send and receive messages over a connection, as well as API to interact with peers through high-level concepts like method calls, signals and properties2. Thanks to the power of Rust macros, zbus is able to make interacting with D-Bus very easy.
The crate has two halves.
The wire format
D-Bus defines a marshalling format for its messages. The zbus::wire module provides a
serde-based API to serialize and deserialize Rust data types to and from that format. It does
not need the rest of zbus, so if the format is all you are after — no connection, no proxies —
depend on zbus with its default features disabled:
[dependencies]
zbus = { version = "6", default-features = false }
Outside of a D-Bus context, a modified form of this format, GVariant, is commonly used for efficient storage of arbitrary data; the separate zgvariant crate implements it.
The D-Bus API
Everything else — connections, messages, proxies, the object server — is the API you will use to interact with D-Bus from Rust. It takes care of the establishment of a connection, the creation, sending and receiving of different kind of D-Bus messages (method calls, signals etc) for you.
The API is async. A program with no async runtime of its own drives it with zbus::block_on,
which runs a future to completion on the calling thread; the
FAQ shows how.
-
D-Bus is ~15y old, unfortunately many documents out there are sometime aging or misleading. ↩
-
These concepts are explained in the following chapter. ↩
Some D-Bus concepts to help newcomers
Bus
A D-Bus “bus” is a server that handles several connections in a bus-topology fashion. As such, it relays messages between connected endpoints, and allows to discover endpoints or sending broadcast messages (signals).
Typically, a Linux system has a system bus, and a session bus. The latter is per-user. It is also possible to have private buses or no bus at all (i-e direct peer-to-peer communication instead).
Bus name / service name
An endpoint can have various names, which allows to address messages to it on the bus. All endpoints are assigned a unique name by the bus at start. Since this name is not static, most services use something called a well-known bus name and typically it’s this name, that you’ll be concerned with.
An example would be the FreeDesktop Notifications Service that uses
org.freedesktop.Notifications as its well-known bus name.
For further details on bus names, please refer to the Bus names chapter of the D-Bus specification.
Objects and Object paths
An object is akin to the concept of an object or an instance in many programming languages. All services expose at least one object on the bus and all clients interact with the service through these objects. These objects can be ephemeral or they could live as long as the service itself.
Every object is identified by a string, which is referred to as its path. An example of an object
path is /org/freedesktop/Notifications, which identities the only object exposed by the
FreeDesktop Notifications Service.
For further details on object paths, please refer to the Basic types chapter of the D-Bus specification.
Interfaces
An interface defines the API exposed by object on the bus. They are akin to the concept of interfaces in many programming languages and traits in Rust. Each object can (and typically do) provide multiple interfaces at the same time. A D-Bus interface can have methods, properties and signals.
While each interface of a service is identified by a unique name, its API is described by an XML description. It is mostly a machine-level detail. Most services can be queried for this description through a D-Bus standard introspection interface.
zbus provides convenient macro that implements the introspection interface for services, and helper to generate client-side Rust API, given an XML description. We’ll see both of these in action in the following chapters.
Good practices & API design
It is recommended to organise the service name, object paths and interface name by using fully-qualified domain names, in order to avoid potential conflicts.
Please read the D-Bus API Design Guidelines carefully for other similar considerations.
Onwards to implementation details & examples!
Establishing a connection
The first thing you will have to do is to connect to a D-Bus bus or to a D-Bus peer. This is the entry point of the zbus API.
Connection to the bus
To connect to the session bus (the per-user bus), simply call Connection::session(). It
returns an instance of the connection (if all went well). Similarly, to connect to the system bus
(to communicate with services such as NetworkManager, BlueZ or PID1), use
Connection::system().
Moreover, it can be converted to a MessageStream that implements futures::stream::Stream,
which can be used to conveniently receive messages, for the times when low-level API is
more appropriate for your use case.
Note: it is common for a D-Bus library to provide a “shared” connection to a bus for a process:
all session() share the same underlying connection for example. At the time of this writing,
zbus doesn’t do that.
Note: on macOS, there is no standard implicit way to connect to a session bus. zbus provides
opt-in compatibility to the Launchd session bus discovery mechanism via the launchctl getenv feature.
The official dbus installation method via Homebrew provides a session bus installation,
utilizing macOS LaunchAgents feature. By default, zbus consumes an address for a bus connection that
is provided via launchctl getenv DBUS_LAUNCHD_SESSION_BUS_SOCKET command output.
Using a custom bus address
You may also specify a custom bus with connection::Builder::address which takes a D-Bus address
as specified in the specification.
Note: the unixexec: and ibus: transports are only available when the unixexec and ibus
cargo features of zbus, respectively, are enabled.
Peer to peer connection
Peer-to-peer connections are bus-less1, and the initial handshake protocol is a bit different. There is the notion of client & server endpoints, but that distinction doesn’t matter once the connection is established (both ends are equal, and can send any messages).
For example to create a bus-less peer-to-peer connection on Unix, you can do:
#[tokio::main]
async fn main() -> zbus::Result<()> {
#[cfg(unix)]
{
use std::os::unix::net::UnixStream;
use zbus::{connection::Builder, Guid};
let guid = Guid::generate();
let (p0, p1) = UnixStream::pair().unwrap();
#[allow(unused)]
let (client_conn, server_conn) = futures_util::try_join!(
// Client
Builder::unix_stream(p0).p2p().build(),
// Server
Builder::unix_stream(p1).server(guid).p2p().build(),
)?;
}
Ok(())
}
unix_stream takes a std::os::unix::net::UnixStream, whichever runtime the connection ends
up on. A stream of another kind is handed over as the socket it wraps: into_std() for a
tokio::net::UnixStream.
Note: the p2p and server methods of connection::Builder are only available when p2p
cargo feature of zbus is enabled.
Runtimes
A connection takes its readiness notifications, timers, spawned tasks and blocking work from one
runtime, chosen once when it is built. A program with no async runtime of its own drives it with
zbus::block_on; see the FAQ. A program built on Tokio turns on the tokio feature, and a
connection built while a Tokio runtime is current runs on it. Any other executor can use the
zruntime backend below as it is, with no integration needed: a connection built outside a
zbus::block_on call is run by that backend’s own helper thread. Builder::runtime is there for
an application that wants to hand a connection a runtime of its own instead, not something every
other executor needs. A connection’s async locks come from zruntime whichever runtime it runs on,
except on a Tokio build, which uses Tokio’s locks instead.
Built-in backends
With the default-rt cargo feature (a default feature), a connection runs on zruntime, one
per thread that runs zbus::block_on, depending on no general-purpose async executor crate. The
thread that drives it is the one inside zbus::block_on: between two polls of the future handed to
it, that thread runs the tasks, sockets and timers of every zruntime connection built in it — apart
from a handful of blocking system calls (a DNS lookup, a nonce-file read, a peer-credential lookup)
that use a short-lived worker thread of their own instead. Two threads that each call
zbus::block_on drive their own connections, in parallel. A thread’s runtime serves every
connection built inside its zbus::block_on calls, so a task of one connection that runs long
delays the others’ I/O until it yields, and a connection used from another thread is still driven by
the thread that built it. A connection built on a thread that is in no zbus::block_on at the time
— one that some other executor polls — has no thread of its own to look to, and goes instead on a
single runtime the whole process shares with every other such connection. Only where a connection
has work and no thread is inside zbus::block_on — it is polled from some other executor, or a call
returned with the connection alive — does zbus start a helper thread, which leaves once nothing is
left to run. If the tokio feature is also enabled and a Tokio runtime is current on the thread
that builds the connection, it runs on Tokio instead. With only tokio enabled, a connection always
runs on the Tokio runtime that is current when it is built; building one from a thread with no such
runtime fails with Error::Unsupported.
A build with neither feature depends on no tokio crate. It still depends on the zruntime
crate, whose Event, async locks, broadcast channels and unblock (the thread that blocking work
runs on by default) zbus uses, but not with its runtime feature, so none of zruntime’s runtime is
built. Every connection in the build needs an explicit runtime, given through
Builder::runtime.
Supplying your own runtime
Builder::runtime takes any implementation of traits::Runtime, for an application whose
event loop is neither backend above:
#![allow(unused)]
fn main() {
use zbus::{Connection, Result, connection::Builder, runtime::traits::Runtime};
async fn connect(runtime: impl Runtime) -> Result<Connection> {
Builder::session().runtime(runtime).build().await
}
}
The runtime::traits module documents what an implementation supplies.
Bringing your own socket
Besides an address, a connection can be built directly over a socket you already have.
Builder::unix_stream and Builder::tcp_stream each take an owned, platform-native stream and
register it on the connection’s runtime; a stream of another kind is handed over as the socket it
wraps, such as into_std() for a Tokio stream. Builder::socket and
Builder::authenticated_socket take any implementation of Socket, for any other transport,
such as a VSOCK stream, an in-process channel or a tunnel of your own. The Socket documentation
has how to write one.
-
Unless you implemented them, none of the bus methods will exist. ↩
Writing a client proxy
- Low-level call from a
zbus::Connection - Trait-derived proxy call
- Generating the trait from an XML interface
In this chapter, we are going to see how to make low-level D-Bus method calls. Then we are going to dive in, and derive from a trait to make a convenient Rust binding. Finally, we will learn about xmlgen, a tool to help us generate a boilerplate trait from the XML of an introspected service.
To make this learning “hands-on”, we are going to call and bind the cross-desktop notification service (please refer to this reference document for further details on this API).
Let’s start by playing with the service from the shell, and notify the desktop with busctl1:
busctl --user call \
org.freedesktop.Notifications \
/org/freedesktop/Notifications \
org.freedesktop.Notifications \
Notify \
susssasa\{sv\}i \
"my-app" 0 "dialog-information" "A summary" "Some body" 0 0 5000
Note: busctl has very good auto-completion support in bash or zsh.
Running this command should pop-up a notification dialog on your desktop. If it does not, your desktop does not support the notification service, and this example will be less interactive. Nonetheless you can use a similar approach for other services.
This command shows us several aspects of the D-Bus communication:
-
--user: Connect to and use the user/session bus. -
call: Send a method call message. (D-Bus also supports signals, error messages, and method replies) -
destination: The name of the service (
org.freedesktop.Notifications). -
object path: Object/interface path (
/org/freedesktop/Notifications). -
interface: The interface name (methods are organized in interfaces, here
org.freedesktop.Notifications, same name as the service). -
method: The name of the method to call,
Notify. -
signature: That
susssasa{sv}imeans the method takes 8 arguments of various types. ‘s’, for example, is for a string. ‘as’ is for array of strings. -
The method arguments.
See busctl man page for more details.
Low-level call from a zbus::Connection
zbus Connection has a call_method() method, which you can use directly:
use std::collections::HashMap;
use std::error::Error;
use zbus::{Variant, Connection};
// Although we use `tokio` here, you can use any async runtime of choice.
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let connection = Connection::session().await?;
let m = connection.call_method(
Some("org.freedesktop.Notifications"),
"/org/freedesktop/Notifications",
Some("org.freedesktop.Notifications"),
"Notify",
&("my-app", 0u32, "dialog-information", "A summary", "Some body",
vec![""; 0], HashMap::<&str, &Variant>::new(), 5000),
).await?;
let reply: u32 = m.body().deserialize().unwrap();
dbg!(reply);
Ok(())
}
Although this is already quite flexible, and handles various details for you (such as the message
signature), it is also somewhat inconvenient and error-prone: you can easily miss arguments, or give
arguments with the wrong type or other kind of errors (what would happen if you typed 0, instead
of 0u32?).
Instead, we want to wrap this Notify D-Bus method in a Rust function. Let’s see how next.
Trait-derived proxy call
A trait declaration T with a proxy attribute will have a derived TProxy implemented thanks to
procedural macros. The trait methods will have respective impl methods wrapping the D-Bus calls:
use std::collections::HashMap;
use std::error::Error;
use zbus::{Variant, proxy, Connection};
#[proxy(
default_service = "org.freedesktop.Notifications",
default_path = "/org/freedesktop/Notifications"
)]
trait Notifications {
/// Call the org.freedesktop.Notifications.Notify D-Bus method
fn notify(&self,
app_name: &str,
replaces_id: u32,
app_icon: &str,
summary: &str,
body: &str,
actions: &[&str],
hints: HashMap<&str, &Variant<'_>>,
expire_timeout: i32) -> zbus::Result<u32>;
}
// Although we use `tokio` here, you can use any async runtime of choice.
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let connection = Connection::session().await?;
let proxy = NotificationsProxy::new(&connection).await?;
let reply = proxy.notify(
"my-app",
0,
"dialog-information",
"A summary", "Some body",
&[],
HashMap::new(),
5000,
).await?;
dbg!(reply);
Ok(())
}
A TProxy has a few associated methods, such as new(connection), using the default associated
service name and object path, and an associated builder if you need to specify something
different.
This should help to avoid the kind of mistakes we saw earlier. It’s also a bit easier to use, thanks to Rust type inference. This makes it also possible to have higher-level types, they fit more naturally with the rest of the code. You can further document the D-Bus API or provide additional helpers.
Note
For a program without an async runtime of its own, see the FAQ.
Signals
Signals are like methods, except they don’t expect a reply. They are typically emitted by services
to notify interested peers of any changes to the state of the service. zbus provides you a
Stream-based API for receiving signals.
Let’s look at this API in action, with an example where we monitor started systemd units.
// NOTE: When changing this, please also keep `zbus/examples/watch-systemd-jobs.rs` in sync.
use futures_util::stream::StreamExt;
use zbus::{OwnedObjectPath, proxy, Connection};
fn main() {
zbus::block_on(watch_systemd_jobs()).expect("Error listening to signal");
}
#[proxy(
default_service = "org.freedesktop.systemd1",
default_path = "/org/freedesktop/systemd1",
interface = "org.freedesktop.systemd1.Manager"
)]
trait Systemd1Manager {
// Defines signature for D-Bus signal named `JobNew`
#[zbus(signal)]
fn job_new(&self, id: u32, job: OwnedObjectPath, unit: String) -> zbus::Result<()>;
}
async fn watch_systemd_jobs() -> zbus::Result<()> {
let connection = Connection::system().await?;
// `Systemd1ManagerProxy` is generated from `Systemd1Manager` trait
let systemd_proxy = Systemd1ManagerProxy::new(&connection).await?;
// Method `receive_job_new` is generated from `job_new` signal
let mut new_jobs_stream = systemd_proxy.receive_job_new().await?;
while let Some(msg) = new_jobs_stream.next().await {
// struct `JobNewArgs` is generated from `job_new` signal function arguments
let args: JobNewArgs = msg.args().expect("Error parsing message");
println!(
"JobNew received: unit={} id={} path={}",
args.unit, args.id, args.job
);
}
panic!("Stream ended unexpectedly");
}
More advanced example
Here is a more elaborate example, where we get our location from Geoclue:
use zbus::{ObjectPath, proxy, Connection, Result};
use futures_util::stream::StreamExt;
#[proxy(
default_service = "org.freedesktop.GeoClue2",
interface = "org.freedesktop.GeoClue2.Manager",
default_path = "/org/freedesktop/GeoClue2/Manager"
)]
trait Manager {
#[zbus(object = "Client")]
fn get_client(&self);
}
#[proxy(
default_service = "org.freedesktop.GeoClue2",
interface = "org.freedesktop.GeoClue2.Client"
)]
trait Client {
fn start(&self) -> Result<()>;
fn stop(&self) -> Result<()>;
#[zbus(property)]
fn set_desktop_id(&mut self, id: &str) -> Result<()>;
#[zbus(signal)]
fn location_updated(&self, old: ObjectPath<'_>, new: ObjectPath<'_>) -> Result<()>;
}
#[proxy(
default_service = "org.freedesktop.GeoClue2",
interface = "org.freedesktop.GeoClue2.Location"
)]
trait Location {
#[zbus(property)]
fn latitude(&self) -> Result<f64>;
#[zbus(property)]
fn longitude(&self) -> Result<f64>;
}
// Although we use `tokio` here, you can use any async runtime of choice.
#[tokio::main]
async fn main() -> Result<()> {
let conn = Connection::system().await?;
let manager = ManagerProxy::new(&conn).await?;
let mut client = manager.get_client().await?;
// Gotta do this, sorry!
client.set_desktop_id("org.freedesktop.zbus").await?;
let props = zbus::fdo::PropertiesProxy::builder(&conn)
.destination("org.freedesktop.GeoClue2")
.path(client.inner().path())
.build()
.await?;
let mut props_changed = props.receive_properties_changed().await?;
let mut location_updated = client.receive_location_updated().await?;
client.start().await?;
futures_util::try_join!(
async {
while let Some(signal) = props_changed.next().await {
let args = signal.args()?;
for (name, value) in args.changed_properties().iter() {
println!("{}.{} changed to `{:?}`", args.interface_name(), name, value);
}
}
Ok::<(), zbus::Error>(())
},
async {
while let Some(signal) = location_updated.next().await {
let args = signal.args()?;
let location = LocationProxy::builder(&conn)
.path(args.new())
.build()
.await?;
println!(
"Latitude: {}\nLongitude: {}",
location.latitude().await?,
location.longitude().await?,
);
}
// No need to specify type of Result each time
Ok(())
}
)?;
Ok(())
}
While the Geoclue’s D-Bus API is a bit involved, we still ended-up with a not-so-complicated (~100 LOC) code for getting our location.
Properties
Interfaces can have associated properties, which can be read or set with the
org.freedesktop.DBus.Properties interface. Here again, the #[proxy] attribute comes to the
rescue to help you. You can annotate a trait method to be a getter:
use zbus::{proxy, Result};
#[proxy]
trait MyInterface {
#[zbus(property)]
fn state(&self) -> Result<String>;
}
The state() method will translate to a "State" property Get call.
To set the property, prefix the name of the property with set_.
For a more real world example, let’s try and read two properties from systemd’s main service:
use zbus::{Connection, proxy, Result};
#[proxy(
interface = "org.freedesktop.systemd1.Manager",
default_service = "org.freedesktop.systemd1",
default_path = "/org/freedesktop/systemd1"
)]
trait SystemdManager {
#[zbus(property)]
fn architecture(&self) -> Result<String>;
#[zbus(property)]
fn environment(&self) -> Result<Vec<String>>;
}
#[tokio::main]
async fn main() -> Result<()> {
let connection = Connection::system().await?;
let proxy = SystemdManagerProxy::new(&connection).await?;
println!("Host architecture: {}", proxy.architecture().await?);
println!("Environment variables:");
for env in proxy.environment().await? {
println!(" {}", env);
}
Ok(())
}
You should get an output similar to this:
Host architecture: x86-64
Environment variables:
HOME=/home/zeenix
LANG=en_US.UTF-8
LC_ADDRESS=de_DE.UTF-8
LC_IDENTIFICATION=de_DE.UTF-8
LC_MEASUREMENT=de_DE.UTF-8
LC_MONETARY=de_DE.UTF-8
LC_NAME=de_DE.UTF-8
LC_NUMERIC=de_DE.UTF-8
LC_PAPER=de_DE.UTF-8
LC_TELEPHONE=de_DE.UTF-8
LC_TIME=de_DE.UTF-8
...
Watching for changes
By default, the proxy will cache the properties and watch for changes.
To be notified of a property change, you use a stream API, just like for receiving signals. The
methods are named after the properties’ names: receive_<prop_name>_changed().
Here is an example:
use zbus::{Connection, proxy, Result};
use futures_util::stream::StreamExt;
#[tokio::main]
async fn main() -> Result<()> {
#[proxy(
interface = "org.freedesktop.systemd1.Manager",
default_service = "org.freedesktop.systemd1",
default_path = "/org/freedesktop/systemd1"
)]
trait SystemdManager {
#[zbus(property)]
fn log_level(&self) -> Result<String>;
}
let connection = Connection::system().await?;
let proxy = SystemdManagerProxy::new(&connection).await?;
let mut stream = proxy.receive_log_level_changed().await;
while let Some(v) = stream.next().await {
println!("LogLevel changed: {:?}", v.get().await);
}
Ok(())
}
Generating the trait from an XML interface
The zbus_xmlgen crate provides a developer-friendly tool, that can generate Rust traits from a
given D-Bus introspection XML for you.
Note: This tool should not be considered a drop-in Rust-specific replacement for similar tools
available for low-level languages, such as gdbus-codegen. Unlike those tools, this is only meant
as a starting point to generate the code, once. In many cases, you will want to tweak the generated
code.
The tool can be used to generate rust code directly from a D-Bus service running on our system:
zbus-xmlgen session \
org.freedesktop.Notifications \
/org/freedesktop/Notifications
Alternatively you can also get the XML interface from a different source and use it to generate the
interface code. Some packages may also provide the XML directly as an installed file, allowing it to
be queried using pkg-config, for example.
We can fetch the XML interface of the notification service, using the --xml-interface option of
the busctl1 command. This option was introduced to busctl in systemd v243.
busctl --user --xml-interface introspect \
org.freedesktop.Notifications \
/org/freedesktop/Notifications
You should get a similar output:
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<!-- other interfaces omitted -->
<interface name="org.freedesktop.Notifications">
<method name="Notify">
<arg type="s" name="arg_0" direction="in">
</arg>
<arg type="u" name="arg_1" direction="in">
</arg>
<arg type="s" name="arg_2" direction="in">
</arg>
<arg type="s" name="arg_3" direction="in">
</arg>
<arg type="s" name="arg_4" direction="in">
</arg>
<arg type="as" name="arg_5" direction="in">
</arg>
<arg type="a{sv}" name="arg_6" direction="in">
</arg>
<arg type="i" name="arg_7" direction="in">
</arg>
<arg type="u" name="arg_8" direction="out">
</arg>
</method>
<method name="CloseNotification">
<arg type="u" name="arg_0" direction="in">
</arg>
</method>
<method name="GetCapabilities">
<arg type="as" name="arg_0" direction="out">
</arg>
</method>
<method name="GetServerInformation">
<arg type="s" name="arg_0" direction="out">
</arg>
<arg type="s" name="arg_1" direction="out">
</arg>
<arg type="s" name="arg_2" direction="out">
</arg>
<arg type="s" name="arg_3" direction="out">
</arg>
</method>
<signal name="NotificationClosed">
<arg type="u" name="arg_0">
</arg>
<arg type="u" name="arg_1">
</arg>
</signal>
<signal name="ActionInvoked">
<arg type="u" name="arg_0">
</arg>
<arg type="s" name="arg_1">
</arg>
</signal>
</interface>
</node>
Save the output to a notify.xml file. Then call:
zbus-xmlgen file notify.xml
This will give back effortlessly the corresponding Rust traits boilerplate code:
use zbus::proxy;
#[proxy(
interface = "org.freedesktop.Notifications",
default_service = "org.freedesktop.Notifications",
default_path= "/org/freedesktop/Notifications",
)]
trait Notifications {
/// CloseNotification method
fn close_notification(&self, arg_0: u32) -> zbus::Result<()>;
/// GetCapabilities method
fn get_capabilities(&self) -> zbus::Result<Vec<String>>;
/// GetServerInformation method
fn get_server_information(&self) -> zbus::Result<(String, String, String, String)>;
/// Notify method
fn notify(
&self,
arg_0: &str,
arg_1: u32,
arg_2: &str,
arg_3: &str,
arg_4: &str,
arg_5: &[&str],
arg_6: std::collections::HashMap<&str, zbus::Variant<'_>>,
arg_7: i32,
) -> zbus::Result<u32>;
/// ActionInvoked signal
#[zbus(signal)]
fn action_invoked(&self, arg_0: u32, arg_1: &str) -> zbus::Result<()>;
/// NotificationClosed signal
#[zbus(signal)]
fn notification_closed(&self, arg_0: u32, arg_1: u32) -> zbus::Result<()>;
}
It should be usable as such. But you may as well improve a bit the naming of the arguments, use better types (bit flags, structs or other custom types), add extra documentation, and other functions to make the binding more pleasing to use from Rust.
For example, the generated GetServerInformation method can be improved to a nicer version:
use serde::{Serialize, Deserialize};
use zbus::{Type, proxy};
#[derive(Debug, Type, Serialize, Deserialize)]
pub struct ServerInformation {
/// The product name of the server.
pub name: String,
/// The vendor name. For example "KDE," "GNOME," "freedesktop.org" or "Microsoft".
pub vendor: String,
/// The server's version number.
pub version: String,
/// The specification version the server is compliant with.
pub spec_version: String,
}
#[proxy(
interface = "org.freedesktop.Notifications",
default_service = "org.freedesktop.Notifications",
default_path= "/org/freedesktop/Notifications",
)]
trait Notifications {
/// Get server information.
///
/// This message returns the information on the server.
fn get_server_information(&self) -> zbus::Result<ServerInformation>;
}
You can learn more from the zbus-ify binding of PolicyKit, for example, which was implemented starting from the xmlgen output.
There you have it, a Rust-friendly binding for your D-Bus service!
Writing a service interface
In this chapter, we are going to implement a service with a method “SayHello”, to greet back the calling client.
We will first discuss the need to associate a service name with the service. Then we are going to
manually handle incoming messages using the low-level API. Finally, we will present the
ObjectServer higher-level API and some of its more advanced concepts.
Taking a service name
As we know from the chapter on D-Bus concepts, each connection on the bus is given a unique name (such as “:1.27”). This could be all you need, depending on your use case, and the design of your D-Bus API. However, typically services use a service name (aka well-known name) so peers (clients, in this context) can easily discover them.
In this example, that is exactly what we’re going to do and request the bus for the service name of our choice:
use zbus::{Connection, Result};
// Although we use `tokio` here, you can use any async runtime of choice.
#[tokio::main]
async fn main() -> Result<()> {
let connection = Connection::session()
.await?;
connection
.request_name("org.zbus.MyGreeter")
.await?;
loop {}
}
We can check our service is running and is associated with the service name:
$ busctl --user list | grep zbus
org.zbus.MyGreeter 412452 service elmarco :1.396 user@1000.service - -
🖐 Hang on
This example is not handling incoming messages yet. Any attempt to call the service will time out (including the shell completion!).
Handling low-level messages
At the low-level, you can handle method calls by checking the incoming messages manually.
Let’s write a SayHello method, that takes a string as argument, and reply with a “hello” greeting
by replacing the loop above with this code:
use futures_util::stream::TryStreamExt;
// Although we use `tokio` here, you can use any async runtime of choice.
#[tokio::main]
async fn main() -> zbus::Result<()> {
let connection = zbus::Connection::session()
.await?;
let mut stream = zbus::MessageStream::from(&connection);
connection
.request_name("org.zbus.MyGreeter")
.await?;
while let Some(msg) = stream.try_next().await? {
let msg_header = msg.header();
dbg!(&msg);
match msg_header.message_type() {
zbus::message::Type::MethodCall => {
// real code would check msg_header path(), interface() and member()
// handle invalid calls, introspection, errors etc
let header = msg.header();
let body = msg.body();
let arg: &str = body.deserialize()?;
connection.reply(&header, &(format!("Hello {}!", arg))).await?;
break;
}
_ => continue,
}
}
Ok(())
}
And check if it works as expected:
$ busctl --user call org.zbus.MyGreeter /org/zbus/MyGreeter org.zbus.MyGreeter1 SayHello s "zbus"
s "Hello zbus!"
This is the crust of low-level message handling. It should give you all the flexibility you ever need, but it is also easy to get it wrong. Fortunately, zbus has a simpler solution to offer.
Using the ObjectServer
One can write an impl block with a set of methods and let the interface procedural macro
write the D-Bus message handling details. It will dispatch the incoming method calls to their
respective handlers, as well as replying to introspection requests.
MyGreeter simple example
Let see how to use it for MyGreeter interface:
use zbus::{Connection, interface, Result};
struct Greeter;
#[interface(name = "org.zbus.MyGreeter1")]
impl Greeter {
async fn say_hello(&self, name: &str) -> String {
format!("Hello {}!", name)
}
}
// Although we use `tokio` here, you can use any async runtime of choice.
#[tokio::main]
async fn main() -> Result<()> {
let connection = Connection::session().await?;
// setup the object server
connection
.object_server()
.at("/org/zbus/MyGreeter", Greeter)
.await?;
// before requesting the name
connection
.request_name("org.zbus.MyGreeter")
.await?;
loop {
// do something else, wait forever or timeout here:
// handling D-Bus messages is done in the background
std::future::pending::<()>().await;
}
}
⚠ Service activation pitfalls
A possible footgun here is that you must request the service name after you setup the handlers,
otherwise incoming messages may be lost. Activated services may receive calls (or messages) right
after taking their name. This is why it’s typically better to make use of connection::Builder for
setting up your interfaces and requesting names, and not have to care about this:
use zbus::{connection, interface, Result};
struct Greeter;
#[interface(name = "org.zbus.MyGreeter1")]
impl Greeter {
async fn say_hello(&self, name: &str) -> String {
format!("Hello {}!", name)
}
}
#[tokio::main]
async fn main() -> Result<()> {
let _connection = connection::Builder::session()
.name("org.zbus.MyGreeter")
.serve_at("/org/zbus/MyGreeter", Greeter)
.build()
.await?;
loop {
// do something else, wait forever or timeout here:
// handling D-Bus messages is done in the background
std::future::pending::<()>().await;
}
}
It should work with the same busctl command used previously.
This time, we can also introspect the service:
$ busctl --user introspect org.zbus.MyGreeter /org/zbus/MyGreeter
NAME TYPE SIGNATURE RESULT/VALUE FLAGS
org.freedesktop.DBus.Introspectable interface - - -
.Introspect method - s -
org.freedesktop.DBus.Peer interface - - -
.GetMachineId method - s -
.Ping method - - -
org.freedesktop.DBus.Properties interface - - -
.Get method ss v -
.GetAll method s a{sv} -
.Set method ssv - -
.PropertiesChanged signal sa{sv}as - -
org.zbus.MyGreeter1 interface - - -
.SayHello method s s -
A more complete example
ObjectServer supports various method attributes to declare properties or signals.
This is a more complete example, demonstrating some of its usages. It also shows one of the many
ways to synchronize with the interface handlers from outside: an Event from the zruntime
crate, where zbus’s default runtime comes from. An Event needs no runtime of its own, so it
works under Tokio as it does here; add zruntime to your own Cargo.toml to use it.
use zbus::{object_server::SignalEmitter, connection::Builder, interface, fdo, Result};
use zruntime::Event;
struct Greeter {
name: String,
done: Event,
}
#[interface(name = "org.zbus.MyGreeter1")]
impl Greeter {
async fn say_hello(&self, name: &str) -> String {
format!("Hello {}!", name)
}
// Rude!
async fn go_away(
&self,
#[zbus(signal_emitter)]
emitter: SignalEmitter<'_>,
) -> fdo::Result<()> {
emitter.greeted_everyone().await?;
self.done.notify(1);
Ok(())
}
/// A "GreeterName" property.
#[zbus(property)]
async fn greeter_name(&self) -> &str {
&self.name
}
/// A setter for the "GreeterName" property.
///
/// Additionally, a `greeter_name_changed` method has been generated for you if you need to
/// notify listeners that "GreeterName" was updated. It will be automatically called when
/// using this setter.
#[zbus(property)]
async fn set_greeter_name(&mut self, name: String) {
self.name = name;
}
/// A signal; the implementation is provided by the macro.
#[zbus(signal)]
async fn greeted_everyone(emitter: &SignalEmitter<'_>) -> Result<()>;
}
// Although we use `tokio` here, you can use any async runtime of choice.
#[tokio::main]
async fn main() -> Result<()> {
let greeter = Greeter {
name: "GreeterName".to_string(),
done: Event::new(),
};
let done_listener = greeter.done.listen();
let connection = Builder::session()
.name("org.zbus.MyGreeter")
.serve_at("/org/zbus/MyGreeter", greeter)
.build()
.await?;
done_listener.await;
// Let's emit the signal again, just for the fun of it.
connection
.object_server()
.interface("/org/zbus/MyGreeter")
.await?
.greeted_everyone()
.await?;
Ok(())
}
This is the introspection result:
$ busctl --user introspect org.zbus.MyGreeter /org/zbus/MyGreeter
NAME TYPE SIGNATURE RESULT/VALUE FLAGS
[...]
org.zbus.MyGreeter1 interface - - -
.GoAway method - - -
.SayHello method s s -
.GreeterName property s "GreeterName" emits-change writable
.GreetedEveryone signal - - -
Method errors
There are two possibilities for the return value of interface methods. The first is for infallible
method calls, where the return type is a directly serializable value, like the String in
say_hello() above.
The second is a result return value, where the Ok variant is the serializable value, and the
error is any type that implements zbus::DBusError. The zbus::fdo::Error type implements this
trait, and should cover most common use cases. However, when a custom error type needs to be emitted
from the method as an error reply, it can be created using derive(zbus::DBusError), and used in
the returned Result<T, E>.
Property getters and setters follow the same rule as methods: the error is any type that
implements zbus::DBusError, so a custom derive(zbus::DBusError) type works there too.
zbus::fdo::Error covers the common cases; zbus::fdo::Error::UnknownProperty is the one
you’ll reach for most often.
Sending signals
As you might have noticed in the previous example, the signal methods don’t take a &self argument
but a SignalEmitter reference. While this allows to emit signals whether from inside or outside of
the interface methods’ context, it does make it a bit inconvenient to use. To make things easier
interface generates a trait, GreeterSignals that provides the same signal methods but
without the SignalEmitter argument. The macro provides two implementations of this trait for:
zbus::object_server::InterfaceRef<Greeter>zbus::object_server::SignalEmitter
The former is useful for emitting signals from outside the context of an interface method and the
latter is useful for emitting signals from inside of it. To make it possible to emit signals from
inside of an interface method, methods can ask to receive a SignalEmitter passed to them using
the special zbus(signal_emitter) attribute.
Please refer to interface documentation for more examples and list of other special
attributes you can make use of.
We saw examples of signal emission in action, from both inside and outside an interface method, in
the previous example.
Notifying property changes
For each property declared through the interface macro, a <property_name>_changed method is
generated that emits the necessary property change signal. Here is how to use it with the previous
example code:
use zbus::interface;
struct Greeter {
name: String
}
#[interface(name = "org.zbus.MyGreeter1")]
impl Greeter {
#[zbus(property)]
async fn greeter_name(&self) -> &str {
&self.name
}
#[zbus(property)]
async fn set_greeter_name(&mut self, name: String) {
self.name = name;
}
}
#[tokio::main]
async fn main() -> zbus::Result<()> {
let connection = zbus::Connection::session().await?;
let object_server = connection.object_server();
let iface_ref = object_server.interface::<_, Greeter>("/org/zbus/MyGreeter").await?;
let mut iface = iface_ref.get_mut().await;
iface.name = String::from("👋");
iface.greeter_name_changed(iface_ref.signal_emitter()).await?;
Ok(())
}
Proxy generation
interface macro can also generate the client-side proxy code for you. It utilizes the proxy
macro behind the scenes to achieve this. Here is how to use it:
use zbus::interface;
struct Greeter {
name: String
}
#[interface(
name = "org.zbus.MyGreeter.WithProxy",
// Specifying the `proxy` attribute instructs `interface` to generate the
// client-side proxy. You can specify proxy-specific attributes
// (e.g `async_name`) here. All the attributes that are common between
// `proxy` and `interface` macros (e.g `name`) are automtically forwarded to
// the `proxy` macro.
proxy(
default_path = "/org/zbus/MyGreeter/WithProxy",
default_service = "org.zbus.MyGreeter.WithProxy",
),
)]
impl Greeter {
#[zbus(property)]
async fn greeter_name(&self) -> String {
self.name.clone()
}
#[zbus(proxy(no_reply))]
async fn whatever(&self) {
println!("Whatever!");
}
}
#[tokio::main]
async fn main() -> zbus::Result<()> {
let greeter = Greeter { name: "GreeterName".to_string() };
let connection = zbus::connection::Builder::session()
.name("org.zbus.MyGreeter.WithProxy")
.serve_at("/org/zbus/MyGreeter/WithProxy", greeter)
.build()
.await?;
let proxy = GreeterProxy::new(&connection).await?;
assert_eq!(proxy.greeter_name().await?, "GreeterName");
proxy.whatever().await?;
Ok(())
}
Known Limitations
While it’s extremely useful to be able to generate the client-side proxy code directly from
interface as it allows you to avoid duplicating code, there are some limitations to be aware of:
- The
proxymacro’s trait bounds also apply to ordinaryinterfacemethods. A service-only method result needsSerialize + Type, while the corresponding generated proxy method also needsDeserializeOwned. - Ordinary method return values cannot borrow. A borrowing property getter is supported: its
generated proxy getter is generic over an owned
DeserializeOwned + Typeresult selected by the caller. - Methods returning [
object_server::ResponseDispatchNotifier] wrapper type will do the same for proxy as well.
Upgrading to zbus 6.0
- Which part applies to you
- Cargo features
- Paths
- Errors
- What the compatibility module covers
- Other changes in 6.0
Valueis nowVariant- Builder setters defer their errors to
build() - Property methods use Serde traits
Builder::runtimereplacesinternal_executor- The blocking API is gone
- The flag types are
bitflagstypes - The
enumflags2feature is gone - Dictionary keys have to be basic types
zbus_xmlborrows from the document and keeps its order- A renamed
zbusdependency needs thecrateattribute - The APIs deprecated in 5.x are removed
- Smaller changes
- A stale zvariant in the dependency graph
- GVariant
zbus 6.0 is one dependency where there used to be as many as four. The zvariant,
zvariant_derive and zbus_names crates were merged into zbus:
| Was | Is now |
|---|---|
the zvariant crate | common types at the zbus root; codecs in zbus::wire |
the zvariant_derive crate | the derives, re-exported from zbus |
the zbus_names crate | the zbus::names module |
GVariant support, deprecated in zvariant 5.15, is gone; it lives on in the zgvariant crate.
Which part applies to you
- You depend on
zbus. Bump the version and, if you disable the default features, see Cargo features.zbus::zvariantremains as a deprecated alias module until 7.0, so most code keeps compiling with a warning; what it does not cover is listed below. Move common types and derives to thezbusroot and encoding and decoding calls tozbus::wireat your own pace. - You depend on
zvariantand not onzbus. Depend onzbuswithdefault-features = falseinstead, with the same features: they kept their names, except for the removedgvariant,ostree-testsandenumflags2. Renamezvariant::tozbus::for common types and derives, minding thatValueis nowVariant, and tozbus::wire::for encoding and decoding APIs. - You depend on
zbus_names. Depend onzbuswithdefault-features = falseinstead and renamezbus_names::tozbus::names::. - You use the
gvariantfeature. Move to zgvariant.
Cargo features
A lot of the API is now behind new Cargo features. Those marked as default are on with a plain
zbus = "6"; the others have to be enabled explicitly:
comms: the D-Bus API, i.e. connections, messages andfdo. Every other D-Bus feature, such astokio,proxyorp2p, enables it. Without it, zbus is the wire format and the name types.proxy(default): the client-side API, i.e.Proxy,#[proxy]and thefdo::*Proxytypes.service(default): the service-side API, i.e.ObjectServerand#[interface].object-manager(default): the service-sidefdo::ObjectManager.tracing(default): log events and spans throughtracing.default-rt(default): zbus’s own runtime, zruntime; replaces 5.x’sasync-io.tokio-multithread: runszbus::block_onon a multi-threaded Tokio runtime.unixexecandibus: theunixexec:andibus:transports. Without them, an address of that transport is rejected, including inDBUS_SESSION_BUS_ADDRESS.
These features are gone and have to be removed from your dependency entry:
async-io: usedefault-rt.blocking-api: the blocking API is gone.vsockandtokio-vsock: VSOCK is no longer a transport. To use a VSOCK stream you open yourself, see the FAQ.enumflags2: see Theenumflags2feature is gone.gvariantandostree-tests: see GVariant.async-fs,async-executor,async-task,async-lock,async-processandblocking: these were never meant to be features. If you use these crates, depend on them directly.
zbus’s tokio feature no longer enables Tokio’s fs, io-util, process, rt-multi-thread and
tracing features (the last three come back with zbus’s unixexec/ibus, tokio-multithread
and tracing features). Enable the ones your code uses in your own tokio dependency.
A direct zbus_macros dependency with default-features = false needs its new proxy and
service features for #[proxy] and #[interface], and comms for DBusError.
zbus 6 needs Rust 1.89 or newer.
Paths
| zbus 5 / zvariant 5 / zbus_names 4 | zbus 6 |
|---|---|
Common zvariant::X, zbus::zvariant::X types and derives | zbus::X |
zvariant::Value and OwnedValue | zbus::Variant and OwnedVariant |
zvariant::serialized::Context | zbus::wire::serialized::Context |
zvariant::as_value, zvariant::dbus | zbus::as_variant, zbus::wire::dbus |
zvariant::signature! | zbus::signature! |
zbus_names::X, zbus::names::X | zbus::names::X |
zvariant::Error, zvariant::Result | zbus::Error, zbus::Result |
zbus_names::Error, zbus::names::Error | zbus::Error |
zvariant::MaxDepthExceeded | zbus::MaxDepthExceeded |
#[zvariant(...)] on a derive | #[zbus(...)]; both spellings stay accepted |
#[zvariant(crate = "zvariant")] | #[zbus(crate = "zbus::wire")] |
Encoding and decoding functions, serialized, Endian and its constants, DynamicDeserialize,
StructureBuilder, the *Seed types, and signature::{Child, Fields} are in zbus::wire.
Errors
There is one error type now. zvariant::Error, zbus_names::Error and the zbus::Error::Variant
and zbus::Error::Names variants that wrapped them are gone: their variants, such as
IncorrectType, SignatureMismatch and InvalidName, are variants of zbus::Error itself. ?
keeps working. Besides that:
zvariant::Error::Message(s), what serde’sError::customproduced, isError::Failure(s).Error::InputOutputdisplays asI/O error: <the io::Error>, with a prefix zvariant’s lacked.Error::MissingFramingOffsetandError::IncompatibleFormatare gone with GVariant.Error::Connectionholds aBox<Address>.Error::MethodError,Error::FDO,Error::Connectionand the newError::DBusonly exist withcomms.- The
Invalid*Namevariants thatzbus_names4.1 deprecated are gone;Error::InvalidNameis what you get. zbus::Errorno longer implementsFrom<hex::FromHexError>.zbus_xml::Error’sVariantandNamevariants are now a singleZbus(zbus::Error)variant.- An error type of yours that implements
Fromfor bothzbus::Errorandzvariant::Errornow has the same impl twice (E0119). Delete the second one.
D-Bus error replies changed too:
- A reply whose name is one that
fdo::Errordefines is now anError::FDOrather than anError::MethodError, so match onfdo::Error::ServiceUnknownand the like instead of comparing names. - The new
Error::DBusholds a D-Bus error of any type. It is how the<property>_changedmethod that#[interface]generates reports a failing getter’s error, anfdo::Errorincluded, which 5.x reported asError::FDO.fdo::Error::from(error), orMyError::from(error)for a#[derive(DBusError)]type with a#[zbus(error)]variant, gets the typed error back.
What the compatibility module covers
zbus::zvariant warns on use zbus::zvariant; and on every type alias, function and constant it
contains, such as zbus::zvariant::Value. It can’t warn on what it re-exports, such as the
traits, the derives and the macros. Those keep resolving silently until 7.0, so search for
zvariant rather than relying on the warnings. zbus::names::Error and zbus::names::Result are
deprecated aliases too.
It does not cover:
- Matching the removed
Error::VariantandError::Namesvariants. - Implementing a trait for both
zbus::Errorandzbus::zvariant::Error, now the same type. - Constructing
DynamicTupleandOwnedStructurethrough the module, since a type alias is not a constructor (E0423). Usezbus::DynamicTuple(..)andzbus::OwnedStructure(..). - Matching on
Value::Value, which is nowVariant::Variant. - Naming the old crates, as in
extern crate zvariant;or#[zbus(crate = "zvariant")].
Other changes in 6.0
Value is now Variant
The type that holds a value of any D-Bus type is now named after the D-Bus VARIANT type, which is
what it encodes as. The encoding does not change:
| zbus 5 / zvariant 5 | zbus 6 |
|---|---|
Value | Variant |
OwnedValue | OwnedVariant |
Value::Value, a variant holding a variant | Variant::Variant |
#[derive(Value)], #[derive(OwnedValue)] | #[derive(Variant)], #[derive(OwnedVariant)] |
as_value | as_variant |
Variant::Value remains as a deprecated function until 7.0, so Value::Value(inner) keeps
compiling as an expression, but a pattern has to use Variant::Variant.
Builder setters defer their errors to build()
Builder setters and constructors that convert a TryInto argument (a name, a path, a GUID, an
address) return Self instead of Result<Self>, and build() returns the first error any of them
hit. Remove the ? after:
- the setters of
message::Builder,proxy::Builder,connection::Builderandmatch_rule::Builder; Message::method_call,Message::signal,Message::errorandMessage::method_return;connection::Builder::session,system,ibus,addressandauthenticated_socket.
match_rule::Builder::build(), on the other hand, used to be infallible and needs a ? now.
Property methods use Serde traits
Properties are converted through Serde and Type, like method arguments, rather than through
Variant conversions. Client-side getters need DeserializeOwned + Type and setters
Serialize + Type; service-side getters need Serialize + Type and setters
Deserialize + Type. A #[proxy] property getter has to return an owned type.
Check the #[serde(...)] attributes of your property types, since they now decide the wire format.
In particular, a derived Serialize encodes a unit enum as its variant index, not its
discriminant; use serde_repr where the discriminants are the D-Bus values. A property of the
wrong type fails with Error::SignatureMismatch rather than Error::IncorrectType.
Builder::runtime replaces internal_executor
Builder::internal_executor, Connection::executor() and the Executor and Task types are
gone. A connection runs on Tokio when the tokio feature is on and a Tokio runtime is current,
otherwise on zruntime (the default-rt feature), unless you pass an implementation of
runtime::traits::Runtime to Builder::runtime. A build with neither
default-rt nor tokio has to pass one, or building a connection returns Error::Unsupported.
A connection’s MessageStreams now end once it is closed.
The blocking API is gone
zbus::blocking, the *ProxyBlocking types and the blocking-api feature have no equivalent in
6.0. Drive the async API with zbus::block_on instead, ideally once around the whole program:
// 5.x
let connection = zbus::blocking::Connection::session()?;
let proxy = FooProxyBlocking::new(&connection)?;
let answer = proxy.bar()?;
// 6.0
let answer = zbus::block_on(async {
let connection = zbus::Connection::session().await?;
let proxy = FooProxy::new(&connection).await?;
proxy.bar().await
})?;
A blocking iterator becomes the stream it wrapped, driven with StreamExt::next. With the tokio
feature, zbus::block_on uses a single-threaded runtime, so a connection’s work only runs inside
it; the tokio-multithread feature gives you the multi-threaded runtime 5.x used. See
the FAQ for more.
The gen_blocking, blocking_name, blocking_object and gen_async attributes of #[proxy] are
gone, and async_object is now spelled proxy_type_name.
The flag types are bitflags types
message::Flags, proxy::MethodFlags and fdo::RequestNameFlags were enumflags2 enums, used as
BitFlags<Flags> and so on. They are bitflags types now, used without the BitFlags wrapper,
and their flags are associated constants: RequestNameFlags::DoNotQueue is
RequestNameFlags::DO_NOT_QUEUE, and so on.
A single flag is a set already, so drop the .into() that turned it into a BitFlags. from_bits
returns an Option, and the types implement neither Display nor TryFrom of their integer.
Decoding now keeps unknown bits instead of failing.
The enumflags2 feature is gone
zbus no longer implements its traits for enumflags2::BitFlags<F>, and the orphan rule keeps you
from doing so yourself. Make your set of flags a newtype over the integer instead, with the flags
generated by the bitflags crate:
use serde::{Deserialize, Serialize};
use zbus::{OwnedVariant, Type, Variant};
// Was:
//
// #[enumflags2::bitflags]
// #[repr(u32)]
// #[derive(Clone, Copy, Debug, PartialEq, Eq)]
// pub enum Permission {
// Read = 0x1,
// Write = 0x2,
// }
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Hash,
Serialize, Deserialize, Type, Variant, OwnedVariant,
)]
pub struct Permissions(u32);
bitflags::bitflags! {
impl Permissions: u32 {
const READ = 0x1;
const WRITE = 0x2;
}
}
It encodes as the u32 it wraps, as BitFlags<Permission> did. If your enum had a
#[bitflags(default = ...)] attribute, implement Default by hand. To use the type as a
dictionary key or to have its Debug name the flags, see the FAQ.
Dictionary keys have to be basic types
The key type of a HashMap or BTreeMap has to implement Basic now, as D-Bus only allows basic
types as keys. A map with any other key, such as a Vec<u8>, had an invalid signature and could not
have worked; send a Vec<(K, V)> instead. A newtype around a basic type did work, and needs a
Basic implementation now:
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use zbus::{Basic, Type};
#[derive(Deserialize, Serialize, Type, PartialEq, Eq, Hash)]
struct Key(u32);
impl Basic for Key {
const SIGNATURE_CHAR: char = u32::SIGNATURE_CHAR;
const SIGNATURE_STR: &'static str = u32::SIGNATURE_STR;
}
assert_eq!(HashMap::<Key, u8>::SIGNATURE, "a{uy}");
zbus_xml borrows from the document and keeps its order
Node::try_from(&str) borrows strings from the document where it can. Annotation, Arg and the
Telepathy types (TypeDef, SimpleType, Enum, EnumValue, Struct, Member, Mapping)
gained a lifetime parameter as a result. Use into_owned() to keep a tree past its document.
Interface::methods(), signals(), properties(), annotations() and telepathy_types() return
iterators rather than slices: .len() becomes .count(), [0] and .first() become .next().
The new Interface::children() yields the methods, signals, properties and annotations together,
in document order. That is also the order that Node::to_writer writes, PartialEq compares and
zbus_xmlgen generates code in now.
The tree types no longer implement Serialize and Deserialize, and zbus_xml::Signature is
replaced by zbus::Signature: drop the .inner() and .into_inner() calls on what Arg::ty and
Property::ty return.
A renamed zbus dependency needs the crate attribute
The macros no longer find a renamed zbus dependency, or one reached through a re-export of
another crate, on their own. Pass its name with the crate attribute: crate = "my_zbus" on
#[proxy], #[interface] and #[derive(DBusError)], and crate = "my_zbus::wire" on the wire
derives. Alternatively, put extern crate my_zbus as zbus; in your crate root, which also covers
signature!.
The APIs deprecated in 5.x are removed
All of these but #[zbus(signal_context)] warned in 5.19:
object_server::SignalContextisSignalEmitter,InterfaceRef::signal_context()issignal_emitter()and#[zbus(signal_context)]is#[zbus(signal_emitter)].Connection::peer_credentials()ispeer_creds(), which returns an&Arc<ConnectionCredentials>rather than an ownedConnectionCredentials.zbus::AuthMechanismiszbus::connection::AuthMechanism.DeserializeValueandSerializeValuearezbus::as_variant::DeserializeandSerialize.zbus_xmlgen::write_interfacesandGenTraitareCodeGenerator::file_codeandinterface_code. The generator only formats the code afterwith_format(true).zbus_xml::Error::QuickXmlandQuickXmlSerare gone; match onError::XmlorError::Io.object_server::DispatchResultis what 5.x calledDispatchResult2, which is now a deprecated alias. It only matters for a hand-writtenInterfaceimplementation: theAsyncfuture returns aResult<(), BoxDBusError>, andInterface::getandset_mutfail with aBoxDBusError.
Smaller changes
- Stream constructors take an owned socket.
Builder::unix_streamtakes astd::os::unix::net::UnixStream(uds_windows::UnixStreamon Windows) andBuilder::tcp_streamastd::net::TcpStream. Convert a Tokio stream withinto_std()and anasync_io::Async<T>withinto_inner();Builder::socketdoesn’t accept those types anymore either. On Windows, a Tokio connection can’t use unix sockets. - The encoding context has no format.
Context::new(Format::DBus, LE, 0)isContext::new(LE, 0),Signature::alignment(format)isalignment_dbus()andBasic::alignment(format)isalignment(). StructureSeedhas no lifetime parameter.PropertiesProxy::settakes a&Variant.#[interface]methods and property setters accept references to any deserializable type, such as&Variant<'_>, and to any slice of one, such as&[u32], as arguments now.DBusProxy::start_service_by_namereturns aStartServiceReplyinstead of au32.- Name types validate when converted from a
Variant, failing withError::InvalidName. Optional<T>compares with the null value before converting, so the empty string reads back asNonefor the name types. These conversions now needT::NoneType: Into<Variant>.FilePathconverts to std types withTryFrom, which fails off unix for paths that are not valid UTF-8.FilePath::as_c_str()returns the raw bytes.- Queue sizes are
NonZeroUsizeinmax_queued,set_max_queuedandfor_match_rule. ObjectServer::atreturnsResult<Option<I>>:Somehands the interface back if the path already had one of that name.Body::deserialize_uncheckedisBody::deserialize_ignore_signature.proxy::Builder::buildrequiresproxy::Defaults.AsyncDropis declared withoutasync-trait, so drop#[async_trait]from your implementations.Connection::monitor_activityandResponseDispatchNotifier::newreturn zbus types rather thanevent_listener::EventListener. Await them.- The xdg-dbus-proxy workarounds are gone. Flatpak apps need xdg-dbus-proxy 0.1.6 or newer.
A stale zvariant in the dependency graph
If another crate in your tree still depends on zvariant 5, its Type, Value and Signature are
unrelated to zbus’s, which leads to errors such as “expected zbus::Variant, found
zvariant::Value”. Find the crate with cargo tree -i zvariant, and upgrade it or keep its types
on its side.
GVariant
The gvariant and ostree-tests features and everything behind them moved to the zgvariant
crate, where Context::new_gvariant(LE, 0) is zgvariant::serialized::Context::new(LE, 0).
zgvariant 2 shares its Signature type with zbus 6; zgvariant 1 does not, so pass a signature
between the two as a string.
FAQ
- How to use a struct as a dictionary?
- How do I use zbus from synchronous code?
- Why do async tokio API calls from interface methods not work?
- I’m experiencing hangs, what could be wrong?
- Why aren’t property values updating for my service that doesn’t notify changes?
- How do I use
Option<T>with zbus? - How do enums work?
- How do I use bit flags?
- How do I connect over VSOCK?
How to use a struct as a dictionary?
Since the use of a dictionary, specifically one with strings as keys and variants as value (i-e
a{sv}) is very common in the D-Bus world and use of HashMaps isn’t as convenient and type-safe as
a struct, you might find yourself wanting to use a struct as a dictionary.
It’s possible to do so, using either of the following methods:
- Using the
SerializeDictandDeserializeDictderive macros fromzbus. This is the best option for simple cases. - Using the
SerializeandDeserializederive macros from theserdecrate. This is the best option for more complex cases, where you need more fine-grained control over the serialization and/or deserialization process.
Here is a simple example using SerializeDict and DeserializeDict:
use zbus::{
proxy, interface, fdo::Result,
DeserializeDict, SerializeDict, Type,
};
#[derive(DeserializeDict, SerializeDict, Type)]
// `Type` treats `dict` is an alias for `a{sv}`.
#[zbus(signature = "dict")]
pub struct Dictionary {
field1: u16,
#[zbus(rename = "another-name")]
field2: i64,
optional_field: Option<String>,
}
#[proxy(
interface = "org.zbus.DictionaryGiver",
default_path = "/org/zbus/DictionaryGiver",
default_service = "org.zbus.DictionaryGiver",
)]
trait DictionaryGiver {
fn give_me(&self) -> Result<Dictionary>;
}
struct DictionaryGiverInterface;
#[interface(interface = "org.zbus.DictionaryGiver")]
impl DictionaryGiverInterface {
fn give_me(&self) -> Result<Dictionary> {
Ok(Dictionary {
field1: 1,
field2: 4,
optional_field: Some(String::from("blah")),
})
}
}
Now let’s say you want to grab all the extra entries that are not explicitly defined in the
struct. You can not do that with DeserializeDict but you can with serde::Deserialize:
use std::collections::HashMap;
use zbus::{as_variant::{self, optional}, OwnedVariant, Type};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Type)]
#[zbus(signature = "dict")]
pub struct Dictionary {
#[serde(with = "as_variant")]
field1: u16,
#[serde(rename = "another-name", with = "as_variant")]
field2: i64,
#[serde(
with = "optional",
skip_serializing_if = "Option::is_none",
default,
)]
optional_field: Option<String>,
#[serde(flatten)]
the_rest: HashMap<String, OwnedVariant>,
}
Since the fields have to be transformed from/into zbus::Variant, make sure to use the
with attribute with the appropriate helper module from the zbus::as_variant module.
Moreover, since D-Bus does not have a concept of nullable types, it’s important to ensure that
skip_serializing_if and default attributes are used for optional fields. Fortunately, you can
make use of the default container attribute if your struct can implemented the Default trait:
use zbus::{as_variant::{self, optional}, Type};
use serde::{Deserialize, Serialize};
#[derive(Default, Deserialize, Serialize, Type)]
#[zbus(signature = "dict")]
#[serde(default)]
pub struct Dictionary {
#[serde(with = "as_variant")]
field1: u16,
#[serde(with = "optional", skip_serializing_if = "Option::is_none")]
optional_field1: Option<i64>,
#[serde(with = "optional", skip_serializing_if = "Option::is_none")]
optional_field2: Option<String>,
}
Nested dictionaries
D-Bus interfaces frequently return dicts whose values are themselves dicts — for example
a{sa{sv}} (the body of org.freedesktop.DBus.ObjectManager.GetManagedObjects and similar APIs).
You can mirror that shape by nesting one *Dict struct inside another. The outer struct’s
signature advertises the nested form; its fields name the outer keys and have an inner *Dict
struct as their type:
use zbus::{DeserializeDict, SerializeDict, Type};
#[derive(DeserializeDict, SerializeDict, Type, PartialEq, Debug, Default, Clone)]
#[zbus(signature = "a{sv}", rename_all = "PascalCase")]
struct Adapter {
address: Option<String>,
name: Option<String>,
powered: bool,
}
#[derive(DeserializeDict, SerializeDict, Type, PartialEq, Debug, Default, Clone)]
#[zbus(signature = "a{sv}", rename_all = "PascalCase")]
struct Media {
supported_features: Vec<String>,
#[zbus(rename = "SupportedUUIDs")]
supported_uuids: Vec<String>,
}
#[derive(DeserializeDict, SerializeDict, Type, PartialEq, Debug, Default)]
#[zbus(signature = "a{sa{sv}}")]
struct Interfaces {
#[zbus(rename = "org.bluez.Adapter1")]
adapter: Option<Adapter>,
#[zbus(rename = "org.bluez.Media1")]
media: Media,
}
The same pattern works for a{oa{sv}} (object-path keys) and composes to deeper nesting — the
outer derive defers each field to its own Serialize/Deserialize impl rather than wrapping it
as a variant.
How do I use zbus from synchronous code?
zbus’s API is async, but a program does not need an async runtime of its own to use it.
zbus::block_on runs a future to completion on the calling thread, so put the D-Bus code
inside one call:
use zbus::{Connection, Result, proxy};
#[proxy(
interface = "org.freedesktop.systemd1.Manager",
default_service = "org.freedesktop.systemd1",
default_path = "/org/freedesktop/systemd1"
)]
trait SystemdManager {
#[zbus(property)]
fn log_level(&self) -> Result<String>;
}
fn main() -> Result<()> {
zbus::block_on(async {
let connection = Connection::session().await?;
let manager = SystemdManagerProxy::new(&connection).await?;
println!("LogLevel: {}", manager.log_level().await?);
Ok(())
})
}
Everything shown in the other chapters works the same way inside that future, and the program
depends on zbus alone, plus futures-util if it reads signals or property changes from a
stream. With the default-rt feature, on by default, the same thread also runs zruntime’s
scheduler and I/O reactor in between polls of the future, and a program that runs block_on on
several threads drives their connections in parallel; with the tokio feature, Tokio’s runtime
runs them instead. A handful of blocking system calls — a DNS lookup, a nonce-file read, a
peer-credential lookup — run on a short-lived worker thread of their own, so they do not tie up
the connection’s thread.
Two rules follow from the connection’s work running on the calling thread:
- The future must not block the thread waiting for the connection. A synchronous wait for a reply, or a busy loop until a signal arrives, never finishes, because the work it waits for can only run in between polls of the future.
zbus::block_onmust not be called from inside a task zbus is running, such as a method of an interface the connection serves, or a future inside anotherzbus::block_on. Such a call panics, because it would be waiting for the very thread it is on. Nor should it be called from a task of another runtime; the function’s documentation has the details.
Why do async tokio API calls from interface methods not work?
Many of the tokio (and tokio-based) APIs assume the tokio runtime to be driving the async
machinery, and by default a connection’s tasks and I/O run on zruntime, on the thread inside
zbus::block_on (the zruntime backend), which is not a tokio runtime. So it’s not
possible to use these APIs from interface methods.
Not to worry, though! You can enable tight integration between tokio and zbus by enabling tokio
feature:
# Sample Cargo.toml snippet.
[dependencies]
zbus = { version = "6", features = ["tokio"] }
With both features enabled, the backend is chosen once per connection, when it is built: Tokio when
a Tokio runtime is current on the thread that builds it, zruntime otherwise. This keeps the
features additive, so an application that relies on zruntime keeps working even when
another crate in the workspace enables zbus’s tokio feature. To leave the zruntime backend out
of the build, disable default features and list what you use instead, e.g.
default-features = false, features = ["tokio", "proxy", "service"]. The zruntime crate stays
in the dependency tree for the Event and broadcast channels zbus’s own types build on, but none
of its runtime is built.
This per-connection selection only matters for a program that has an async runtime of its own. A
program without one uses zbus::block_on instead. Without the tokio feature, that call and the
connection built inside it use zruntime; with it, the call runs inside a Tokio
runtime, so a connection built inside it always uses Tokio. That runtime is single-threaded, so
the connection’s work only runs while a thread is inside zbus::block_on; the
tokio-multithread feature (which enables tokio) makes it a multi-threaded runtime instead.
Note that zbus::block_on must not be called from inside a task zbus is running, such as an
interface method: it panics there.
Note: On Windows, a connection that ends up on Tokio cannot use a Unix domain socket, even
when the default-rt feature is also on; give it a TCP or autolaunch: address instead, or
build it from a thread with no Tokio runtime current so it picks zruntime. See the
corresponding tokio issue on GitHub.
I’m experiencing hangs, what could be wrong?
There are typically two reasons this can happen with zbus:
1. A interface method that takes a &mut self argument is taking too long
Simply put, this is because of one of the primary rules of Rust: while a mutable reference to a resource exists, no other references to that same resource can exist at the same time. This means that before the method in question returns, all other method calls on the providing interface will have to wait in line.
A typical solution here is use of interior mutability or launching tasks to do the actual work
combined with signals to report the progress of the work to clients. Both of these solutions
involve converting the methods in question to take &self argument instead of &mut self.
2. A stream (e.g SignalStream) is not being continuously polled
Please consult MessageStream documentation for details.
Why aren’t property values updating for my service that doesn’t notify changes?
A common issue might arise when using a zbus proxy is that your proxy’s property values aren’t updating. This is due to zbus’ default caching policy, which updates the value of a property only when a change is signaled, primarily to minimize latency and optimize client request performance. By default, if your service does not emit change notifications, the property values will not update accordingly.
However, you can disabling caching for specific properties:
-
Add the
#[zbus(property(emits_changed_signal = "false"))]annotation to the property for which you desire to disable caching on. For more information about all the possible values foremits_changed_signalrefer toproxydocumentation. -
Use
proxy::Builderto build your proxy instance and useproxy::Builder::uncached_propertiesmethod to list all properties you wish to disable caching for. -
In order to disable caching for either type of proxy use the
proxy::Builder::cache_propertiesmethod.
How do I use Option<T> with zbus?
While Option<T> is a very commonly used type in Rust, there is unfortunately no concept of a
nullable-type in the D-Bus protocol. However, there are two ways to simulate it:
1. Designation of a special value as None
This is the simplest way to simulate Option<T>. Typically the
default value for the type in question is a good choice. For example the empty string ("") is
often used as None for strings and string-based types. Note however that this solution can not
work for all types, for example bool.
Since this is the most widely used solution in the D-Bus world and is even used by the D-Bus
standard interfaces, zbus::wire provides a custom type for this, Optional<T> that
makes it super easy to simulate a nullable type, especially if the contained type implements the
Default trait.
2. Encoding as an array (a?)
The idea here is to represent None case with 0 elements (empty array) and the Some case with 1
element. zbus provides an option-as-array Cargo feature, which when enabled, allows the
(de)serialization of Option<T>. Unlike the previous solution, this solution can be used with
all types. However, it does come with some caveats and limitations:
- Since the D-Bus type signature does not provide any hints about the array being in fact a
nullable type, this can be confusing for users of generic tools like
d-feet. It is therefore highly recommended that service authors document each use ofOption<T>in their D-Bus interface documentation. - Currently it is not possible to use
Option<T>forinterfaceandproxyproperty methods. - Both the sender and receiver must agree on use of this encoding. If the sender sends
T, the receiver will not be able to decode it successfully asOption<T>and vice versa. - While
zbus::Variantcan be converted intoOption<T>, the reverse is currently not possible.
Due to these limitations, option-as-array feature is not enabled by default and must be explicitly
enabled.
Note: We hope to be able to remove #2 and #4, once specialization lands in stable Rust.
How do enums work?
By default, zbus::wire encodes an unit-type enum as a u32, denoting the variant index. Other
enums are encoded as a structure whose first field is the variant index and the second one are the
variant’s field(s). The only caveat here is that all variants must have the same number and types
of fields. Names of fields don’t matter though. You can make use of Variant or OwnedVariant
if you want to encode different data in different fields. Here is a simple example:
use zbus::{Type, wire::{serialized::Context, to_bytes, LE}};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Type, PartialEq, Debug)]
enum Enum<'s> {
Variant1 { field1: u16, field2: i64, field3: &'s str },
Variant2(u16, i64, &'s str),
Variant3 { f1: u16, f2: i64, f3: &'s str },
}
let e = Enum::Variant3 {
f1: 42,
f2: i64::MAX,
f3: "hello",
};
let ctxt = Context::new(LE, 0);
let encoded = to_bytes(ctxt, &e).unwrap();
let decoded: Enum = encoded.deserialize().unwrap().0;
assert_eq!(decoded, e);
Enum encoding can be adjusted by using the serde_repr crate and by annotating the
representation of the enum with repr:
use zbus::{Type, wire::{serialized::Context, to_bytes, LE}};
use serde_repr::{Serialize_repr, Deserialize_repr};
#[derive(Deserialize_repr, Serialize_repr, Type, PartialEq, Debug)]
#[repr(u8)]
enum UnitEnum {
Variant1,
Variant2,
Variant3,
}
let ctxt = Context::new(LE, 0);
let encoded = to_bytes(ctxt, &UnitEnum::Variant2).unwrap();
let e: UnitEnum = encoded.deserialize().unwrap().0;
assert_eq!(e, UnitEnum::Variant2);
Unit enums can also be (de)serialized as strings:
use zbus::{Type, wire::{serialized::Context, to_bytes, LE}};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Type, PartialEq, Debug)]
#[zbus(signature = "s")]
enum StrEnum {
Variant1,
Variant2,
Variant3,
// Catch-all that preserves the raw value for any variant we don't know about.
#[serde(untagged)]
Other(String),
}
let ctxt = Context::new(LE, 0);
let encoded = to_bytes(ctxt, &StrEnum::Variant2).unwrap();
let e: StrEnum = encoded.deserialize().unwrap().0;
assert_eq!(e, StrEnum::Variant2);
let s: &str = encoded.deserialize().unwrap().0;
assert_eq!(s, "Variant2");
How do I use bit flags?
D-Bus has no type for bit flags: a set of flags goes over the bus as the unsigned integer with
their bits set, usually a u32. zbus needs no special support for them either. Declare the set as
a newtype over that integer, derive zbus’s traits on it as on any other type of yours, and have
the bitflags crate generate the flags and the methods and operators to work with them, through
the impl form of its bitflags! macro:
use serde::{Deserialize, Serialize};
use zbus::{
Basic, OwnedVariant, Type, Variant, proxy,
wire::{serialized::Context, to_bytes, LE},
};
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Hash,
Serialize, Deserialize, Type, Variant, OwnedVariant,
)]
pub struct Permissions(u32);
bitflags::bitflags! {
impl Permissions: u32 {
const READ = 0x1;
const WRITE = 0x2;
const EXECUTE = 0x4;
}
}
// Only needed to use the flags as the key of a dictionary.
impl Basic for Permissions {
const SIGNATURE_CHAR: char = u32::SIGNATURE_CHAR;
const SIGNATURE_STR: &'static str = u32::SIGNATURE_STR;
}
#[proxy(
interface = "org.zbus.Files",
default_path = "/org/zbus/Files",
default_service = "org.zbus.Files",
)]
trait Files {
fn open(&self, path: &str, permissions: Permissions) -> zbus::Result<()>;
#[zbus(property)]
fn default_permissions(&self) -> zbus::Result<Permissions>;
}
// On the bus, the flags are the integer.
assert_eq!(Permissions::SIGNATURE, "u");
let ctxt = Context::new(LE, 0);
let flags = Permissions::READ | Permissions::WRITE;
let encoded = to_bytes(ctxt, &flags).unwrap();
assert_eq!(encoded.bytes(), to_bytes(ctxt, &0x3u32).unwrap().bytes());
let decoded: Permissions = encoded.deserialize().unwrap().0;
assert_eq!(decoded, flags);
Type gives the flags the signature of the integer, and the serde derives encode them as it.
Variant and OwnedVariant convert them to and from a Variant holding the integer, which a
property needs, since property values travel as variants. Default makes the empty set the null
value of an Optional<T>.
A few things to be aware of:
- Decoding keeps bits that none of the flags define, so a peer can set flags your code does not
know yet without breaking it. Where you would rather refuse them,
Permissions::from_bits(flags.bits())returnsNonefor such a set. - The derived
Debugprints the integer (Permissions(3)). To print the flags’ names instead (READ | WRITE), implementDebugwithbitflags::parser::to_writer; it writes nothing for an empty set. - A flags type made with the
structform ofbitflags!can go over the bus too, but without theVariantconversions: enable thebitflagscrate’sserdefeature, deriveSerialize,DeserializeandTypeinside the macro, and giveTypethe signature with#[zbus(signature = "u")], as the struct wraps an internal type the macro generates, which implements none of zbus’s traits.
How do I connect over VSOCK?
zbus has no transport for VSOCK, which links a virtual machine to its host, as D-Bus defines no
address for it. A connection still runs over a VSOCK stream that you open yourself, such as the
VsockStream of the tokio-vsock crate: implement Socket for it and hand it to
connection::Builder::socket. The Socket documentation has an implementation for any stream
of Tokio’s, VsockStream among them. As a VSOCK peer has no credentials to check, that
implementation asks for the ANONYMOUS authentication mechanism, which a bus at the other end has
to allow.
Contributors
Here is a list of the contributors who have helped improving zbus. Big shout-out to them!
If you feel you’re missing from this list, feel free to add yourself in a PR.