Skip to content

Rewrite in pure Swift - #8

Merged
colemancda merged 91 commits into
masterfrom
pure-swift
Aug 7, 2026
Merged

Rewrite in pure Swift#8
colemancda merged 91 commits into
masterfrom
pure-swift

Conversation

@colemancda

@colemancda colemancda commented Aug 7, 2026

Copy link
Copy Markdown
Member

Replaces the libdbus wrapper with a pure-Swift implementation of the D-Bus protocol. No C library, no CDBus system-library package, no OpaquePointer.

Why

The wrapper layer held several defects that were artifacts of wrapping C rather than of D-Bus itself: borrowed messages were never returned to the connection, DBusPendingCall retained itself and could never deallocate, DBusConnection.init trapped on a NULL handle before it could throw, and the header-field getters called fatalError() on remote-controlled input. Separately, variant and dict — the two types every service uses through org.freedesktop.DBus.Properties — were unimplemented and crashed on receipt.

Rewriting in Swift removes that class of bug by construction. It also drops the libdbus build dependency, which was emitting couldn't find pc file for dbus-1.0, and lets the package adopt Swift 6 concurrency.

What is implemented

Wire format. Marshalling and unmarshalling in both byte orders, with the full alignment table, the 128 MiB message cap, and required-zero padding on read. Empty arrays encode from the declared element type rather than inferring it from the (absent) elements. variant and dictionary are real argument cases now.

Transport. Unix sockets including the Linux abstract namespace, plus TCP and nonce-TCP. Socket.UnixSocketAddress stores a FilePath and writes sun_path via withPlatformString, so it cannot express an abstract address; this package defines its own.

Authentication. SASL EXTERNAL, ANONYMOUS and DBUS_COOKIE_SHA1, with SHA-1 and a keyring parser. Cookie contexts are validated as bus-name elements so a hostile server cannot escape the keyring directory with a relative path.

Client. DBusConnection is an actor. A read loop unmarshals frames and resumes per-serial continuations, so send(_:) is a plain async throws call and DBusPendingCall is gone. Serials wrap to 1, never 0. Malformed input throws; nothing on a decode path traps.

Bus API. Hello, name registration and release, ownership queries, ListNames, GetNameOwner, connection credentials, bus ID.

Signals. Match rules with correct value escaping, subscriptions delivered as AsyncStream, reference-counted bus-side match rules shared between subscriptions on one connection, and local demultiplexing so each subscription only sees what its own rule matches.

Server side. Object export with method dispatch, and the standard Peer, Introspectable and Properties interfaces including PropertiesChanged.

File descriptor passing. UNIX_FD marshals as an index into the out-of-band descriptor array, as the specification requires. The UNIX_FDS header field is derived from what marshalling actually produced, so it cannot disagree with the body. Descriptors ride along via SCM_RIGHTS.

Introspection. XML generation, and parsing into a node model. Parsing uses a small XML reader in-package rather than pulling in FoundationXML or libxml2.

API

Public value types are preserved: DBusSignature, DBusObjectPath, DBusInterface, DBusMember, DBusBusName, DBusMessageArgument, DBusError. DBusMessage becomes a Sendable struct instead of a class wrapping a pointer, and MethodCall/Signal gain public initializers — the synthesized memberwise ones were internal, so DBusMessage.init(methodCall:) was unreachable from client code.

Removed: PendingCall, MessageIterator, Boolean (dbus_bool_t), Timeout (now Duration), DispatchStatus and HandlerResult (libdbus main-loop concepts with no analogue here).

Validation

The validators no longer delegate to libdbus, so they are solely responsible for correctness now. Three real bugs surfaced once the existing tests were fixed to actually run every case — testInvalid was a loop whose catch ended in return, so exactly one of nine object paths and one of twelve signatures was ever checked:

  • a{vs} was accepted. DictionaryType.init? only rejected containers, and a variant is not a container by that definition. Dictionary keys are now checked against an explicit basic-type list.
  • org.7zip.Plugin was accepted. Interface elements must not begin with a digit.
  • /com//example was accepted. parse split with omittingEmptySubsequences: true.

Length caps now count UTF-8 bytes rather than Characters, and container nesting is capped at 32 levels.

Tests

205 tests across 23 suites, migrated from XCTest to Swift Testing.

Marshalling is checked against hand-computed byte vectors in both endiannesses, not only round trips — a marshaller can be self-consistently wrong. Live tests run against a real session bus and are reported as skipped, not passed, when none is available. The TCP tests start a dbus-daemon configured for ANONYMOUS, which also exercises the mechanism fallback for real.

Dependency

Building this out surfaced four bugs in PureSwift/Socket, all fixed there rather than worked around here:

  • #24 (merged) — SCM_RIGHTS support, plus a deferred poll result being applied to a reused descriptor number.
  • #26 (merged) — a lost wakeup that stranded a waiter forever, and a stale POLLHUP acted on after the socket had connected. Together these were the intermittent ESHUTDOWN on a fresh connection.
  • #27 (open) — a reported readiness treated as current, surfacing EWOULDBLOCK to the caller as a spurious failure.

Package.swift therefore tracks PureSwift/Socket at branch fix/stale-readiness until #27 merges, at which point it returns to main. That is isolated in a single commit.

Continuous integration

.github/workflows/swift.yml builds macOS and Linux on both x86_64 and arm64.

Linux runs the full suite — 205 tests — on Swift 6.2.3 and 6.3.3, in debug and release, statically and as a dynamic library, under a real session bus started with dbus-run-session. Without a bus the live tests report as skipped rather than passing, which would make the run meaningless. macOS is a compile target: it builds the library and the tests, so they cannot rot out of compiling on Darwin, but does not run them, since D-Bus is not part of the system there.

All 20 jobs pass.

Not implemented

Code generation from introspection XML. Parsing landed; generation did not.

@colemancda
colemancda merged commit d6c37ad into master Aug 7, 2026
20 checks passed
@colemancda
colemancda deleted the pure-swift branch August 7, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant