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.
zbus project provides two crates:
zvariant
D-Bus defines a marshalling format for its messages. The zvariant crate provides a serde-based API to serialize/deserialize Rust data types to/from this format. Outside of D-Bus context, a modified form of this format, GVariant is very commonly used for efficient storage of arbitrary data and is also supported by this crate.
zbus
The zbus crate provides the main 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.
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.
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)]
{
#[cfg(not(feature = "tokio"))]
use std::os::unix::net::UnixStream;
#[cfg(feature = "tokio")]
use tokio::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(())
}
Note: the p2p
and server
methods of connection::Builder
are only available when p2p
cargo feature of zbus
is enabled.
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 [busctl
]1:
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}i
means 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::{zvariant::Value, 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, &Value>::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
and
TProxyBlocking
(see chapter on “blocking” for more information on that) 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::{zvariant::Value, 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, &Value<'_>>, 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
and TProxyBlocking
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 simple transient cases like the one above, you may find the blocking API very convenient to use.
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::{zvariant::OwnedObjectPath, proxy, Connection}; fn main() { async_io::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::{zvariant::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
...
Trait-bounds for property values
If you use custom types for property values, you might get a compile error for missing
TryFrom<zvariant::Value<'_>>
and/or TryFrom<OwnedValue>
implementations. This is because
properties are always sent as Variants on the bus, so you need to implement these conversions for
your custom types.
Not to worry though, the zvariant
crate provides a Value
and OwnedValue
derive macro to
implement these conversions for you.
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 busctl
1 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, zvariant::Value<'_>>,
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 (using BitFlags
, 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::{zvariant::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!
busctl
is part of systemd
.
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 a way to
synchronize with the interface handlers from outside, thanks to the event_listener
crate
(this is just one of the many ways).
use zbus::{object_server::SignalEmitter, connection::Builder, interface, fdo, Result}; use event_listener::{Event, Listener}; 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_listener::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.wait(); // 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 - - -
Trait-bounds for property values
If you use custom types for property values, you might get a compile error for missing
TryFrom<zvariant::Value<'_>>
and/or TryFrom<OwnedValue>
implementations. This is because
properties are always sent as Variants on the bus, so you need to implement these conversions for
your custom types.
Not to worry though, the zvariant
crate provides a Value
and OwnedValue
derive macro to
implement these conversions for you.
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 methods may also return errors, but they must be zbus::fdo::Error
. Most often you’ll
want to use zbus::fdo::Error::UnknownProperty
variant.
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 `gen_blocking) here. All the attributes that are common between // `proxy` and `interface` macros (e.g `name`) are automtically forwarded to // the `proxy` macro. proxy( gen_blocking = false, 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 trait bounds of the
proxy
macro methods’ arguments and return value, now also apply to theinterface
methods. For example, when only generating the service-side code, the method return values need to implementserde::Serialize
but when generating the client-side proxy code, the method return values need to implementserde::DeserializeOwned
as well. - Reference types in return values of
interface
methods won’t work. As you may have noticed, unlike the previous examples thegreeter_name
method in the example above returns aString
instead of a&str
. This is because the methods in theproxy
macro do not support reference type to be returned from its methods. - Methods returning [
object_server::ResponseDispatchNotifier
] wrapper type will do the same for proxy as well.
Blocking API
While zbus API being primarily asynchronous (since 2.0) is a great thing, it could easily feel daunting for simple use cases. Not to worry! In the spirit of “ease” being a primary goal of zbus, it provides blocking wrapper types, under the blocking module.
Note: Use of the blocking API presented in this chapter in an async context will likely result
in panics and hangs. This is not a limitation of zbus but rather a
well-known general problem in the Rust async/await world. The blocking
crate,
async-std
and tokio
crates provide a easy way around this problem.
Note: Since zbus 5.0, blocking API can be disabled through the blocking-api
cargo feature. If
you use this API, make sure you are not unintentionally disabling it by disabling the default
features in your Cargo.toml
.
Establishing a connection
The only difference to that of asynchronous Connection
API is that you use
blocking::Connection
type instead. This type’s API is almost identical to that of Connection
,
except all its methods are blocking.
Client
Similar to blocking::Connection
, you use blocking::Proxy
type. Its constructors require
blocking::Connection
instead of Connection
. Moreover, proxy
macro generates a
blocking::Proxy
wrapper for you as well. Let’s convert the last example in the previous chapter,
to use the blocking connection and proxy:
#![allow(unused)] fn main() { use zbus::{blocking::Connection, zvariant::ObjectPath, proxy, Result}; #[proxy( default_service = "org.freedesktop.GeoClue2", interface = "org.freedesktop.GeoClue2.Manager", default_path = "/org/freedesktop/GeoClue2/Manager" )] trait Manager { #[zbus(object = "Client")] /// The method normally returns an `ObjectPath`. /// With the object attribute, we can make it return a `ClientProxy` directly. 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>; } let conn = Connection::system().unwrap(); let manager = ManagerProxyBlocking::new(&conn).unwrap(); let mut client = manager.get_client().unwrap(); // Gotta do this, sorry! client.set_desktop_id("org.freedesktop.zbus").unwrap(); let mut location_updated = client.receive_location_updated().unwrap(); client.start().unwrap(); // Wait for the signal. let signal = location_updated.next().unwrap(); let args = signal.args().unwrap(); let location = LocationProxyBlocking::builder(&conn) .path(args.new()) .unwrap() .build() .unwrap(); println!( "Latitude: {}\nLongitude: {}", location.latitude().unwrap(), location.longitude().unwrap(), ); }
As you can see, nothing changed in the proxy
usage here and the rest largely remained the
same as well. One difference that’s not obvious is that the blocking API for receiving signals,
implement std::iter::Iterator
trait instead of futures::stream::Stream
.
Watching for properties
That’s almost the same as receiving signals:
use zbus::{blocking::Connection, proxy, 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) -> zbus::Result<String>; } fn main() -> Result<()> { let connection = Connection::session()?; let proxy = SystemdManagerProxyBlocking::new(&connection)?; let v = proxy.receive_log_level_changed().next().unwrap(); println!("LogLevel changed: {:?}", v.get()); Ok(()) }
Server
Similarly here, you’d use [blocking::ObjectServer
] that is associated with every
blocking::Connection
instance. While there is no blocking version of Interface
,
interface
allows you to write non-async methods.
Note: Even though you can write non-async methods, these methods are still called from an async context. Therefore, you can not use blocking API in the method implementation directly. See note at the beginning of this chapter for details on why and a possible workaround.
use std::error::Error; use zbus::{blocking::connection, interface, fdo, object_server::SignalEmitter}; use event_listener::{Event, Listener}; struct Greeter { name: String, done: Event, } #[interface(name = "org.zbus.MyGreeter1")] impl Greeter { 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)] 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)] 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<'_>) -> zbus::Result<()>; } fn main() -> Result<(), Box<dyn Error>> { let greeter = Greeter { name: "GreeterName".to_string(), done: event_listener::Event::new(), }; let done_listener = greeter.done.listen(); let _handle = connection::Builder::session()? .name("org.zbus.MyGreeter")? .serve_at("/org/zbus/MyGreeter", greeter)? .build()?; done_listener.wait(); Ok(()) }
FAQ
- How to use a struct as a dictionary?
- 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 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.
zvariant
provides convenient macros for making this possible: SerializeDict
and
DeserializeDict
. You’ll also need to tell Type
macro to treat the type as a dictionary using
the signature
attribute. Here is a simple example:
use zbus::{
proxy, interface, fdo::Result,
zvariant::{DeserializeDict, SerializeDict, Type},
};
#[derive(DeserializeDict, SerializeDict, Type)]
// `Type` treats `dict` is an alias for `a{sv}`.
#[zvariant(signature = "dict")]
pub struct Dictionary {
field1: u16,
#[zvariant(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")),
})
}
}
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 since by default, zbus runs the ObjectServer
in its own internal runtime thread, it’s not
possible to use these APIs from interface methods. Moreover, by default zbus relies on async-io
crate to communicate with the bus, which uses its own thread.
Not to worry, though! You can enable tight integration between tokio and zbus by enabling tokio
feature:
# Sample Cargo.toml snippet.
[dependencies]
# Also disable the default `async-io` feature to avoid unused dependencies.
zbus = { version = "3", default-features = false, features = ["tokio"] }
Note: On Windows, the async-io
feature is currently required for UNIX domain socket support,
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_signal
refer toproxy
documentation. -
Use
proxy::Builder
to build your proxy instance and useproxy::Builder::uncached_properties
method 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_properites
method.
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, zvariant
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. zvariant
and zbus
provide 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>
forinterface
andproxy
property 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
zvariant::Value
can 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, zvariant
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 Value
or OwnedValue
if you want to encode different data in different fields. Here is a simple example:
use zbus::zvariant::{serialized::Context, to_bytes, Type, 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_value(),
f3: "hello",
};
let ctxt = Context::new_dbus(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::zvariant::{serialized::Context, to_bytes, Type, 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_dbus(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::zvariant::{serialized::Context, to_bytes, Type, LE};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Type, PartialEq, Debug)]
#[zvariant(signature = "s")]
enum StrEnum {
Variant1,
Variant2,
Variant3,
}
let ctxt = Context::new_dbus(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");
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.