Runic.Platform.Linux.DBus
0.11.0
dotnet add package Runic.Platform.Linux.DBus --version 0.11.0
NuGet\Install-Package Runic.Platform.Linux.DBus -Version 0.11.0
<PackageReference Include="Runic.Platform.Linux.DBus" Version="0.11.0" />
<PackageVersion Include="Runic.Platform.Linux.DBus" Version="0.11.0" />
<PackageReference Include="Runic.Platform.Linux.DBus" />
paket add Runic.Platform.Linux.DBus --version 0.11.0
#r "nuget: Runic.Platform.Linux.DBus, 0.11.0"
#:package Runic.Platform.Linux.DBus@0.11.0
#addin nuget:?package=Runic.Platform.Linux.DBus&version=0.11.0
#tool nuget:?package=Runic.Platform.Linux.DBus&version=0.11.0
Runic.Platform.Linux.DBus
D-Bus client and service API for Runic applications on Linux: connections to the session and system buses, typed values, calls, properties, signals, well-known names and exported objects. Many Linux desktop integrations are plain D-Bus services, such as UDisks2, UPower, NetworkManager and MPRIS media players; this package is how a Runic application talks to them, or offers its own.
Targets .NET 10 and NativeAOT; it uses no reflection. A source generator turns introspection XML into typed proxies and services. The wire protocol comes from Tmds.DBus.Protocol, which stays an implementation detail: every public type belongs to this package.
Connect
Register the application's shared connections and take IDBusConnections where
you need a bus. Each bus is connected on first use, shared by every caller, and
closed when the service provider is disposed; a connection that closed, for example
because the bus restarted, is replaced on the next request. Do not dispose a shared
connection.
services.AddRunicDBus();
public sealed class PowerStatus(IDBusConnections buses)
{
public async Task<bool?> OnBatteryAsync(CancellationToken cancellationToken)
{
if (await buses.GetAsync(DBusBus.System, cancellationToken) is not PlatformResult<DBusConnection>.Success(var bus))
return null; // No system bus, or not Linux.
var upower = new DBusProxy(bus, "org.freedesktop.UPower", "/org/freedesktop/UPower");
return (await upower.GetPropertyAsync("org.freedesktop.UPower", "OnBattery", cancellationToken)).GetBoolean();
}
}
DBusConnection.ConnectAsync(DBusBus.Session) creates a connection the caller owns
and disposes; ConnectToAddressAsync(address) connects to any D-Bus address, such
as a private bus in tests. Connecting returns PlatformResult<DBusConnection>:
Unavailable(BackendUnavailable) when the bus is not running or refuses the
connection, and Unavailable(ProviderNotConfigured) on Windows and macOS.
Values
DBusValue holds one value with its type signature. DBusValue.From(...) creates
basic values (From(42) is an i, From(42u) a u, From("text") an s), and
Variant, Array, Dictionary, Struct, StringArray and VariantDictionary
build containers. Read a value with the getter for its type, such as GetInt32(),
GetItems() or GetVariantDictionary(); another getter throws
InvalidOperationException. Values compare by signature and content.
IsDefault is true only for default(DBusValue), which holds nothing; an empty
container such as an as without items is not the default.
D-Bus does not stop a peer from sending a dictionary whose key repeats.
GetEntries() lists every entry in message order, and GetVariantDictionary()
and generated readers keep the last entry's value for a repeated key instead of
throwing, as GLib's GVariantDict and GDBusProxy and dbus-python do. Dictionary
and VariantDictionary still reject a repeated key, so this library does not send one.
An ay keeps its bytes as one block, however it was created or received, so a
large file or image costs about its length in memory. GetByteArray() returns
those bytes without copying them; GetItems() still lists them as y values.
| D-Bus | Create | Read |
|---|---|---|
y b n q i u x t d |
From(byte) … From(double) |
GetByte() … GetDouble() |
s o g |
From(string), From(DBusObjectPath), From(DBusSignature) |
GetString(), GetObjectPath(), GetTypeSignature() |
h |
UnixFd(SafeHandle); the caller keeps its handle |
GetUnixFd(); a received descriptor is a SafeFileHandle that belongs to the receiver |
v |
Variant(value) |
GetVariant() |
aT |
Array(elementSignature, items), StringArray, ObjectPathArray, ByteArray |
GetItems(), GetStringArray(), GetObjectPathArray(), GetByteArray() |
a{KV} |
Dictionary(key, value, entries), VariantDictionary for a{sv} |
GetEntries(), GetVariantDictionary() |
(...) |
Struct(fields) |
GetFields() |
Call, read properties and receive signals
A DBusProxy addresses one object of one service. CallAsync(interface, member, arguments) returns the reply's values; GetPropertyAsync, GetAllPropertiesAsync
and SetPropertyAsync use org.freedesktop.DBus.Properties. Set
DBusCallOptions.AllowInteractiveAuthorization when the service may ask the user
for a password, as UDisks2 does through Polkit.
A call waits for its reply until the bus or the service answers or its token is
cancelled. To bound the wait, set Timeout on the proxy, which covers every call
and property access through it (including generated proxies), or on a single
DBusMethodCall:
var upower = new DBusProxy(bus, "org.freedesktop.UPower", "/org/freedesktop/UPower") { Timeout = TimeSpan.FromSeconds(5) };
A call that times out throws DBusException with DBusException.NoReply, the
error the bus also sends when it gives up waiting, and ignores a later reply. The
service may still have carried out the call.
Signals and property changes arrive as IAsyncEnumerable streams, in the order the
connection received them. SubscribeAsync, SubscribePropertiesAsync and
SubscribeSignalAsync return a subscription that is already active, so read the
current state after subscribing and nothing is missed in between:
var player = new DBusProxy(bus, "org.mpris.MediaPlayer2.vlc", "/org/mpris/MediaPlayer2");
await using var changes = await player.SubscribePropertiesAsync("org.mpris.MediaPlayer2.Player", cancellationToken);
var status = (await player.GetPropertyAsync("org.mpris.MediaPlayer2.Player", "PlaybackStatus", cancellationToken)).GetString();
await foreach (var signal in changes.WithCancellation(cancellationToken))
if (DBusProxy.ReadPropertyChanges(signal) is { } change && change.Changed.TryGetValue("PlaybackStatus", out var value))
status = value.GetString();
Disposing a subscription closes the file descriptors of signals it received but
nobody read. DBusSignal, DBusPropertyChanges and DBusMethodCall compare by
content, like the values they carry. The connection creates signals and
DBusProxy.ReadPropertyChanges creates property changes, so neither has a public
constructor; a DBusMethodCall is a request you create.
A DBusSignalFilter sets the match rule's keys: Sender, Path, PathNamespace,
Interface, Member, and for arguments Arg0 (or Arguments, string arguments
by position 0–63), ArgumentPaths (argNpath: a path argument equal to the value,
or below it when the value ends in /) and Arg0Namespace (a bus or interface
name equal to the value or below it, such as org.mpris.MediaPlayer2 for every
player). The bus applies the keys of the first argument; the connection itself
checks the keys of later arguments, so those signals still reach the connection.
WatchPropertiesAsync and WatchSignalAsync are shorter forms that subscribe when
enumeration starts. DBusConnection.WatchNameOwnerAsync(name) reports a bus name's
owner, first the current one and then every change, so an application notices when
a service starts or stops.
Follow a service's objects
Services such as UDisks2, BlueZ and NetworkManager list their objects through
org.freedesktop.DBus.ObjectManager. DBusObjectManagerProxy reads them:
GetManagedObjectsAsync returns each object's interfaces with their properties,
and ReadInterfaceChanges reads the InterfacesAdded and InterfacesRemoved
signals of SubscribeInterfaceChangesAsync. Subscribe first, so no object added in
between is missed:
var manager = new DBusObjectManagerProxy(bus, "org.freedesktop.UDisks2", "/org/freedesktop/UDisks2");
await using var changes = await manager.SubscribeInterfaceChangesAsync(cancellationToken);
var objects = await manager.GetManagedObjectsAsync(cancellationToken);
var drives = objects.Where(static entry => entry.Value.ContainsKey("org.freedesktop.UDisks2.Drive")).Select(static entry => entry.Key).ToHashSet();
await foreach (var signal in changes.WithCancellation(cancellationToken))
{
if (DBusObjectManagerProxy.ReadInterfaceChanges(signal) is not { } change) continue;
if (change.Added.ContainsKey("org.freedesktop.UDisks2.Drive")) drives.Add(change.Path);
if (change.Removed.Contains("org.freedesktop.UDisks2.Drive")) drives.Remove(change.Path);
}
WatchInterfaceChangesAsync is the shorter form that subscribes when enumeration
starts. The objects' own property changes arrive as PropertiesChanged; subscribe
to them with a DBusSignalFilter whose PathNamespace is the manager's path.
Offer a service
Derive from DBusObject, export it at a path and request a well-known name:
sealed class Counter : DBusObject
{
private int _value;
protected override string IntrospectionXml => """
<interface name="org.example.Counter"><method name="Add"><arg direction="in" type="i"/><arg direction="out" type="i"/></method>
<property name="Value" type="i" access="read"/></interface>
""";
protected override ValueTask<ImmutableArray<DBusValue>> HandleMethodAsync(DBusMethodContext call) => call switch
{
{ Interface: "org.example.Counter", Member: "Add", Signature.Value: "i" } => Add(call.Arguments[0].GetInt32()),
_ => throw new DBusException(DBusException.UnknownMethod, $"No method {call.Member}."),
};
private ValueTask<ImmutableArray<DBusValue>> Add(int amount)
{
int value = Interlocked.Add(ref _value, amount);
EmitPropertiesChanged("org.example.Counter", new Dictionary<string, DBusValue> { ["Value"] = DBusValue.From(value) });
return ValueTask.FromResult<ImmutableArray<DBusValue>>([DBusValue.From(value)]);
}
protected override ValueTask<DBusValue> GetPropertyAsync(string interfaceName, string name, DBusMethodContext call) =>
name == "Value" ? ValueTask.FromResult(DBusValue.From(Volatile.Read(ref _value))) : base.GetPropertyAsync(interfaceName, name, call);
}
await using var export = bus.Export("/org/example/Counter", new Counter());
await bus.RequestNameAsync("org.example.Counter", DBusRequestNameOptions.DoNotQueue);
The base class answers org.freedesktop.DBus.Introspectable from
IntrospectionXml and org.freedesktop.DBus.Properties from the property methods;
the connection answers org.freedesktop.DBus.Peer. EmitSignal and
EmitPropertiesChanged send signals while the object is exported. Throw
DBusException to reply with an error name; any other exception replies
org.freedesktop.DBus.Error.Failed without its message, which can contain
internal details. Disposing the export stops answering at the path.
Any program on the bus can call an exported object, so each object bounds its
calls: at most MaxConcurrentCalls (16 unless the object overrides it) run at
once, up to 64 more wait for a turn, and the connection refuses any further call
with DBusException.LimitsExceeded until one finishes. Calls start in the order
they arrive; an object that sets MaxConcurrentCalls to 1 answers them one at a
time, in that order. Raise the limit for an object whose calls wait a long time,
such as for a user's answer.
Generate proxies and services
The package includes a source generator that turns D-Bus introspection XML into
typed C#. Save the service's interface XML in the project, for example from
busctl --user introspect --xml-interface <service> <path> or the service's
published XML, and list it as an AdditionalFiles item:
<ItemGroup>
<AdditionalFiles Include="DBus/org.mpris.MediaPlayer2.Player.xml" RunicDBusMode="Proxy" RunicDBusNamespace="MyApp.DBus" />
</ItemGroup>
| Metadata | Values |
|---|---|
RunicDBusMode |
Proxy, Service, or Proxy,Service for both (a comma, not a semicolon). Required. |
RunicDBusNamespace |
The namespace of the generated types. Required. Namespace, its name before 0.9, still works until 2.0; RunicDBusNamespace wins when both are set. |
RunicDBusVisibility |
internal (default) or public. |
RunicDBusClassName |
The generated class's name, instead of one derived from the interface. |
A project that also references Tmds.DBus.Generator, which reads every .xml
AdditionalFiles item, sets GenerateDBusTypes="false" on the items this
generator owns, so the two do not generate the same interfaces twice.
Proxy generates one class per interface, named after the interface's last
segment (Player for org.mpris.MediaPlayer2.Player), deriving from
DBusProxy:
var player = new Player(bus, "org.mpris.MediaPlayer2.vlc", "/org/mpris/MediaPlayer2");
await player.PlayPauseAsync();
double volume = await player.GetVolumeAsync();
await player.SetVolumeAsync(0.5);
PlayerProperties all = await player.GetPropertiesAsync();
await foreach (long position in player.WatchSeekedAsync(cancellationToken)) { /* … */ }
Each method becomes <Method>Async, with the call's options and a cancellation
token after its arguments; a method with several results returns a named tuple.
Each property becomes Get<Property>Async and, when writable,
Set<Property>Async. GetPropertiesAsync and WatchPropertiesAsync return
<Name>Properties, whose properties are null when the service did not send
them. Each signal becomes Watch<Signal>Async and a static Read<Signal> for
signals from SubscribeSignalAsync; a signal without arguments yields
PlatformUnit.
Service generates one abstract class for all interfaces of the file,
named after the first interface (PlayerService), deriving from DBusObject.
Derive from it and implement each method as protected abstract <Method>Async(arguments, DBusMethodContext context). Each property becomes a
C# property; setting it emits PropertiesChanged as the interface's
org.freedesktop.DBus.Property.EmitsChangedSignal annotation says (true,
invalidates, const or false), and a caller's write of a writable property
calls OnSet<Property>Async, which sets it by default. Each signal becomes
Emit<Signal>(arguments). The class answers introspection with the file's
interfaces and checks each call's signature.
A generated member never hides or overloads an inherited one. When a D-Bus name
would produce a member that DBusProxy, DBusObject or another generated member
already uses, such as a method Call (CallAsync) or a signal Properties
(WatchPropertiesAsync), the generator reports RUNICDBUS012 instead.
D-Bus types map to C# as follows: y b n q i u x t d s to byte, bool,
short, ushort, int, uint, long, ulong, double and string; o
and g to DBusObjectPath and DBusSignature; h to SafeHandle; v to
DBusValue; arrays to ImmutableArray<T> (an ay as one block of bytes),
dictionaries to ImmutableDictionary<TKey, TValue> (a repeated key keeps its last
value) and structures to tuples. A reply or
property of another type than the XML declares throws DBusException with
org.freedesktop.DBus.Error.InvalidArgs. The generator reports problems in the
XML as RUNICDBUS diagnostics,
located at the XML element or line they concern, so the IDE opens the file there.
It warns (RUNICDBUS013) about an org.freedesktop.DBus.Deprecated annotation,
and in proxies about org.freedesktop.DBus.Method.NoReply, because the generated
code does not carry them out: a deprecated member is not marked obsolete, and a
proxy method waits for a reply, so bound it with a cancellation token or the
proxy's Timeout.
Generated code uses only this package's public API and works with NativeAOT.
The generator adapts parts of Tmds.DBus.Generator, under the MIT License; the
package's NOTICE and licenses/Tmds.DBus-LICENSE.txt carry the notice.
Threads and errors
No method captures the caller's synchronization context. Call results, signal
streams and service calls run on the thread pool; a ViewModel moves results to its
model context, for example with await context.InvokeAsync(...). Service calls can
run concurrently, also with each other, so an object that keeps state guards it
(or sets MaxConcurrentCalls to 1).
Method calls throw DBusException for an error reply: branch on ErrorName, such
as org.freedesktop.DBus.Error.ServiceUnknown or a service's own error. The
message is the peer's text, meant for logs. A closed connection reports
DBusException.Disconnected, a call past its Timeout reports
DBusException.NoReply, and cancelling a call's token throws
OperationCanceledException and ignores the reply. Higher-level Runic services
built on this package report their outcomes as PlatformResult.
Platforms
| Platform | Status |
|---|---|
| Linux | Session and system bus. Verified in CI on a private bus with JIT and NativeAOT. |
| Linux in Flatpak | Session bus names need --talk-name or --own-name, the system bus --system-talk-name; without them the bus refuses the calls with an access error. |
| Windows, macOS | Not applicable: there is no D-Bus. ConnectAsync returns Unavailable(ProviderNotConfigured). |
Runic.Platform.Linux.Portal makes all of its D-Bus calls through this package, on connections it shares across its services.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Runic.Platform (= 0.11.0)
- Tmds.DBus.Protocol (>= 0.95.1)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Runic.Platform.Linux.DBus:
| Package | Downloads |
|---|---|
|
Runic.Platform.Linux.Portal
The Linux provider of the Runic.Platform services, built on XDG desktop portals, D-Bus services and freedesktop.org specifications. |
|
|
Runic.Desktop
A managed .NET runtime for web-powered desktop applications. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.11.0 | 2,554 | 10/25/2026 |
| 0.10.0 | 5,288 | 10/22/2026 |
| 0.9.1 | 857 | 10/21/2026 |
| 0.9.0 | 200 | 10/21/2026 |
| 0.8.0-preview.6 | 1,366 | 10/19/2026 |
| 0.8.0-preview.5 | 229 | 10/17/2026 |