Skip to content

Add a libdbus-1 C ABI and a CMake build - #9

Merged
colemancda merged 23 commits into
masterfrom
c-abi
Aug 7, 2026
Merged

Add a libdbus-1 C ABI and a CMake build#9
colemancda merged 23 commits into
masterfrom
c-abi

Conversation

@colemancda

Copy link
Copy Markdown
Member

Adds a C ABI for the pure-Swift implementation, and a CMake build that ships it as libdbus-swift.so.0. Modelled on the PureSwift/Bluetooth packaging: a C target holding the header, a Swift target holding the entry points, a version script pinning the exports, and a check-exports target that fails if the surface drifts.

What the ABI is

A subset of libdbus-1, with the real dbus_* symbol names and ABI-compatible types, so a C program using the core of libdbus links against this unchanged. 87 exported symbols covering errors, memory, messages, the message iterators, connections and bus operations.

DBusError and DBusMessageIter reproduce the reference layouts exactly — callers allocate both on the stack — and the type codes and reply constants reproduce the reference values. The declarations are written against the documented ABI rather than copied from libdbus, so no AFL/GPL headers are vendored.

What is deliberately absent

The main-loop integration: dbus_connection_set_watch_functions, dbus_connection_set_timeout_functions, dbus_connection_dispatch, dbus_connection_read_write* and DBusPendingCall.

That API exists to hand the caller raw pollable descriptors so it can drive I/O from its own event loop. This implementation owns its I/O in an actor with its own read loop, so there is no descriptor to hand over and no dispatch step to perform. Exposing those functions would mean either lying about what they do or building a second event loop next to the one that already exists. The blocking calls — which is what most callers of that API end up writing anyway — are fully supported.

cmake/symbols.txt pins the list and Scripts/check-exports.sh fails the build on a symbol in either direction, so that boundary is asserted rather than described.

Notes on the implementation

The soname is this library's own, not libdbus-1's. It implements a subset, so claiming libdbus-1.so.3 would let a program linked against the real thing load this and fail at the first symbol outside it.

Async to blocking. The Swift API is async; the C ABI cannot be. A C caller arrives on its own thread, never on a cooperative thread, so blocking(_:) parks that thread on a semaphore while a detached task runs — which is safe precisely because the caller is not on the executor.

Reference counting is Unmanaged, not ARC: a pointer handed to C carries a retain that only the matching unref releases.

Iterator state. DBusMessageIter is a stack value with no destructor, so its state cannot be an allocation the caller is expected to free. The state is owned by the message and reached through a pointer parked in one of the struct's opaque fields, which matches the reference's rule that an iterator is valid only while its message is. An uninitialized iterator is detected and refused rather than read as garbage.

The variadics live in C. Swift cannot declare a C variadic function, so dbus_set_error, dbus_message_append_args and dbus_message_get_args (plus the _valist forms) are implemented in Sources/CDBusABI/varargs.c. They contain no protocol logic — each walks a va_list and calls the Swift iterator entry points.

Opt-in

The C ABI targets build only when SWIFTPM_DBUS_CABI=1. They export fixed C symbol names, so linking them into a process that also links the real libdbus-1 is a duplicate-symbol error, and a Swift package that merely depends on DBus should never be exposed to that. CMakeLists.txt always builds them. The default swift build and the existing 205-test suite are unaffected.

The one change outside the new targets is DBusBusType gaining Sendable, which it needs to cross into the detached task.

Tests

  • 15 Swift tests covering construction, reference counting, borrowed header strings, basic types, empty arrays keeping their declared element type, arrays, structures, a{sv}, abandoned containers, and refusing an uninitialized iterator.
  • A C smoke test built and run by CMake against the installed .so. It is not a duplicate: it is the only thing that can exercise the variadic entry points, and it proves the shared object links and loads with every symbol resolvable from outside.

Two real bugs surfaced this way and are fixed here:

  • dbus_message_append_args was reading the array argument one indirection short. The reference takes the address of the array pointer, not the array.
  • CMakeLists.txt named the static archives to the linker through LINKER: strings, which CMake treats as opaque, so a changed archive did not relink the library — the build silently kept shipping the previous object code. They are now declared in LINK_DEPENDS. Worth knowing, because the same shape appears in the Bluetooth CMakeLists this was modelled on.

CI

.github/workflows/cmake.yml configures, builds, checks exports, runs the C smoke test, installs to a staging prefix and asserts the installed layout — on x86_64 and arm64, against Swift 6.2.3 and 6.3.3. It uses noble rather than jammy because CMakeLists requires CMake 3.26+ and jammy ships 3.22. A second job builds and tests the SwiftPM C ABI targets, which are otherwise never built.

Also

Package.swift returns to tracking PureSwift/Socket at main, now that PureSwift/Socket#27 has merged.

@colemancda
colemancda merged commit d34567d into master Aug 7, 2026
26 checks passed
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