FAQ
- Should I use zlink or zbus?
- Where are the signals and properties?
- Why do proxy methods take
&mut self? How do I make concurrent calls? - Why can’t I
tokio::spawn(server.run())? - How do I use
Option<T>? - Can I pass file descriptors?
- Why doesn’t my proxy method get a
chain_variant? - How do I try this against real services?
- Is there a blocking (non-async) API?
Should I use zlink or zbus?
Both are maintained by the same project, so this is an honest referee’s answer: it depends on which protocol you need to speak, and that’s usually decided for you.
- Talking to an existing D-Bus service (desktop APIs, NetworkManager, BlueZ, …) → zbus.
- Talking to an existing Varlink service (a growing list of systemd components, …) → zlink.
- Designing a new system service or a point-to-point IPC link and free to choose? Varlink and zlink make a compelling default: no broker dependency, a simpler protocol, and the efficiency benefits that come with it. If you need what a bus genuinely provides — broadcast signals to many unrelated listeners, discovery/enumeration of running services, bus-enforced policy — D-Bus still earns its keep.
Where are the signals and properties?
Varlink doesn’t have them as separate concepts — both patterns are served by
streaming (more) calls. A property is just a method returning the
value; a “property changed” signal or any other notification is a more call streaming updates to
each interested client on its own connection. There is no broadcast: nothing is emitted for
nobody, and subscribers state their interest explicitly. notified::State gives services the
monitor-a-value pattern out of the box.
Why do proxy methods take &mut self? How do I make concurrent calls?
Because a Connection is exclusively owned — mutation via &mut instead of internal locks is a
core design decision (exterior mutability). A single connection
is a strictly ordered request/reply pipeline, so concurrent calls over one connection wouldn’t buy
what you might hope anyway.
To make concurrent calls, do the Varlink-idiomatic thing: open one connection per concurrent conversation. Connections are cheap — this is not the D-Bus world where connections are a scarce resource. For many calls to the same service, consider pipelining them over one connection instead: often faster than true concurrency, with none of the coordination.
Why can’t I tokio::spawn(server.run())?
A rustc bug prevents the future returned by Server::run from being Send. Until
that’s fixed, drive the server with select! alongside your other work, or give it a
LocalSet/LocalRuntime (or a plain dedicated thread). See
the service chapter.
How do I use Option<T>?
Just use it. Varlink has nullable types (?type in the IDL), and
serde’s Option<T> handling maps straight onto them — no option-as-array workarounds or
sentinel values as in the D-Bus world.
Can I pass file descriptors?
Yes — over Unix domain sockets, methods can send and receive OwnedFds alongside the JSON payload
(see the client chapter and the fds parameters throughout
the low-level API). Note that the Varlink IDL has no file-descriptor type, so the convention is for
JSON parameters to carry indexes into the accompanying descriptor array. This zlink extension
interoperates with systemd’s use of the same convention; it requires std.
Why doesn’t my proxy method get a chain_ variant?
Chain methods are only generated for methods whose reply and error types are owned
(DeserializeOwned). Borrowed reply types can’t be used in chains because the read buffer is
reused from one reply to the next — see the pipelining chapter.
Input parameters may borrow either way.
How do I try this against real services?
Recent systemd versions ship a growing set of Varlink services. Look around in /run for sockets
named like interfaces (e.g. /run/systemd/resolve/io.systemd.Resolve,
/run/systemd/io.systemd.Hostname), point varlinkctl
at them, and then do the same from Rust with a proxy. The
resolved and varlink-inspect examples in the repository
are ready-made starting points.
Is there a blocking (non-async) API?
No. zlink is async-first by design — IPC is I/O-bound, and the APIs (streaming replies, pipelining,
serving many connections from one task) lean heavily on async. If you’re in a synchronous program,
you can wrap calls with your runtime’s block_on (e.g. tokio’s current-thread runtime or
smol::block_on).