diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml new file mode 100644 index 0000000..184a5f4 --- /dev/null +++ b/.github/workflows/cmake.yml @@ -0,0 +1,73 @@ +name: CMake +on: [push] +jobs: + + # Verifies the CMake packaging path, which builds `libdbus-swift.so` — the + # libdbus-1 C ABI — independently of SwiftPM's build graph, since soname, + # symbol versioning and install name are not expressible in Package.swift. + # SwiftPM remains the primary build used by the other workflow; this guards + # CMakeLists.txt from silently drifting, and is the only place the variadic + # entry points are exercised, because Swift cannot call a C variadic + # function. + cmake: + name: CMake (${{ matrix.arch }}, ${{ matrix.container }}) + strategy: + fail-fast: false + matrix: + arch: ["x86_64", "arm64"] + container: ["swift:6.2.3", "swift:6.3.3"] + include: + - arch: x86_64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + # noble, not jammy like the Swift workflow: CMakeLists.txt requires CMake + # 3.26+, and jammy's apt only has 3.22. noble ships 3.28. + container: ${{ matrix.container }}-noble + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Swift Version + run: swift --version + - name: Install CMake and Ninja + run: apt-get update -qq && apt-get install -y --no-install-recommends cmake ninja-build + - name: Resolve dependencies + # CMake has no package resolution: it reuses the checkouts SwiftPM makes. + run: swift package resolve + - name: Configure + run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + - name: Build + run: cmake --build build + - name: Check exported symbols + run: cmake --build build --target check-exports + - name: Run the C ABI smoke test + run: cmake --build build --target check-abi + - name: Install + run: cmake --install build --prefix "$PWD/staging" + - name: Check the installed layout + run: | + set -eu + test -f staging/lib/libdbus-swift.so.0 + test -f staging/lib/pkgconfig/dbus-swift.pc + test -f staging/include/dbus-swift/dbus/dbus.h + + # The C ABI targets are off unless SWIFTPM_DBUS_CABI=1, so they need a build + # of their own to be covered at all. + swiftpm-cabi: + name: SwiftPM C ABI (${{ matrix.config }}) + strategy: + fail-fast: false + matrix: + config: ["debug", "release"] + runs-on: ubuntu-latest + container: swift:6.3.3-noble + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build + run: SWIFTPM_DBUS_CABI=1 swift build -c ${{ matrix.config }} + - name: Test + run: SWIFTPM_DBUS_CABI=1 swift test -c ${{ matrix.config }} --filter DBusABITests --no-parallel diff --git a/.gitignore b/.gitignore index 673e5a9..291c92f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Packages/* .build DBus.xcodeproj/* *.pins +build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..03c7d69 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,286 @@ +## +## CMakeLists.txt +## DBus +## +## Builds `libdbus-swift.so.0` — the subset of the libdbus-1 C ABI +## declared in `Sources/CDBusABI/include/dbus/dbus.h`, implemented in +## Swift. +## +## SwiftPM drives development and the test suites; CMake exists because +## soname, symbol versioning and install name are not expressible in +## `Package.swift`, and because the C ABI is only worth shipping as an +## installable shared object. +## +## The Ninja generator is required — CMake does not support the Swift +## language with Makefiles. +## +## Usage: +## cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release +## cmake --build build +## cmake --build build --target check-exports +## cmake --build build --target check-abi +## cmake --install build --prefix /usr/local +## + +cmake_minimum_required(VERSION 3.26) + +if(NOT CMAKE_GENERATOR MATCHES "Ninja") + message(FATAL_ERROR + "The Ninja generator is required for Swift targets; " + "re-run with -G Ninja.") +endif() + +project(DBusABI + VERSION 0.1.0 + DESCRIPTION "Swift implementation of the libdbus-1 C ABI" + LANGUAGES C Swift) + +# --------------------------------------------------------------------- +# Options +# --------------------------------------------------------------------- + +option(DBUS_ABI_SHARED + "Build the shared library (off builds the static archives only)" ON) + +option(DBUS_ABI_STATIC_STDLIB + "Statically link the Swift runtime into the shared library" OFF) + +option(DBUS_ABI_INSTALL_HEADERS + "Install dbus.h under /include/dbus-swift/dbus" ON) + +# The soname is this library's own, not libdbus-1's. It implements a +# subset of that ABI but is not a drop-in replacement for the whole of +# it, so it must not claim libdbus-1's soname: a program linked against +# the real thing would otherwise load this and fail at the first symbol +# outside the subset. +set(DBUS_SWIFT_SOVERSION 0) +set(DBUS_SWIFT_VERSION 0.1.0) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE) +endif() + +include(GNUInstallDirs) + +# --------------------------------------------------------------------- +# Socket — the transport dependency +# --------------------------------------------------------------------- +# +# `Sources/DBus` imports `Socket` and `SystemPackage`, which SwiftPM +# resolves from Package.swift. CMake has no package resolution, so the +# checkout SwiftPM already made is reused: configure once with +# `swift build` having been run, or point DBUS_SOCKET_SOURCE_DIR at a +# checkout. + +set(DBUS_SOCKET_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.build/checkouts/Socket" + CACHE PATH "PureSwift/Socket checkout") +set(DBUS_SYSTEM_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.build/checkouts/swift-system" + CACHE PATH "apple/swift-system checkout") + +foreach(dependency + "${DBUS_SOCKET_SOURCE_DIR}" + "${DBUS_SYSTEM_SOURCE_DIR}") + if(NOT EXISTS "${dependency}") + message(FATAL_ERROR + "${dependency} not found. Run `swift build` first so SwiftPM " + "resolves the dependencies, or set DBUS_SOCKET_SOURCE_DIR and " + "DBUS_SYSTEM_SOURCE_DIR.") + endif() +endforeach() + +file(GLOB CSYSTEM_SOURCES CONFIGURE_DEPENDS + "${DBUS_SYSTEM_SOURCE_DIR}/Sources/CSystem/*.c") + +add_library(CSystem STATIC ${CSYSTEM_SOURCES}) +target_include_directories(CSystem PUBLIC + "${DBUS_SYSTEM_SOURCE_DIR}/Sources/CSystem/include") + +file(GLOB_RECURSE SYSTEM_PACKAGE_SOURCES CONFIGURE_DEPENDS + "${DBUS_SYSTEM_SOURCE_DIR}/Sources/System/*.swift") + +add_library(SystemPackage STATIC ${SYSTEM_PACKAGE_SOURCES}) +set_target_properties(SystemPackage PROPERTIES Swift_MODULE_NAME SystemPackage) +target_compile_definitions(SystemPackage PRIVATE SYSTEM_PACKAGE) +target_link_libraries(SystemPackage PUBLIC CSystem) + +file(GLOB CSOCKET_SOURCES CONFIGURE_DEPENDS + "${DBUS_SOCKET_SOURCE_DIR}/Sources/CSocket/*.c") + +add_library(CSocket STATIC ${CSOCKET_SOURCES}) +target_include_directories(CSocket PUBLIC + "${DBUS_SOCKET_SOURCE_DIR}/Sources/CSocket/include") + +file(GLOB_RECURSE SOCKET_SOURCES CONFIGURE_DEPENDS + "${DBUS_SOCKET_SOURCE_DIR}/Sources/Socket/*.swift") + +add_library(Socket STATIC ${SOCKET_SOURCES}) +set_target_properties(Socket PROPERTIES Swift_MODULE_NAME Socket) +target_link_libraries(Socket PUBLIC SystemPackage CSocket) + +# --------------------------------------------------------------------- +# CDBusABI — the C surface +# --------------------------------------------------------------------- +# +# The header plus the four variadic entry points, which Swift cannot +# declare. They contain no protocol logic: each walks a `va_list` and +# calls the Swift iterator entry points. + +add_library(CDBusABI STATIC + Sources/CDBusABI/varargs.c) + +target_include_directories(CDBusABI PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/Sources/CDBusABI/include") + +# --------------------------------------------------------------------- +# DBus — the Swift implementation +# --------------------------------------------------------------------- + +file(GLOB_RECURSE DBUS_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/Sources/DBus/*.swift") + +add_library(DBus STATIC ${DBUS_SOURCES}) +set_target_properties(DBus PROPERTIES Swift_MODULE_NAME DBus) +target_link_libraries(DBus PUBLIC Socket) + +# --------------------------------------------------------------------- +# DBusABI — the Swift implementations of the C entry points +# --------------------------------------------------------------------- + +file(GLOB DBUS_ABI_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/Sources/DBusABI/*.swift") + +add_library(DBusABI STATIC ${DBUS_ABI_SOURCES}) +set_target_properties(DBusABI PROPERTIES Swift_MODULE_NAME DBusABI) +target_link_libraries(DBusABI PUBLIC DBus CDBusABI) + +# --------------------------------------------------------------------- +# libdbus-swift.so.0 — the installable artifact +# --------------------------------------------------------------------- + +if(DBUS_ABI_SHARED) + + set(DBUS_SWIFT_VERSION_SCRIPT + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/libdbus-swift.map") + + # A shared library needs at least one of its own objects; the Swift + # and C implementations arrive through --whole-archive below. + add_library(dbus-swift SHARED cmake/empty.swift) + + set_target_properties(dbus-swift PROPERTIES + OUTPUT_NAME dbus-swift + SOVERSION ${DBUS_SWIFT_SOVERSION} + VERSION ${DBUS_SWIFT_VERSION} + # swiftc must drive the link, so that the Swift runtime is recorded + # as a dependency of the library; CMake would otherwise pick C from + # the lone placeholder source and produce an object with unresolved + # stdlib symbols. `dbus-swift` is not a valid Swift identifier, so + # the vestigial module is named explicitly. + LINKER_LANGUAGE Swift + Swift_MODULE_NAME dbus_swift) + + # Every entry point is referenced only from outside, so the archives + # holding them must be linked whole or the linker drops the lot. + # + # The archives are named explicitly through `LINKER:` rather than via + # `$`: the link is driven by swiftc, whose + # driver silently discards bare `.a` arguments, so the result would be + # a shared library with no symbols in it. `add_dependencies` below + # restores the build ordering that naming targets directly would give. + # + # The engine archives that follow are named the same way, and for the + # same reason, even though they are linked normally rather than whole: + # `target_link_libraries` would hand swiftc bare `.a` paths, and the + # result is a library missing every symbol they define. + target_link_options(dbus-swift PRIVATE + "LINKER:--whole-archive" + "LINKER:$" + "LINKER:$" + "LINKER:--no-whole-archive" + # Ordered as the linker resolves them: each may only depend on the + # archives that follow it. + "LINKER:$" + "LINKER:$" + "LINKER:$" + "LINKER:$" + "LINKER:$") + + add_dependencies(dbus-swift + DBusABI CDBusABI DBus Socket SystemPackage CSocket CSystem) + + # `add_dependencies` only orders the build. The archives above reach the + # link as opaque `LINKER:` strings, so without naming them here as link + # inputs the library is not relinked when one of them changes, and the + # build silently keeps shipping the previous object code. + set_target_properties(dbus-swift PROPERTIES + LINK_DEPENDS "$;$;$;$;$;$;$;${DBUS_SWIFT_VERSION_SCRIPT}") + + # Pin the export list, so the ABI surface is asserted rather than + # incidental. + target_link_options(dbus-swift PRIVATE + "LINKER:--version-script=${DBUS_SWIFT_VERSION_SCRIPT}") + + if(DBUS_ABI_STATIC_STDLIB) + # Any process loading this library otherwise pulls in libswiftCore; + # static linking trades size for isolation. + target_link_options(dbus-swift PRIVATE "-static-stdlib") + endif() + + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/dbus-swift.pc.in" + "${CMAKE_CURRENT_BINARY_DIR}/dbus-swift.pc" + @ONLY) + + install(TARGETS dbus-swift + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}") + + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus-swift.pc" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig") +endif() + +if(DBUS_ABI_INSTALL_HEADERS) + # Under `dbus-swift/dbus/` rather than `dbus/`, so installing this + # alongside the real libdbus-1 development headers cannot shadow them. + install(FILES + "${CMAKE_CURRENT_SOURCE_DIR}/Sources/CDBusABI/include/dbus/dbus.h" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/dbus-swift/dbus") +endif() + +# --------------------------------------------------------------------- +# Verification +# --------------------------------------------------------------------- + +if(DBUS_ABI_SHARED) + + # `check-exports` diffs the built library's dynamic symbol table + # against cmake/symbols.txt. Both a missing and an extra symbol fail, + # so the export surface cannot drift silently. + add_custom_target(check-exports + COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/Scripts/check-exports.sh" + "$" + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/symbols.txt" + DEPENDS dbus-swift + COMMENT "Checking exported symbols against cmake/symbols.txt" + VERBATIM) + + # `check-abi` builds and runs a C program against the shared library. + # It is the only thing that can exercise the variadic entry points — + # Swift cannot call a C variadic function, so the SwiftPM test suite + # reaches everything except those. + # Links only the shared library, exactly as a consumer would: linking the + # static archives too would resolve some entry points from a second copy + # and prove nothing about the installed artifact. + add_executable(dbus-abi-smoke Tests/CABI/smoke.c) + target_include_directories(dbus-abi-smoke PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/Sources/CDBusABI/include") + target_link_libraries(dbus-abi-smoke PRIVATE dbus-swift) + + add_custom_target(check-abi + COMMAND dbus-abi-smoke + DEPENDS dbus-abi-smoke + COMMENT "Running the C ABI smoke test" + VERBATIM) +endif() diff --git a/Package.resolved b/Package.resolved index 9d14dfc..3ec872d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "4450dc9d58c314eae579e6834118bfe9a1e0a76ca1c166800d4b802afacbc6c9", + "originHash" : "44137a077cb3c3e179b949b044d33eb210b4bf13052bfe7f7259069bec17a208", "pins" : [ { "identity" : "socket", "kind" : "remoteSourceControl", "location" : "https://github.com/PureSwift/Socket.git", "state" : { - "branch" : "fix/stale-readiness", - "revision" : "37381d5dc881da2acb13ef38f23d645bad87b8c6" + "branch" : "main", + "revision" : "87047eaac3fb3012747d6d7b512edad0bc921311" } }, { diff --git a/Package.swift b/Package.swift index 099035b..5f18844 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/PureSwift/Socket.git", - branch: "fix/stale-readiness" + branch: "main" ) ], targets: [ @@ -39,3 +39,41 @@ let package = Package( ) ] ) + +// The libdbus-1 C ABI, built only when `SWIFTPM_DBUS_CABI=1`. +// +// Off by default because it exports fixed C symbol names: linking it 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`, which builds the installable shared library, always +// sets it. +if ProcessInfo.processInfo.environment["SWIFTPM_DBUS_CABI"] == "1" { + + package.products.append( + .library( + name: "DBusABI", + type: libraryType, + targets: ["DBusABI"] + ) + ) + + package.targets.append(contentsOf: [ + .target( + name: "CDBusABI" + ), + .target( + name: "DBusABI", + dependencies: [ + "DBus", + "CDBusABI" + ] + ), + .testTarget( + name: "DBusABITests", + dependencies: [ + "DBusABI", + "CDBusABI" + ] + ) + ]) +} diff --git a/Scripts/check-exports.sh b/Scripts/check-exports.sh new file mode 100755 index 0000000..8e905dc --- /dev/null +++ b/Scripts/check-exports.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# +# Assert a library's exported symbols against an expected list. +# +# Both directions fail: a symbol in the list but not in the library +# (something was never implemented, or the version script does not +# match), and a symbol in the library but not in the list (an internal +# detail leaked into the ABI surface). +# +# Usage: Scripts/check-exports.sh [symbols.txt] +# +set -euo pipefail + +LIBRARY="${1:?usage: check-exports.sh [symbols.txt]}" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +EXPECTED_FILE="${2:-${ROOT}/cmake/symbols.txt}" + +if [[ ! -f "${LIBRARY}" ]]; then + echo "error: ${LIBRARY} not found" >&2 + exit 1 +fi + +if [[ ! -f "${EXPECTED_FILE}" ]]; then + echo "error: ${EXPECTED_FILE} not found" >&2 + exit 1 +fi + +work="$(mktemp -d)" +trap 'rm -rf "${work}"' EXIT + +# Defined, exported functions and data — not undefined imports. +nm --dynamic --defined-only --format=posix "${LIBRARY}" \ + | awk '$2 ~ /^[TDBRWi]$/ { print $1 }' \ + | sed 's/@.*//' \ + | sort -u > "${work}/actual" + +grep -vE '^\s*(#|$)' "${EXPECTED_FILE}" | sort -u > "${work}/expected" + +comm -13 "${work}/actual" "${work}/expected" > "${work}/missing" +comm -23 "${work}/actual" "${work}/expected" > "${work}/extra" + +status=0 + +if [[ -s "${work}/missing" ]]; then + echo "error: $(wc -l < "${work}/missing") expected symbol(s) not exported by ${LIBRARY}:" >&2 + sed 's/^/ - /' "${work}/missing" >&2 + status=1 +fi + +if [[ -s "${work}/extra" ]]; then + echo "error: $(wc -l < "${work}/extra") unexpected symbol(s) exported by ${LIBRARY}:" >&2 + sed 's/^/ + /' "${work}/extra" >&2 + status=1 +fi + +if [[ "${status}" -eq 0 ]]; then + echo "exports: $(wc -l < "${work}/expected") symbols match $(basename "${EXPECTED_FILE}")" +fi + +exit "${status}" diff --git a/Sources/CDBusABI/include/dbus/dbus.h b/Sources/CDBusABI/include/dbus/dbus.h new file mode 100644 index 0000000..5b4de8b --- /dev/null +++ b/Sources/CDBusABI/include/dbus/dbus.h @@ -0,0 +1,404 @@ +/* + * dbus.h + * DBus + * + * The subset of the libdbus-1 C ABI implemented in Swift by this package. + * + * These declarations are written to be ABI compatible with the reference + * libdbus-1, not copied from it: `DBusError` and `DBusMessageIter` reproduce + * the reference layouts exactly, because callers allocate both on the stack, + * and the integer constants reproduce the reference values. A program that + * uses only the entry points declared here can link against this library + * instead of libdbus-1 without being recompiled. + * + * 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` -- is not provided. + * That API hands the caller raw pollable descriptors so it can drive I/O + * from its own event loop. This implementation owns its I/O in a Swift + * 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 reproducing an event loop that already + * exists. Blocking calls, which is what most callers of that API end up + * writing anyway, are fully supported. + * + * `cmake/symbols.txt` pins the exported symbol list, and + * `Scripts/check-exports.sh` fails the build if the library exports + * anything more or less than that list, so the boundary above is asserted + * rather than described. + */ + +#ifndef DBUS_H +#define DBUS_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ------------------------------------------------------------------ */ +/* Primitive types */ +/* ------------------------------------------------------------------ */ + +typedef unsigned int dbus_bool_t; +typedef unsigned int dbus_unichar_t; +typedef signed short dbus_int16_t; +typedef unsigned short dbus_uint16_t; +typedef signed int dbus_int32_t; +typedef unsigned int dbus_uint32_t; +typedef signed long long dbus_int64_t; +typedef unsigned long long dbus_uint64_t; + +#define TRUE 1 +#define FALSE 0 + +/* ------------------------------------------------------------------ */ +/* Type codes */ +/* ------------------------------------------------------------------ */ + +/* + * Written as plain integers rather than the reference's `((int) 'y')`: the + * value is what the ABI fixes, and a cast expression cannot be imported by + * Swift, which needs these same constants. + */ +#define DBUS_TYPE_INVALID 0 /* '\0' */ +#define DBUS_TYPE_BYTE 121 /* 'y' */ +#define DBUS_TYPE_BOOLEAN 98 /* 'b' */ +#define DBUS_TYPE_INT16 110 /* 'n' */ +#define DBUS_TYPE_UINT16 113 /* 'q' */ +#define DBUS_TYPE_INT32 105 /* 'i' */ +#define DBUS_TYPE_UINT32 117 /* 'u' */ +#define DBUS_TYPE_INT64 120 /* 'x' */ +#define DBUS_TYPE_UINT64 116 /* 't' */ +#define DBUS_TYPE_DOUBLE 100 /* 'd' */ +#define DBUS_TYPE_STRING 115 /* 's' */ +#define DBUS_TYPE_OBJECT_PATH 111 /* 'o' */ +#define DBUS_TYPE_SIGNATURE 103 /* 'g' */ +#define DBUS_TYPE_UNIX_FD 104 /* 'h' */ +#define DBUS_TYPE_ARRAY 97 /* 'a' */ +#define DBUS_TYPE_VARIANT 118 /* 'v' */ +#define DBUS_TYPE_STRUCT 114 /* 'r' */ +#define DBUS_TYPE_DICT_ENTRY 101 /* 'e' */ + +#define DBUS_STRUCT_BEGIN_CHAR 40 /* '(' */ +#define DBUS_STRUCT_END_CHAR 41 /* ')' */ +#define DBUS_DICT_ENTRY_BEGIN_CHAR 123 /* '{' */ +#define DBUS_DICT_ENTRY_END_CHAR 125 /* '}' */ + +/* ------------------------------------------------------------------ */ +/* Message types */ +/* ------------------------------------------------------------------ */ + +#define DBUS_MESSAGE_TYPE_INVALID 0 +#define DBUS_MESSAGE_TYPE_METHOD_CALL 1 +#define DBUS_MESSAGE_TYPE_METHOD_RETURN 2 +#define DBUS_MESSAGE_TYPE_ERROR 3 +#define DBUS_MESSAGE_TYPE_SIGNAL 4 + +/* ------------------------------------------------------------------ */ +/* Timeouts */ +/* ------------------------------------------------------------------ */ + +#define DBUS_TIMEOUT_INFINITE 0x7fffffff +#define DBUS_TIMEOUT_USE_DEFAULT (-1) + +/* ------------------------------------------------------------------ */ +/* Well known names */ +/* ------------------------------------------------------------------ */ + +#define DBUS_SERVICE_DBUS "org.freedesktop.DBus" +#define DBUS_PATH_DBUS "/org/freedesktop/DBus" +#define DBUS_INTERFACE_DBUS "org.freedesktop.DBus" + +#define DBUS_INTERFACE_INTROSPECTABLE "org.freedesktop.DBus.Introspectable" +#define DBUS_INTERFACE_PROPERTIES "org.freedesktop.DBus.Properties" +#define DBUS_INTERFACE_PEER "org.freedesktop.DBus.Peer" + +/* ------------------------------------------------------------------ */ +/* Error names */ +/* ------------------------------------------------------------------ */ + +#define DBUS_ERROR_FAILED "org.freedesktop.DBus.Error.Failed" +#define DBUS_ERROR_NO_MEMORY "org.freedesktop.DBus.Error.NoMemory" +#define DBUS_ERROR_SERVICE_UNKNOWN "org.freedesktop.DBus.Error.ServiceUnknown" +#define DBUS_ERROR_NAME_HAS_NO_OWNER "org.freedesktop.DBus.Error.NameHasNoOwner" +#define DBUS_ERROR_NO_REPLY "org.freedesktop.DBus.Error.NoReply" +#define DBUS_ERROR_IO_ERROR "org.freedesktop.DBus.Error.IOError" +#define DBUS_ERROR_BAD_ADDRESS "org.freedesktop.DBus.Error.BadAddress" +#define DBUS_ERROR_NOT_SUPPORTED "org.freedesktop.DBus.Error.NotSupported" +#define DBUS_ERROR_LIMITS_EXCEEDED "org.freedesktop.DBus.Error.LimitsExceeded" +#define DBUS_ERROR_ACCESS_DENIED "org.freedesktop.DBus.Error.AccessDenied" +#define DBUS_ERROR_AUTH_FAILED "org.freedesktop.DBus.Error.AuthFailed" +#define DBUS_ERROR_NO_SERVER "org.freedesktop.DBus.Error.NoServer" +#define DBUS_ERROR_TIMEOUT "org.freedesktop.DBus.Error.Timeout" +#define DBUS_ERROR_DISCONNECTED "org.freedesktop.DBus.Error.Disconnected" +#define DBUS_ERROR_INVALID_ARGS "org.freedesktop.DBus.Error.InvalidArgs" +#define DBUS_ERROR_UNKNOWN_METHOD "org.freedesktop.DBus.Error.UnknownMethod" +#define DBUS_ERROR_UNKNOWN_OBJECT "org.freedesktop.DBus.Error.UnknownObject" +#define DBUS_ERROR_UNKNOWN_INTERFACE "org.freedesktop.DBus.Error.UnknownInterface" +#define DBUS_ERROR_UNKNOWN_PROPERTY "org.freedesktop.DBus.Error.UnknownProperty" +#define DBUS_ERROR_PROPERTY_READ_ONLY "org.freedesktop.DBus.Error.PropertyReadOnly" +#define DBUS_ERROR_INVALID_SIGNATURE "org.freedesktop.DBus.Error.InvalidSignature" + +/* ------------------------------------------------------------------ */ +/* Name registration */ +/* ------------------------------------------------------------------ */ + +#define DBUS_NAME_FLAG_ALLOW_REPLACEMENT 0x1 +#define DBUS_NAME_FLAG_REPLACE_EXISTING 0x2 +#define DBUS_NAME_FLAG_DO_NOT_QUEUE 0x4 + +#define DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER 1 +#define DBUS_REQUEST_NAME_REPLY_IN_QUEUE 2 +#define DBUS_REQUEST_NAME_REPLY_EXISTS 3 +#define DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER 4 + +#define DBUS_RELEASE_NAME_REPLY_RELEASED 1 +#define DBUS_RELEASE_NAME_REPLY_NON_EXISTENT 2 +#define DBUS_RELEASE_NAME_REPLY_NOT_OWNER 3 + +/* ------------------------------------------------------------------ */ +/* Opaque objects */ +/* ------------------------------------------------------------------ */ + +typedef struct DBusConnection DBusConnection; +typedef struct DBusMessage DBusMessage; + +typedef enum { + DBUS_BUS_SESSION, + DBUS_BUS_SYSTEM, + DBUS_BUS_STARTER +} DBusBusType; + +/* ------------------------------------------------------------------ */ +/* DBusError */ +/* ------------------------------------------------------------------ */ + +/* + * Layout copied from the reference so callers can allocate one on the + * stack. `name` and `message` are the only fields a caller reads. + */ +typedef struct { + const char *name; + const char *message; + + unsigned int dummy1 : 1; + unsigned int dummy2 : 1; + unsigned int dummy3 : 1; + unsigned int dummy4 : 1; + unsigned int dummy5 : 1; + + void *padding1; +} DBusError; + +void dbus_error_init(DBusError *error); +void dbus_error_free(DBusError *error); +dbus_bool_t dbus_error_is_set(const DBusError *error); +dbus_bool_t dbus_error_has_name(const DBusError *error, const char *name); +void dbus_set_error_const(DBusError *error, const char *name, const char *message); +void dbus_set_error(DBusError *error, const char *name, const char *format, ...); +void dbus_move_error(DBusError *src, DBusError *dest); + +/* ------------------------------------------------------------------ */ +/* DBusMessageIter */ +/* ------------------------------------------------------------------ */ + +/* + * Layout copied from the reference, for the same reason as DBusError: it + * is a stack value. The fields are opaque; this implementation stores its + * own state in them and never exposes their meaning. + * + * As in the reference, an iterator is only valid while the message it was + * created from is alive, and there is no function to release one. + */ +typedef struct { + void *dummy1; + void *dummy2; + dbus_uint32_t dummy3; + int dummy4; + int dummy5; + int dummy6; + int dummy7; + int dummy8; + int dummy9; + int dummy10; + int dummy11; + int pad1; + void *pad2; + void *pad3; +} DBusMessageIter; + +/* ------------------------------------------------------------------ */ +/* Memory */ +/* ------------------------------------------------------------------ */ + +void *dbus_malloc(size_t bytes); +void *dbus_malloc0(size_t bytes); +void dbus_free(void *memory); +void dbus_free_string_array(char **string_array); +void dbus_shutdown(void); + +/* + * Provided as a no-op returning TRUE. The reference needs an explicit + * opt-in to thread safety; this implementation is thread safe by + * construction, so there is nothing to initialize, and callers that + * dutifully call it keep working. + */ +dbus_bool_t dbus_threads_init_default(void); + +/* ------------------------------------------------------------------ */ +/* Messages */ +/* ------------------------------------------------------------------ */ + +DBusMessage *dbus_message_new(int message_type); +DBusMessage *dbus_message_new_method_call(const char *destination, + const char *path, + const char *interface, + const char *method); +DBusMessage *dbus_message_new_method_return(DBusMessage *method_call); +DBusMessage *dbus_message_new_signal(const char *path, + const char *interface, + const char *name); +DBusMessage *dbus_message_new_error(DBusMessage *reply_to, + const char *error_name, + const char *error_message); + +DBusMessage *dbus_message_ref(DBusMessage *message); +void dbus_message_unref(DBusMessage *message); +DBusMessage *dbus_message_copy(const DBusMessage *message); + +int dbus_message_get_type(DBusMessage *message); +dbus_uint32_t dbus_message_get_serial(DBusMessage *message); +dbus_uint32_t dbus_message_get_reply_serial(DBusMessage *message); + +dbus_bool_t dbus_message_set_path(DBusMessage *message, const char *path); +const char *dbus_message_get_path(DBusMessage *message); +dbus_bool_t dbus_message_set_interface(DBusMessage *message, const char *interface); +const char *dbus_message_get_interface(DBusMessage *message); +dbus_bool_t dbus_message_set_member(DBusMessage *message, const char *member); +const char *dbus_message_get_member(DBusMessage *message); +dbus_bool_t dbus_message_set_destination(DBusMessage *message, const char *destination); +const char *dbus_message_get_destination(DBusMessage *message); +dbus_bool_t dbus_message_set_sender(DBusMessage *message, const char *sender); +const char *dbus_message_get_sender(DBusMessage *message); +dbus_bool_t dbus_message_set_error_name(DBusMessage *message, const char *name); +const char *dbus_message_get_error_name(DBusMessage *message); +const char *dbus_message_get_signature(DBusMessage *message); + +void dbus_message_set_no_reply(DBusMessage *message, dbus_bool_t no_reply); +dbus_bool_t dbus_message_get_no_reply(DBusMessage *message); + +dbus_bool_t dbus_message_is_method_call(DBusMessage *message, + const char *interface, + const char *method); +dbus_bool_t dbus_message_is_signal(DBusMessage *message, + const char *interface, + const char *signal_name); +dbus_bool_t dbus_message_is_error(DBusMessage *message, const char *error_name); +dbus_bool_t dbus_message_has_path(DBusMessage *message, const char *path); +dbus_bool_t dbus_message_has_member(DBusMessage *message, const char *member); +dbus_bool_t dbus_message_has_interface(DBusMessage *message, const char *interface); +dbus_bool_t dbus_message_has_destination(DBusMessage *message, const char *name); +dbus_bool_t dbus_message_has_sender(DBusMessage *message, const char *name); + +/* ------------------------------------------------------------------ */ +/* Reading and writing arguments */ +/* ------------------------------------------------------------------ */ + +dbus_bool_t dbus_message_iter_init(DBusMessage *message, DBusMessageIter *iter); +dbus_bool_t dbus_message_iter_has_next(DBusMessageIter *iter); +dbus_bool_t dbus_message_iter_next(DBusMessageIter *iter); +int dbus_message_iter_get_arg_type(DBusMessageIter *iter); +int dbus_message_iter_get_element_type(DBusMessageIter *iter); +void dbus_message_iter_recurse(DBusMessageIter *iter, DBusMessageIter *sub); +char *dbus_message_iter_get_signature(DBusMessageIter *iter); +void dbus_message_iter_get_basic(DBusMessageIter *iter, void *value); + +void dbus_message_iter_init_append(DBusMessage *message, DBusMessageIter *iter); +dbus_bool_t dbus_message_iter_append_basic(DBusMessageIter *iter, int type, const void *value); +dbus_bool_t dbus_message_iter_open_container(DBusMessageIter *iter, + int type, + const char *contained_signature, + DBusMessageIter *sub); +dbus_bool_t dbus_message_iter_close_container(DBusMessageIter *iter, DBusMessageIter *sub); +void dbus_message_iter_abandon_container(DBusMessageIter *iter, DBusMessageIter *sub); + +/* + * The variadic argument helpers. Both accept basic types, and arrays of a + * basic type given as (DBUS_TYPE_ARRAY, element_type, &pointer, count) when + * appending and (DBUS_TYPE_ARRAY, element_type, &pointer, &count) when + * reading. Nested containers beyond that are expressible only through the + * iterator functions above, exactly as in the reference. + */ +dbus_bool_t dbus_message_append_args(DBusMessage *message, int first_arg_type, ...); +dbus_bool_t dbus_message_append_args_valist(DBusMessage *message, int first_arg_type, va_list var_args); +dbus_bool_t dbus_message_get_args(DBusMessage *message, DBusError *error, int first_arg_type, ...); +dbus_bool_t dbus_message_get_args_valist(DBusMessage *message, DBusError *error, int first_arg_type, va_list var_args); + +/* ------------------------------------------------------------------ */ +/* Connections */ +/* ------------------------------------------------------------------ */ + +DBusConnection *dbus_bus_get(DBusBusType type, DBusError *error); +DBusConnection *dbus_bus_get_private(DBusBusType type, DBusError *error); +DBusConnection *dbus_connection_open(const char *address, DBusError *error); +DBusConnection *dbus_connection_open_private(const char *address, DBusError *error); + +DBusConnection *dbus_connection_ref(DBusConnection *connection); +void dbus_connection_unref(DBusConnection *connection); +void dbus_connection_close(DBusConnection *connection); + +dbus_bool_t dbus_connection_get_is_connected(DBusConnection *connection); +dbus_bool_t dbus_connection_get_is_authenticated(DBusConnection *connection); +char *dbus_connection_get_server_id(DBusConnection *connection); + +dbus_bool_t dbus_connection_send(DBusConnection *connection, + DBusMessage *message, + dbus_uint32_t *serial); +DBusMessage *dbus_connection_send_with_reply_and_block(DBusConnection *connection, + DBusMessage *message, + int timeout_milliseconds, + DBusError *error); +void dbus_connection_flush(DBusConnection *connection); + +/* ------------------------------------------------------------------ */ +/* Bus operations */ +/* ------------------------------------------------------------------ */ + +dbus_bool_t dbus_bus_register(DBusConnection *connection, DBusError *error); +const char *dbus_bus_get_unique_name(DBusConnection *connection); +char *dbus_bus_get_id(DBusConnection *connection, DBusError *error); + +int dbus_bus_request_name(DBusConnection *connection, + const char *name, + unsigned int flags, + DBusError *error); +int dbus_bus_release_name(DBusConnection *connection, + const char *name, + DBusError *error); + +dbus_bool_t dbus_bus_name_has_owner(DBusConnection *connection, + const char *name, + DBusError *error); +dbus_bool_t dbus_bus_start_service_by_name(DBusConnection *connection, + const char *name, + dbus_uint32_t flags, + dbus_uint32_t *result, + DBusError *error); + +void dbus_bus_add_match(DBusConnection *connection, const char *rule, DBusError *error); +void dbus_bus_remove_match(DBusConnection *connection, const char *rule, DBusError *error); + +unsigned long dbus_bus_get_unix_user(DBusConnection *connection, + const char *name, + DBusError *error); + +#ifdef __cplusplus +} +#endif + +#endif /* DBUS_H */ diff --git a/Sources/CDBusABI/include/module.modulemap b/Sources/CDBusABI/include/module.modulemap new file mode 100644 index 0000000..96337e7 --- /dev/null +++ b/Sources/CDBusABI/include/module.modulemap @@ -0,0 +1,4 @@ +module CDBusABI { + header "dbus/dbus.h" + export * +} diff --git a/Sources/CDBusABI/varargs.c b/Sources/CDBusABI/varargs.c new file mode 100644 index 0000000..cf6c9cd --- /dev/null +++ b/Sources/CDBusABI/varargs.c @@ -0,0 +1,332 @@ +/* + * varargs.c + * DBus + * + * The variadic entry points of the libdbus-1 ABI. + * + * Swift cannot declare a C variadic function, so these four live in C and + * are implemented entirely in terms of the iterator entry points, which are + * Swift. There is no protocol logic here: this file walks a `va_list` and + * makes the same calls a caller could have made by hand. + */ + +#include "include/dbus/dbus.h" + +#include +#include +#include + +/* Implemented in Swift; sets an error from two NUL terminated strings. */ +extern void _dbus_abi_set_error(DBusError *error, const char *name, const char *message); + +void dbus_set_error(DBusError *error, const char *name, const char *format, ...) +{ + if (error == NULL) { + return; + } + + if (format == NULL) { + _dbus_abi_set_error(error, name, ""); + return; + } + + va_list args; + va_start(args, format); + + va_list measure; + va_copy(measure, args); + int length = vsnprintf(NULL, 0, format, measure); + va_end(measure); + + if (length < 0) { + va_end(args); + _dbus_abi_set_error(error, name, ""); + return; + } + + char *buffer = malloc((size_t) length + 1); + if (buffer == NULL) { + va_end(args); + _dbus_abi_set_error(error, DBUS_ERROR_NO_MEMORY, "Out of memory"); + return; + } + + vsnprintf(buffer, (size_t) length + 1, format, args); + va_end(args); + + _dbus_abi_set_error(error, name, buffer); + free(buffer); +} + +/* Whether a type code is a fixed size basic type passed by pointer. */ +static int is_basic_type(int type) +{ + switch (type) { + case DBUS_TYPE_BYTE: + case DBUS_TYPE_BOOLEAN: + case DBUS_TYPE_INT16: + case DBUS_TYPE_UINT16: + case DBUS_TYPE_INT32: + case DBUS_TYPE_UINT32: + case DBUS_TYPE_INT64: + case DBUS_TYPE_UINT64: + case DBUS_TYPE_DOUBLE: + case DBUS_TYPE_UNIX_FD: + case DBUS_TYPE_STRING: + case DBUS_TYPE_OBJECT_PATH: + case DBUS_TYPE_SIGNATURE: + return 1; + default: + return 0; + } +} + +/* The width of one element of a fixed size array, for the array helpers. */ +static size_t element_size(int type) +{ + switch (type) { + case DBUS_TYPE_BYTE: + return 1; + case DBUS_TYPE_INT16: + case DBUS_TYPE_UINT16: + return 2; + case DBUS_TYPE_BOOLEAN: + case DBUS_TYPE_INT32: + case DBUS_TYPE_UINT32: + case DBUS_TYPE_UNIX_FD: + return 4; + case DBUS_TYPE_INT64: + case DBUS_TYPE_UINT64: + case DBUS_TYPE_DOUBLE: + return 8; + default: + return 0; + } +} + +dbus_bool_t dbus_message_append_args_valist(DBusMessage *message, + int first_arg_type, + va_list var_args) +{ + if (message == NULL) { + return FALSE; + } + + DBusMessageIter iter; + dbus_message_iter_init_append(message, &iter); + + int type = first_arg_type; + + while (type != DBUS_TYPE_INVALID) { + + if (is_basic_type(type)) { + + const void *value = va_arg(var_args, const void *); + if (!dbus_message_iter_append_basic(&iter, type, value)) { + return FALSE; + } + } + else if (type == DBUS_TYPE_ARRAY) { + + int element = va_arg(var_args, int); + /* + * The reference takes the *address of* the array pointer here, + * not the array itself, so this dereferences once to reach the + * elements. + */ + const void *const *arrayPointer = va_arg(var_args, const void *const *); + int count = va_arg(var_args, int); + + const void *array = (arrayPointer != NULL) ? *arrayPointer : NULL; + + if (array == NULL && count > 0) { + return FALSE; + } + + char signature[2] = { (char) element, '\0' }; + + DBusMessageIter sub; + if (!dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, signature, &sub)) { + return FALSE; + } + + if (element == DBUS_TYPE_STRING || + element == DBUS_TYPE_OBJECT_PATH || + element == DBUS_TYPE_SIGNATURE) { + + /* An array of strings arrives as `const char **`. */ + const char *const *strings = (const char *const *) array; + for (int index = 0; index < count; index++) { + const char *entry = strings[index]; + if (!dbus_message_iter_append_basic(&sub, element, &entry)) { + dbus_message_iter_abandon_container(&iter, &sub); + return FALSE; + } + } + } + else { + + size_t width = element_size(element); + if (width == 0) { + dbus_message_iter_abandon_container(&iter, &sub); + return FALSE; + } + + const unsigned char *bytes = (const unsigned char *) array; + for (int index = 0; index < count; index++) { + if (!dbus_message_iter_append_basic(&sub, element, bytes + (size_t) index * width)) { + dbus_message_iter_abandon_container(&iter, &sub); + return FALSE; + } + } + } + + if (!dbus_message_iter_close_container(&iter, &sub)) { + return FALSE; + } + } + else { + /* Nested containers are expressible only through the iterators. */ + return FALSE; + } + + type = va_arg(var_args, int); + } + + return TRUE; +} + +dbus_bool_t dbus_message_append_args(DBusMessage *message, int first_arg_type, ...) +{ + va_list args; + va_start(args, first_arg_type); + dbus_bool_t result = dbus_message_append_args_valist(message, first_arg_type, args); + va_end(args); + return result; +} + +dbus_bool_t dbus_message_get_args_valist(DBusMessage *message, + DBusError *error, + int first_arg_type, + va_list var_args) +{ + if (message == NULL) { + _dbus_abi_set_error(error, DBUS_ERROR_INVALID_ARGS, "No message"); + return FALSE; + } + + DBusMessageIter iter; + dbus_bool_t hasArguments = dbus_message_iter_init(message, &iter); + + int type = first_arg_type; + + while (type != DBUS_TYPE_INVALID) { + + if (!hasArguments) { + _dbus_abi_set_error(error, DBUS_ERROR_INVALID_ARGS, + "Message has too few arguments"); + return FALSE; + } + + int actual = dbus_message_iter_get_arg_type(&iter); + + if (type == DBUS_TYPE_ARRAY) { + + int element = va_arg(var_args, int); + void *out = va_arg(var_args, void *); + int *count = va_arg(var_args, int *); + + if (actual != DBUS_TYPE_ARRAY || + dbus_message_iter_get_element_type(&iter) != element) { + _dbus_abi_set_error(error, DBUS_ERROR_INVALID_ARGS, + "Message argument is not an array of the expected type"); + return FALSE; + } + + if (element != DBUS_TYPE_STRING && + element != DBUS_TYPE_OBJECT_PATH && + element != DBUS_TYPE_SIGNATURE) { + _dbus_abi_set_error(error, DBUS_ERROR_NOT_SUPPORTED, + "Only arrays of strings can be read this way; " + "use dbus_message_iter_recurse"); + return FALSE; + } + + DBusMessageIter sub; + dbus_message_iter_recurse(&iter, &sub); + + int capacity = 8; + int length = 0; + char **strings = malloc(sizeof(char *) * (size_t) capacity); + if (strings == NULL) { + _dbus_abi_set_error(error, DBUS_ERROR_NO_MEMORY, "Out of memory"); + return FALSE; + } + + while (dbus_message_iter_get_arg_type(&sub) != DBUS_TYPE_INVALID) { + + const char *value = NULL; + dbus_message_iter_get_basic(&sub, &value); + + if (length + 1 >= capacity) { + capacity *= 2; + char **grown = realloc(strings, sizeof(char *) * (size_t) capacity); + if (grown == NULL) { + dbus_free_string_array(strings); + _dbus_abi_set_error(error, DBUS_ERROR_NO_MEMORY, "Out of memory"); + return FALSE; + } + strings = grown; + } + + strings[length] = strdup(value != NULL ? value : ""); + if (strings[length] == NULL) { + strings[length] = NULL; + dbus_free_string_array(strings); + _dbus_abi_set_error(error, DBUS_ERROR_NO_MEMORY, "Out of memory"); + return FALSE; + } + + length++; + dbus_message_iter_next(&sub); + } + + strings[length] = NULL; + + *(char ***) out = strings; + if (count != NULL) { + *count = length; + } + } + else if (is_basic_type(type)) { + + if (actual != type) { + _dbus_abi_set_error(error, DBUS_ERROR_INVALID_ARGS, + "Message argument is not of the expected type"); + return FALSE; + } + + void *out = va_arg(var_args, void *); + dbus_message_iter_get_basic(&iter, out); + } + else { + _dbus_abi_set_error(error, DBUS_ERROR_NOT_SUPPORTED, + "Type is not readable this way; use dbus_message_iter_recurse"); + return FALSE; + } + + hasArguments = dbus_message_iter_next(&iter); + type = va_arg(var_args, int); + } + + return TRUE; +} + +dbus_bool_t dbus_message_get_args(DBusMessage *message, DBusError *error, int first_arg_type, ...) +{ + va_list args; + va_start(args, first_arg_type); + dbus_bool_t result = dbus_message_get_args_valist(message, error, first_arg_type, args); + va_end(args); + return result; +} diff --git a/Sources/DBus/BusType.swift b/Sources/DBus/BusType.swift index 20c5688..5ece48c 100644 --- a/Sources/DBus/BusType.swift +++ b/Sources/DBus/BusType.swift @@ -7,7 +7,7 @@ // /// Well-known D-Bus bus types. -public enum DBusBusType: UInt32 { +public enum DBusBusType: UInt32, Sendable { /// The login session bus. case session diff --git a/Sources/DBusABI/Bus.swift b/Sources/DBusABI/Bus.swift new file mode 100644 index 0000000..0f0892c --- /dev/null +++ b/Sources/DBusABI/Bus.swift @@ -0,0 +1,280 @@ +// +// Bus.swift +// DBus +// +// `dbus_bus_*` — the operations the message bus itself provides. +// + +import Foundation +import CDBusABI +import DBus + +/// `dbus_bool_t dbus_bus_register(DBusConnection *connection, DBusError *error)` +/// +/// Reports whether the connection is registered with the bus. Connecting +/// performs the `Hello` handshake here, so a connection that exists at all is +/// already registered and this always succeeds for one — but callers written +/// against the reference call it, so it is provided rather than omitted. +@_cdecl("dbus_bus_register") +public func abi_dbus_bus_register(_ pointer: OpaquePointer?, + _ error: UnsafeMutablePointer?) -> dbus_bool_t { + + guard let box = connection(pointer) else { + setError(error, name: DBus.DBusError.Name.invalidArguments.rawValue, message: "No connection") + return false.cBool + } + + let connection = box.connection + + do { + guard try blocking({ await connection.uniqueName }) != nil else { + setError(error, + name: DBus.DBusError.Name.failed.rawValue, + message: "The connection has no unique name") + return false.cBool + } + + return true.cBool + } + catch let thrown { + setError(error, from: thrown) + return false.cBool + } +} + +/// `const char *dbus_bus_get_unique_name(DBusConnection *connection)` +/// +/// Borrowed, valid while the connection is. +@_cdecl("dbus_bus_get_unique_name") +public func abi_dbus_bus_get_unique_name(_ pointer: OpaquePointer?) -> UnsafePointer? { + + guard let box = connection(pointer) + else { return nil } + + let connection = box.connection + let name = try? blocking { await connection.uniqueName } + + return box.borrowedUniqueName((name ?? nil)?.rawValue) +} + +/// `char *dbus_bus_get_id(DBusConnection *connection, DBusError *error)` +/// +/// The caller releases the result with `dbus_free`. +@_cdecl("dbus_bus_get_id") +public func abi_dbus_bus_get_id(_ pointer: OpaquePointer?, + _ error: UnsafeMutablePointer?) -> UnsafeMutablePointer? { + + guard let box = connection(pointer) else { + setError(error, name: DBus.DBusError.Name.invalidArguments.rawValue, message: "No connection") + return nil + } + + let connection = box.connection + + do { + return try blocking { try await connection.getBusID() }.copiedCString() + } + catch let thrown { + setError(error, from: thrown) + return nil + } +} + +/// `int dbus_bus_request_name(DBusConnection *, const char *, unsigned int, DBusError *)` +/// +/// Returns one of the `DBUS_REQUEST_NAME_REPLY_*` values, or -1 on failure. +@_cdecl("dbus_bus_request_name") +public func abi_dbus_bus_request_name(_ pointer: OpaquePointer?, + _ name: UnsafePointer?, + _ flags: UInt32, + _ error: UnsafeMutablePointer?) -> Int32 { + + guard let box = connection(pointer), + let nameString = string(name), + let busName = DBusBusName(rawValue: nameString) else { + + setError(error, + name: DBus.DBusError.Name.invalidArguments.rawValue, + message: "Invalid bus name") + return -1 + } + + let connection = box.connection + let requestFlags = DBusConnection.RequestNameFlags(rawValue: flags) + + do { + let result = try blocking { try await connection.requestName(busName, flags: requestFlags) } + return Int32(result.rawValue) + } + catch let thrown { + setError(error, from: thrown) + return -1 + } +} + +/// `int dbus_bus_release_name(DBusConnection *, const char *, DBusError *)` +@_cdecl("dbus_bus_release_name") +public func abi_dbus_bus_release_name(_ pointer: OpaquePointer?, + _ name: UnsafePointer?, + _ error: UnsafeMutablePointer?) -> Int32 { + + guard let box = connection(pointer), + let nameString = string(name), + let busName = DBusBusName(rawValue: nameString) else { + + setError(error, + name: DBus.DBusError.Name.invalidArguments.rawValue, + message: "Invalid bus name") + return -1 + } + + let connection = box.connection + + do { + let result = try blocking { try await connection.releaseName(busName) } + return Int32(result.rawValue) + } + catch let thrown { + setError(error, from: thrown) + return -1 + } +} + +/// `dbus_bool_t dbus_bus_name_has_owner(DBusConnection *, const char *, DBusError *)` +@_cdecl("dbus_bus_name_has_owner") +public func abi_dbus_bus_name_has_owner(_ pointer: OpaquePointer?, + _ name: UnsafePointer?, + _ error: UnsafeMutablePointer?) -> dbus_bool_t { + + guard let box = connection(pointer), + let nameString = string(name), + let busName = DBusBusName(rawValue: nameString) else { + + setError(error, + name: DBus.DBusError.Name.invalidArguments.rawValue, + message: "Invalid bus name") + return false.cBool + } + + let connection = box.connection + + do { + return try blocking { try await connection.nameHasOwner(busName) }.cBool + } + catch let thrown { + setError(error, from: thrown) + return false.cBool + } +} + +/// `dbus_bool_t dbus_bus_start_service_by_name(DBusConnection *, const char *, dbus_uint32_t, dbus_uint32_t *, DBusError *)` +@_cdecl("dbus_bus_start_service_by_name") +public func abi_dbus_bus_start_service_by_name(_ pointer: OpaquePointer?, + _ name: UnsafePointer?, + _ flags: dbus_uint32_t, + _ result: UnsafeMutablePointer?, + _ error: UnsafeMutablePointer?) -> dbus_bool_t { + + guard let box = connection(pointer), + let nameString = string(name), + let busName = DBusBusName(rawValue: nameString) else { + + setError(error, + name: DBus.DBusError.Name.invalidArguments.rawValue, + message: "Invalid bus name") + return false.cBool + } + + let connection = box.connection + + do { + let value = try blocking { try await connection.startServiceByName(busName, flags: flags) } + result?.pointee = value + return true.cBool + } + catch let thrown { + setError(error, from: thrown) + return false.cBool + } +} + +/// `unsigned long dbus_bus_get_unix_user(DBusConnection *, const char *, DBusError *)` +@_cdecl("dbus_bus_get_unix_user") +public func abi_dbus_bus_get_unix_user(_ pointer: OpaquePointer?, + _ name: UnsafePointer?, + _ error: UnsafeMutablePointer?) -> UInt { + + guard let box = connection(pointer), + let nameString = string(name), + let busName = DBusBusName(rawValue: nameString) else { + + setError(error, + name: DBus.DBusError.Name.invalidArguments.rawValue, + message: "Invalid bus name") + return UInt.max + } + + let connection = box.connection + + do { + return UInt(try blocking { try await connection.getConnectionUnixUser(busName) }) + } + catch let thrown { + setError(error, from: thrown) + return UInt.max + } +} + +// MARK: - Match rules + +/// Call `AddMatch` or `RemoveMatch` on the bus. +private func match(_ pointer: OpaquePointer?, + _ rule: UnsafePointer?, + _ error: UnsafeMutablePointer?, + method: String) { + + guard let box = connection(pointer), let ruleString = string(rule) else { + setError(error, name: DBus.DBusError.Name.invalidArguments.rawValue, message: "No match rule") + return + } + + let connection = box.connection + + let call = DBus.DBusMessage.MethodCall( + destination: DBusBusName(rawValue: "org.freedesktop.DBus")!, + path: DBusObjectPath(rawValue: "/org/freedesktop/DBus")!, + interface: DBusInterface(rawValue: "org.freedesktop.DBus")!, + method: DBusMember(rawValue: method)! + ) + + let request = DBus.DBusMessage(methodCall: call, arguments: [.string(ruleString)]) + + do { + _ = try blocking { try await connection.send(request) } + } + catch let thrown { + setError(error, from: thrown) + } +} + +/// `void dbus_bus_add_match(DBusConnection *connection, const char *rule, DBusError *error)` +/// +/// The rule is installed on the bus. Delivered signals are readable through +/// this package's Swift API; the C ABI has no message queue to pop them from, +/// because it does not expose the dispatch loop. +@_cdecl("dbus_bus_add_match") +public func abi_dbus_bus_add_match(_ connection: OpaquePointer?, + _ rule: UnsafePointer?, + _ error: UnsafeMutablePointer?) { + + match(connection, rule, error, method: "AddMatch") +} + +/// `void dbus_bus_remove_match(DBusConnection *connection, const char *rule, DBusError *error)` +@_cdecl("dbus_bus_remove_match") +public func abi_dbus_bus_remove_match(_ connection: OpaquePointer?, + _ rule: UnsafePointer?, + _ error: UnsafeMutablePointer?) { + + match(connection, rule, error, method: "RemoveMatch") +} diff --git a/Sources/DBusABI/Connection.swift b/Sources/DBusABI/Connection.swift new file mode 100644 index 0000000..1dedf3c --- /dev/null +++ b/Sources/DBusABI/Connection.swift @@ -0,0 +1,340 @@ +// +// Connection.swift +// DBus +// +// `dbus_connection_*` — opening, closing and the blocking send. +// + +import Foundation +import CDBusABI +import DBus + +// MARK: - Storage + +/// The object a `DBusConnection *` points at. +internal final class ConnectionBox: Box { + + let connection: DBusConnection + + /// Cached so `dbus_bus_get_unique_name` can return a borrowed pointer. + private var uniqueName: UnsafeMutablePointer? + + /// Whether this connection came from the shared `dbus_bus_get` cache. + let isShared: Bool + + init(_ connection: DBusConnection, isShared: Bool) { + + self.connection = connection + self.isShared = isShared + } + + deinit { + + free(uniqueName) + + // The read loop holds the socket open until the actor is told to stop. + let connection = self.connection + Task { await connection.close() } + } + + /// A borrowed C string for the unique name, valid while the connection is. + func borrowedUniqueName(_ value: String?) -> UnsafePointer? { + + guard let value = value + else { return nil } + + if let existing = uniqueName, strcmp(existing, value) == 0 { + return UnsafePointer(existing) + } + + free(uniqueName) + uniqueName = strdup(value) + + return UnsafePointer(uniqueName) + } +} + +internal func connection(_ pointer: OpaquePointer?) -> ConnectionBox? { + + guard let pointer = pointer + else { return nil } + + return Box.unretained(pointer) +} + +/// The shared connections `dbus_bus_get` hands out, one per bus type. +/// +/// The reference caches these and returns the same object to every caller, so +/// a program that calls `dbus_bus_get(DBUS_BUS_SESSION, ...)` twice gets one +/// connection with two references, not two connections. +private final class SharedConnections: @unchecked Sendable { + + static let shared = SharedConnections() + + private let lock = NSLock() + private var connections: [Int32: ConnectionBox] = [:] + + func connection(for busType: Int32, _ make: () throws -> ConnectionBox) rethrows -> ConnectionBox { + + lock.lock() + defer { lock.unlock() } + + if let existing = connections[busType] { + return existing + } + + let box = try make() + connections[busType] = box + return box + } +} + +// MARK: - Opening + +private func busType(_ raw: Int32) -> DBus.DBusBusType? { + + switch raw { + case Int32(CDBusABI.DBUS_BUS_SESSION.rawValue): return .session + case Int32(CDBusABI.DBUS_BUS_SYSTEM.rawValue): return .system + // The reference falls back to the session bus when DBUS_STARTER_ADDRESS + // is unset, which is the case for anything not activated by the bus. + case Int32(CDBusABI.DBUS_BUS_STARTER.rawValue): return .session + default: return nil + } +} + +/// `DBusConnection *dbus_bus_get(DBusBusType type, DBusError *error)` +/// +/// Returns a shared connection, already registered with the bus. The caller +/// owns a reference to it and releases it with `dbus_connection_unref`. +@_cdecl("dbus_bus_get") +public func abi_dbus_bus_get(_ type: Int32, + _ error: UnsafeMutablePointer?) -> OpaquePointer? { + + guard let busType = busType(type) else { + setError(error, name: DBus.DBusError.Name.failed.rawValue, message: "Unknown bus type") + return nil + } + + do { + let box = try SharedConnections.shared.connection(for: type) { + + // `connect` performs the Hello handshake, so the connection this + // returns is already registered with the bus. + let connection = try blocking { try await DBusConnection.connect(to: busType) } + return ConnectionBox(connection, isShared: true) + } + + // The cache holds one reference; the caller gets its own. + return box.retainedPointer() + } + catch let thrown { + setError(error, from: thrown) + return nil + } +} + +/// `DBusConnection *dbus_bus_get_private(DBusBusType type, DBusError *error)` +@_cdecl("dbus_bus_get_private") +public func abi_dbus_bus_get_private(_ type: Int32, + _ error: UnsafeMutablePointer?) -> OpaquePointer? { + + guard let busType = busType(type) else { + setError(error, name: DBus.DBusError.Name.failed.rawValue, message: "Unknown bus type") + return nil + } + + do { + let connection = try blocking { try await DBusConnection.connect(to: busType) } + return ConnectionBox(connection, isShared: false).retainedPointer() + } + catch let thrown { + setError(error, from: thrown) + return nil + } +} + +/// `DBusConnection *dbus_connection_open(const char *address, DBusError *error)` +/// +/// The connection is authenticated but not registered; call +/// `dbus_bus_register` before using it with a message bus. +@_cdecl("dbus_connection_open") +public func abi_dbus_connection_open(_ address: UnsafePointer?, + _ error: UnsafeMutablePointer?) -> OpaquePointer? { + + return abi_dbus_connection_open_private(address, error) +} + +/// `DBusConnection *dbus_connection_open_private(const char *address, DBusError *error)` +@_cdecl("dbus_connection_open_private") +public func abi_dbus_connection_open_private(_ address: UnsafePointer?, + _ error: UnsafeMutablePointer?) -> OpaquePointer? { + + guard let addressString = string(address) else { + setError(error, name: DBus.DBusError.Name.badAddress.rawValue, message: "No address") + return nil + } + + do { + let connection = try blocking { try await DBusConnection.connect(to: addressString) } + return ConnectionBox(connection, isShared: false).retainedPointer() + } + catch let thrown { + setError(error, from: thrown) + return nil + } +} + +// MARK: - Reference counting + +/// `DBusConnection *dbus_connection_ref(DBusConnection *connection)` +@_cdecl("dbus_connection_ref") +public func abi_dbus_connection_ref(_ connection: OpaquePointer?) -> OpaquePointer? { + + guard let connection = connection + else { return nil } + + Box.retain(connection) + return connection +} + +/// `void dbus_connection_unref(DBusConnection *connection)` +@_cdecl("dbus_connection_unref") +public func abi_dbus_connection_unref(_ connection: OpaquePointer?) { + + guard let connection = connection + else { return } + + Box.release(connection) +} + +/// `void dbus_connection_close(DBusConnection *connection)` +@_cdecl("dbus_connection_close") +public func abi_dbus_connection_close(_ pointer: OpaquePointer?) { + + guard let box = connection(pointer) + else { return } + + let connection = box.connection + _ = try? blocking { await connection.close() } +} + +// MARK: - State + +/// `dbus_bool_t dbus_connection_get_is_connected(DBusConnection *connection)` +@_cdecl("dbus_connection_get_is_connected") +public func abi_dbus_connection_get_is_connected(_ pointer: OpaquePointer?) -> dbus_bool_t { + + guard let box = connection(pointer) + else { return false.cBool } + + let connection = box.connection + let isConnected = (try? blocking { await connection.isConnected }) ?? false + + return isConnected.cBool +} + +/// `dbus_bool_t dbus_connection_get_is_authenticated(DBusConnection *connection)` +/// +/// A connection this implementation hands back has already completed the SASL +/// handshake, so this reports the same thing as `get_is_connected`. +@_cdecl("dbus_connection_get_is_authenticated") +public func abi_dbus_connection_get_is_authenticated(_ pointer: OpaquePointer?) -> dbus_bool_t { + + return abi_dbus_connection_get_is_connected(pointer) +} + +/// `char *dbus_connection_get_server_id(DBusConnection *connection)` +/// +/// The caller releases the result with `dbus_free`. +@_cdecl("dbus_connection_get_server_id") +public func abi_dbus_connection_get_server_id(_ pointer: OpaquePointer?) -> UnsafeMutablePointer? { + + guard let box = connection(pointer) + else { return nil } + + let connection = box.connection + let guid = try? blocking { await connection.serverGUID } + + return (guid ?? nil)?.copiedCString() +} + +// MARK: - Sending + +/// `dbus_bool_t dbus_connection_send(DBusConnection *, DBusMessage *, dbus_uint32_t *serial)` +/// +/// Sends without waiting for a reply. +@_cdecl("dbus_connection_send") +public func abi_dbus_connection_send(_ pointer: OpaquePointer?, + _ message: OpaquePointer?, + _ serial: UnsafeMutablePointer?) -> dbus_bool_t { + + guard let box = connection(pointer), let messageBox = DBusABI.message(message) + else { return false.cBool } + + let connection = box.connection + let value = messageBox.message + + do { + let assigned = try blocking { try await connection.send(oneWay: value) } + messageBox.message.serial = assigned + serial?.pointee = assigned + return true.cBool + } + catch { + return false.cBool + } +} + +/// `DBusMessage *dbus_connection_send_with_reply_and_block(DBusConnection *, DBusMessage *, int, DBusError *)` +/// +/// The caller owns the returned message and releases it with +/// `dbus_message_unref`. +@_cdecl("dbus_connection_send_with_reply_and_block") +public func abi_dbus_connection_send_with_reply_and_block( + _ pointer: OpaquePointer?, + _ message: OpaquePointer?, + _ timeoutMilliseconds: Int32, + _ error: UnsafeMutablePointer? +) -> OpaquePointer? { + + guard let box = connection(pointer), let messageBox = DBusABI.message(message) else { + setError(error, name: DBus.DBusError.Name.invalidArguments.rawValue, message: "No message") + return nil + } + + let connection = box.connection + let value = messageBox.message + + let timeout: Duration? + + switch timeoutMilliseconds { + case Int32(DBUS_TIMEOUT_USE_DEFAULT): + timeout = DBusConnection.defaultTimeout + case Int32(DBUS_TIMEOUT_INFINITE): + timeout = nil + case ..<0: + timeout = DBusConnection.defaultTimeout + default: + timeout = .milliseconds(Int(timeoutMilliseconds)) + } + + do { + let reply = try blocking { try await connection.send(value, timeout: timeout) } + return MessageBox(reply).retainedPointer() + } + catch let thrown { + setError(error, from: thrown) + return nil + } +} + +/// `void dbus_connection_flush(DBusConnection *connection)` +/// +/// A no-op: every send here completes before it returns, so there is never +/// anything queued to flush. +@_cdecl("dbus_connection_flush") +public func abi_dbus_connection_flush(_ connection: OpaquePointer?) { + + // Deliberately empty. +} diff --git a/Sources/DBusABI/Error.swift b/Sources/DBusABI/Error.swift new file mode 100644 index 0000000..c086bc6 --- /dev/null +++ b/Sources/DBusABI/Error.swift @@ -0,0 +1,103 @@ +// +// Error.swift +// DBus +// +// `dbus_error_*` — the C ABI's out-parameter error type. +// + +import Foundation +import CDBusABI +import DBus + +/// `void dbus_error_init(DBusError *error)` +@_cdecl("dbus_error_init") +public func abi_dbus_error_init(_ error: UnsafeMutablePointer?) { + + guard let error = error + else { return } + + error.pointee.name = nil + error.pointee.message = nil + error.pointee.padding1 = nil +} + +/// `void dbus_error_free(DBusError *error)` +/// +/// Returns the error to the unset state, so it may be reused, as in the +/// reference. +@_cdecl("dbus_error_free") +public func abi_dbus_error_free(_ error: UnsafeMutablePointer?) { + + freeError(error) +} + +/// `dbus_bool_t dbus_error_is_set(const DBusError *error)` +@_cdecl("dbus_error_is_set") +public func abi_dbus_error_is_set(_ error: UnsafePointer?) -> dbus_bool_t { + + guard let error = error + else { return false.cBool } + + return (error.pointee.name != nil).cBool +} + +/// `dbus_bool_t dbus_error_has_name(const DBusError *error, const char *name)` +@_cdecl("dbus_error_has_name") +public func abi_dbus_error_has_name(_ error: UnsafePointer?, + _ name: UnsafePointer?) -> dbus_bool_t { + + guard let error = error, + let actual = error.pointee.name, + let expected = name + else { return false.cBool } + + return (strcmp(actual, expected) == 0).cBool +} + +/// `void dbus_set_error_const(DBusError *error, const char *name, const char *message)` +@_cdecl("dbus_set_error_const") +public func abi_dbus_set_error_const(_ error: UnsafeMutablePointer?, + _ name: UnsafePointer?, + _ message: UnsafePointer?) { + + setError(error, + name: string(name) ?? DBus.DBusError.Name.failed.rawValue, + message: string(message) ?? "") +} + +/// The Swift half of the variadic `dbus_set_error`, which lives in C. +@_cdecl("_dbus_abi_set_error") +public func abi_set_error_bridge(_ error: UnsafeMutablePointer?, + _ name: UnsafePointer?, + _ message: UnsafePointer?) { + + abi_dbus_set_error_const(error, name, message) +} + +/// `void dbus_move_error(DBusError *src, DBusError *dest)` +/// +/// Hands ownership to `dest` and clears `src`. A null `dest` discards the +/// error, which is how the reference lets a caller ignore one. +@_cdecl("dbus_move_error") +public func abi_dbus_move_error(_ source: UnsafeMutablePointer?, + _ destination: UnsafeMutablePointer?) { + + guard let source = source + else { return } + + guard let destination = destination else { + freeError(source) + return + } + + freeError(destination) + + destination.pointee.name = source.pointee.name + destination.pointee.message = source.pointee.message + destination.pointee.padding1 = source.pointee.padding1 + + // Cleared without freeing: the strings belong to `destination` now. + source.pointee.name = nil + source.pointee.message = nil + source.pointee.padding1 = nil +} diff --git a/Sources/DBusABI/Memory.swift b/Sources/DBusABI/Memory.swift new file mode 100644 index 0000000..9f8a92a --- /dev/null +++ b/Sources/DBusABI/Memory.swift @@ -0,0 +1,85 @@ +// +// Memory.swift +// DBus +// +// `dbus_malloc` and friends. +// +// The reference routes every allocation it hands to a caller through these, +// so that a caller can free one with `dbus_free` regardless of which +// allocator the library was built against. They are plain wrappers here, +// which is all they need to be: every pointer this library returns comes +// from the same C allocator these use. +// + +import Foundation +import CDBusABI + +/// `void *dbus_malloc(size_t bytes)` +/// +/// Returns NULL for a zero byte request, as the reference does, so that +/// `dbus_free` on the result is always valid. +@_cdecl("dbus_malloc") +public func abi_dbus_malloc(_ bytes: Int) -> UnsafeMutableRawPointer? { + + guard bytes > 0 + else { return nil } + + return malloc(bytes) +} + +/// `void *dbus_malloc0(size_t bytes)` +@_cdecl("dbus_malloc0") +public func abi_dbus_malloc0(_ bytes: Int) -> UnsafeMutableRawPointer? { + + guard bytes > 0 + else { return nil } + + return calloc(1, bytes) +} + +/// `void dbus_free(void *memory)` +@_cdecl("dbus_free") +public func abi_dbus_free(_ memory: UnsafeMutableRawPointer?) { + + free(memory) +} + +/// `void dbus_free_string_array(char **string_array)` +/// +/// Frees a NULL terminated array of strings and the array itself. +@_cdecl("dbus_free_string_array") +public func abi_dbus_free_string_array(_ array: UnsafeMutablePointer?>?) { + + guard let array = array + else { return } + + var index = 0 + while let element = array[index] { + free(element) + index += 1 + } + + free(array) +} + +/// `dbus_bool_t dbus_threads_init_default(void)` +/// +/// A no-op returning TRUE. The reference needs an explicit opt-in before it is +/// safe to use from more than one thread; this implementation is thread safe +/// by construction, so there is nothing to switch on, and callers that +/// dutifully call it keep working. +@_cdecl("dbus_threads_init_default") +public func abi_dbus_threads_init_default() -> dbus_bool_t { + + return true.cBool +} + +/// `void dbus_shutdown(void)` +/// +/// A no-op. The reference frees global state that this implementation does not +/// keep; the shared connections are released by their own reference counts. +@_cdecl("dbus_shutdown") +public func abi_dbus_shutdown() { + + // Deliberately empty. +} diff --git a/Sources/DBusABI/Message.swift b/Sources/DBusABI/Message.swift new file mode 100644 index 0000000..612612a --- /dev/null +++ b/Sources/DBusABI/Message.swift @@ -0,0 +1,574 @@ +// +// Message.swift +// DBus +// +// `dbus_message_*` — construction, reference counting and header accessors. +// + +import Foundation +import CDBusABI +import DBus + +// MARK: - Storage + +/// The object a `DBusMessage *` points at. +/// +/// `DBus.DBusMessage` is a value type, so the box supplies the reference +/// identity the C ABI is built around. It also owns the strings returned by +/// the `dbus_message_get_*` accessors: those return a borrowed `const char *` +/// that the reference keeps alive as long as the message, so each is cached +/// here rather than freshly allocated and leaked. +internal final class MessageBox: Box { + + var message: DBus.DBusMessage + + /// Iterator states created from this message, kept alive until it dies. + /// + /// A `DBusMessageIter` is a stack value with no destructor, so its state + /// cannot be freed by the caller. The reference stores the state inline; + /// here it lives out of line and is tied to the message's lifetime, which + /// matches the documented rule that an iterator is valid only while its + /// message is. + var iterators: [IteratorState] = [] + + private var strings: [String: UnsafeMutablePointer] = [:] + + /// Copies handed out by `dbus_message_iter_get_basic` for string values. + /// + /// That entry point yields a borrowed pointer the caller must not free, so + /// each copy lives as long as the message, exactly as in the reference. + private var temporaries: [UnsafeMutablePointer] = [] + + init(_ message: DBus.DBusMessage) { + + self.message = message + } + + deinit { + + strings.values.forEach { free($0) } + temporaries.forEach { free($0) } + } + + /// A borrowed C string valid while the message is. + func borrowedCopy(_ value: String) -> UnsafePointer? { + + guard let copy = strdup(value) + else { return nil } + + temporaries.append(copy) + return UnsafePointer(copy) + } + + /// A borrowed C string for a header field, valid while the message is. + func borrowedString(_ key: String, _ value: String?) -> UnsafePointer? { + + guard let value = value else { + if let existing = strings.removeValue(forKey: key) { free(existing) } + return nil + } + + if let existing = strings[key], strcmp(existing, value) == 0 { + return UnsafePointer(existing) + } + + if let existing = strings.removeValue(forKey: key) { free(existing) } + + guard let copy = strdup(value) + else { return nil } + + strings[key] = copy + return UnsafePointer(copy) + } +} + +internal func message(_ pointer: OpaquePointer?) -> MessageBox? { + + guard let pointer = pointer + else { return nil } + + return Box.unretained(pointer) +} + +// MARK: - Construction + +/// `DBusMessage *dbus_message_new(int message_type)` +@_cdecl("dbus_message_new") +public func abi_dbus_message_new(_ messageType: Int32) -> OpaquePointer? { + + guard let type = DBusMessageType(rawValue: UInt8(truncatingIfNeeded: messageType)) + else { return nil } + + return MessageBox(DBus.DBusMessage(type: type)).retainedPointer() +} + +/// `DBusMessage *dbus_message_new_method_call(const char *, const char *, const char *, const char *)` +/// +/// Returns NULL if any argument is not valid for its field, which is what the +/// reference does for a malformed path, interface or member. +@_cdecl("dbus_message_new_method_call") +public func abi_dbus_message_new_method_call(_ destination: UnsafePointer?, + _ path: UnsafePointer?, + _ interface: UnsafePointer?, + _ method: UnsafePointer?) -> OpaquePointer? { + + guard let pathString = string(path), + let objectPath = DBusObjectPath(rawValue: pathString), + let methodString = string(method), + let member = DBusMember(rawValue: methodString) + else { return nil } + + var busName: DBusBusName? + if let destinationString = string(destination) { + guard let name = DBusBusName(rawValue: destinationString) + else { return nil } + busName = name + } + + var interfaceName: DBusInterface? + if let interfaceString = string(interface) { + guard let name = DBusInterface(rawValue: interfaceString) + else { return nil } + interfaceName = name + } + + let call = DBus.DBusMessage.MethodCall(destination: busName, + path: objectPath, + interface: interfaceName, + method: member) + + return MessageBox(DBus.DBusMessage(methodCall: call)).retainedPointer() +} + +/// `DBusMessage *dbus_message_new_method_return(DBusMessage *method_call)` +@_cdecl("dbus_message_new_method_return") +public func abi_dbus_message_new_method_return(_ methodCall: OpaquePointer?) -> OpaquePointer? { + + guard let call = message(methodCall) + else { return nil } + + var reply = DBus.DBusMessage(type: .methodReturn) + reply.replySerial = call.message.serial + reply.destination = call.message.sender + + return MessageBox(reply).retainedPointer() +} + +/// `DBusMessage *dbus_message_new_signal(const char *, const char *, const char *)` +@_cdecl("dbus_message_new_signal") +public func abi_dbus_message_new_signal(_ path: UnsafePointer?, + _ interface: UnsafePointer?, + _ name: UnsafePointer?) -> OpaquePointer? { + + guard let pathString = string(path), + let objectPath = DBusObjectPath(rawValue: pathString), + let interfaceString = string(interface), + let interfaceName = DBusInterface(rawValue: interfaceString), + let memberString = string(name), + let member = DBusMember(rawValue: memberString) + else { return nil } + + let signal = DBus.DBusMessage.Signal(path: objectPath, + interface: interfaceName, + name: member) + + return MessageBox(DBus.DBusMessage(signal: signal)).retainedPointer() +} + +/// `DBusMessage *dbus_message_new_error(DBusMessage *, const char *, const char *)` +@_cdecl("dbus_message_new_error") +public func abi_dbus_message_new_error(_ replyTo: OpaquePointer?, + _ errorName: UnsafePointer?, + _ errorMessage: UnsafePointer?) -> OpaquePointer? { + + guard let original = message(replyTo), + let nameString = string(errorName), + let name = DBus.DBusError.Name(rawValue: nameString) + else { return nil } + + var reply = DBus.DBusMessage(type: .error) + reply.errorName = name + reply.replySerial = original.message.serial + reply.destination = original.message.sender + + if let text = string(errorMessage) { + reply.arguments = [.string(text)] + } + + return MessageBox(reply).retainedPointer() +} + +// MARK: - Reference counting + +/// `DBusMessage *dbus_message_ref(DBusMessage *message)` +@_cdecl("dbus_message_ref") +public func abi_dbus_message_ref(_ message: OpaquePointer?) -> OpaquePointer? { + + guard let message = message + else { return nil } + + Box.retain(message) + return message +} + +/// `void dbus_message_unref(DBusMessage *message)` +@_cdecl("dbus_message_unref") +public func abi_dbus_message_unref(_ message: OpaquePointer?) { + + guard let message = message + else { return } + + Box.release(message) +} + +/// `DBusMessage *dbus_message_copy(const DBusMessage *message)` +@_cdecl("dbus_message_copy") +public func abi_dbus_message_copy(_ original: OpaquePointer?) -> OpaquePointer? { + + guard let box = message(original) + else { return nil } + + return MessageBox(box.message).retainedPointer() +} + +// MARK: - Header fields + +/// `int dbus_message_get_type(DBusMessage *message)` +@_cdecl("dbus_message_get_type") +public func abi_dbus_message_get_type(_ message: OpaquePointer?) -> Int32 { + + guard let box = DBusABI.message(message) + else { return Int32(DBUS_MESSAGE_TYPE_INVALID) } + + return Int32(box.message.type.rawValue) +} + +/// `dbus_uint32_t dbus_message_get_serial(DBusMessage *message)` +@_cdecl("dbus_message_get_serial") +public func abi_dbus_message_get_serial(_ message: OpaquePointer?) -> dbus_uint32_t { + + return DBusABI.message(message)?.message.serial ?? 0 +} + +/// `dbus_uint32_t dbus_message_get_reply_serial(DBusMessage *message)` +@_cdecl("dbus_message_get_reply_serial") +public func abi_dbus_message_get_reply_serial(_ message: OpaquePointer?) -> dbus_uint32_t { + + return DBusABI.message(message)?.message.replySerial ?? 0 +} + +/// `dbus_bool_t dbus_message_set_path(DBusMessage *message, const char *path)` +@_cdecl("dbus_message_set_path") +public func abi_dbus_message_set_path(_ message: OpaquePointer?, + _ path: UnsafePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message) + else { return false.cBool } + + guard let value = string(path) else { + box.message.path = nil + return true.cBool + } + + guard let objectPath = DBusObjectPath(rawValue: value) + else { return false.cBool } + + box.message.path = objectPath + return true.cBool +} + +/// `const char *dbus_message_get_path(DBusMessage *message)` +@_cdecl("dbus_message_get_path") +public func abi_dbus_message_get_path(_ message: OpaquePointer?) -> UnsafePointer? { + + guard let box = DBusABI.message(message) + else { return nil } + + return box.borrowedString("path", box.message.path?.rawValue) +} + +/// `dbus_bool_t dbus_message_set_interface(DBusMessage *message, const char *interface)` +@_cdecl("dbus_message_set_interface") +public func abi_dbus_message_set_interface(_ message: OpaquePointer?, + _ interface: UnsafePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message) + else { return false.cBool } + + guard let value = string(interface) else { + box.message.interface = nil + return true.cBool + } + + guard let name = DBusInterface(rawValue: value) + else { return false.cBool } + + box.message.interface = name + return true.cBool +} + +/// `const char *dbus_message_get_interface(DBusMessage *message)` +@_cdecl("dbus_message_get_interface") +public func abi_dbus_message_get_interface(_ message: OpaquePointer?) -> UnsafePointer? { + + guard let box = DBusABI.message(message) + else { return nil } + + return box.borrowedString("interface", box.message.interface?.rawValue) +} + +/// `dbus_bool_t dbus_message_set_member(DBusMessage *message, const char *member)` +@_cdecl("dbus_message_set_member") +public func abi_dbus_message_set_member(_ message: OpaquePointer?, + _ member: UnsafePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message) + else { return false.cBool } + + guard let value = string(member) else { + box.message.member = nil + return true.cBool + } + + guard let name = DBusMember(rawValue: value) + else { return false.cBool } + + box.message.member = name + return true.cBool +} + +/// `const char *dbus_message_get_member(DBusMessage *message)` +@_cdecl("dbus_message_get_member") +public func abi_dbus_message_get_member(_ message: OpaquePointer?) -> UnsafePointer? { + + guard let box = DBusABI.message(message) + else { return nil } + + return box.borrowedString("member", box.message.member?.rawValue) +} + +/// `dbus_bool_t dbus_message_set_destination(DBusMessage *message, const char *destination)` +@_cdecl("dbus_message_set_destination") +public func abi_dbus_message_set_destination(_ message: OpaquePointer?, + _ destination: UnsafePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message) + else { return false.cBool } + + guard let value = string(destination) else { + box.message.destination = nil + return true.cBool + } + + guard let name = DBusBusName(rawValue: value) + else { return false.cBool } + + box.message.destination = name + return true.cBool +} + +/// `const char *dbus_message_get_destination(DBusMessage *message)` +@_cdecl("dbus_message_get_destination") +public func abi_dbus_message_get_destination(_ message: OpaquePointer?) -> UnsafePointer? { + + guard let box = DBusABI.message(message) + else { return nil } + + return box.borrowedString("destination", box.message.destination?.rawValue) +} + +/// `dbus_bool_t dbus_message_set_sender(DBusMessage *message, const char *sender)` +@_cdecl("dbus_message_set_sender") +public func abi_dbus_message_set_sender(_ message: OpaquePointer?, + _ sender: UnsafePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message) + else { return false.cBool } + + guard let value = string(sender) else { + box.message.sender = nil + return true.cBool + } + + guard let name = DBusBusName(rawValue: value) + else { return false.cBool } + + box.message.sender = name + return true.cBool +} + +/// `const char *dbus_message_get_sender(DBusMessage *message)` +@_cdecl("dbus_message_get_sender") +public func abi_dbus_message_get_sender(_ message: OpaquePointer?) -> UnsafePointer? { + + guard let box = DBusABI.message(message) + else { return nil } + + return box.borrowedString("sender", box.message.sender?.rawValue) +} + +/// `dbus_bool_t dbus_message_set_error_name(DBusMessage *message, const char *name)` +@_cdecl("dbus_message_set_error_name") +public func abi_dbus_message_set_error_name(_ message: OpaquePointer?, + _ name: UnsafePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message) + else { return false.cBool } + + guard let value = string(name) else { + box.message.errorName = nil + return true.cBool + } + + guard let errorName = DBus.DBusError.Name(rawValue: value) + else { return false.cBool } + + box.message.errorName = errorName + return true.cBool +} + +/// `const char *dbus_message_get_error_name(DBusMessage *message)` +@_cdecl("dbus_message_get_error_name") +public func abi_dbus_message_get_error_name(_ message: OpaquePointer?) -> UnsafePointer? { + + guard let box = DBusABI.message(message) + else { return nil } + + return box.borrowedString("errorName", box.message.errorName?.rawValue) +} + +/// `const char *dbus_message_get_signature(DBusMessage *message)` +@_cdecl("dbus_message_get_signature") +public func abi_dbus_message_get_signature(_ message: OpaquePointer?) -> UnsafePointer? { + + guard let box = DBusABI.message(message) + else { return nil } + + return box.borrowedString("signature", box.message.signature.rawValue) +} + +/// `void dbus_message_set_no_reply(DBusMessage *message, dbus_bool_t no_reply)` +@_cdecl("dbus_message_set_no_reply") +public func abi_dbus_message_set_no_reply(_ message: OpaquePointer?, _ noReply: dbus_bool_t) { + + guard let box = DBusABI.message(message) + else { return } + + if noReply != 0 { + box.message.flags.insert(.noReplyExpected) + } else { + box.message.flags.remove(.noReplyExpected) + } +} + +/// `dbus_bool_t dbus_message_get_no_reply(DBusMessage *message)` +@_cdecl("dbus_message_get_no_reply") +public func abi_dbus_message_get_no_reply(_ message: OpaquePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message) + else { return false.cBool } + + return box.message.flags.contains(.noReplyExpected).cBool +} + +// MARK: - Predicates + +private func matches(_ actual: String?, _ expected: UnsafePointer?) -> Bool { + + guard let expected = string(expected) + else { return actual == nil } + + return actual == expected +} + +/// `dbus_bool_t dbus_message_is_method_call(DBusMessage *, const char *, const char *)` +@_cdecl("dbus_message_is_method_call") +public func abi_dbus_message_is_method_call(_ message: OpaquePointer?, + _ interface: UnsafePointer?, + _ method: UnsafePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message), box.message.type == .methodCall + else { return false.cBool } + + return (matches(box.message.interface?.rawValue, interface) + && matches(box.message.member?.rawValue, method)).cBool +} + +/// `dbus_bool_t dbus_message_is_signal(DBusMessage *, const char *, const char *)` +@_cdecl("dbus_message_is_signal") +public func abi_dbus_message_is_signal(_ message: OpaquePointer?, + _ interface: UnsafePointer?, + _ signalName: UnsafePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message), box.message.type == .signal + else { return false.cBool } + + return (matches(box.message.interface?.rawValue, interface) + && matches(box.message.member?.rawValue, signalName)).cBool +} + +/// `dbus_bool_t dbus_message_is_error(DBusMessage *message, const char *error_name)` +@_cdecl("dbus_message_is_error") +public func abi_dbus_message_is_error(_ message: OpaquePointer?, + _ errorName: UnsafePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message), box.message.type == .error + else { return false.cBool } + + return matches(box.message.errorName?.rawValue, errorName).cBool +} + +/// `dbus_bool_t dbus_message_has_path(DBusMessage *message, const char *path)` +@_cdecl("dbus_message_has_path") +public func abi_dbus_message_has_path(_ message: OpaquePointer?, + _ path: UnsafePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message) + else { return false.cBool } + + return matches(box.message.path?.rawValue, path).cBool +} + +/// `dbus_bool_t dbus_message_has_member(DBusMessage *message, const char *member)` +@_cdecl("dbus_message_has_member") +public func abi_dbus_message_has_member(_ message: OpaquePointer?, + _ member: UnsafePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message) + else { return false.cBool } + + return matches(box.message.member?.rawValue, member).cBool +} + +/// `dbus_bool_t dbus_message_has_interface(DBusMessage *message, const char *interface)` +@_cdecl("dbus_message_has_interface") +public func abi_dbus_message_has_interface(_ message: OpaquePointer?, + _ interface: UnsafePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message) + else { return false.cBool } + + return matches(box.message.interface?.rawValue, interface).cBool +} + +/// `dbus_bool_t dbus_message_has_destination(DBusMessage *message, const char *name)` +@_cdecl("dbus_message_has_destination") +public func abi_dbus_message_has_destination(_ message: OpaquePointer?, + _ name: UnsafePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message) + else { return false.cBool } + + return matches(box.message.destination?.rawValue, name).cBool +} + +/// `dbus_bool_t dbus_message_has_sender(DBusMessage *message, const char *name)` +@_cdecl("dbus_message_has_sender") +public func abi_dbus_message_has_sender(_ message: OpaquePointer?, + _ name: UnsafePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message) + else { return false.cBool } + + return matches(box.message.sender?.rawValue, name).cBool +} diff --git a/Sources/DBusABI/MessageIterator.swift b/Sources/DBusABI/MessageIterator.swift new file mode 100644 index 0000000..bbff4ab --- /dev/null +++ b/Sources/DBusABI/MessageIterator.swift @@ -0,0 +1,551 @@ +// +// MessageIterator.swift +// DBus +// +// `dbus_message_iter_*` — reading and writing message arguments. +// +// A `DBusMessageIter` is a stack value with no destructor, so its state +// cannot live in an allocation the caller is expected to release. The state +// is instead owned by the message it was created from and reached through a +// pointer parked in one of the struct's opaque fields. That matches the +// reference's rule that an iterator is valid only while its message is. +// + +import Foundation +import CDBusABI +import DBus + +// MARK: - State + +/// One element of a container being read. +/// +/// A dictionary entry is not a `DBusMessageArgument` — this package models a +/// dictionary as entries rather than as an array of two-field structures — but +/// the C ABI presents one as a `DBUS_TYPE_DICT_ENTRY` container, so reading +/// needs a representation for it. +internal enum IteratorItem { + + case argument(DBusMessageArgument) + case dictionaryEntry(DBusMessageArgument.Dictionary.Entry) +} + +/// What an append iterator is currently building. +internal enum IteratorContainer { + + case array(DBusSignature.ValueType) + case dictionary(key: DBusSignature.ValueType, value: DBusSignature.ValueType) + case dictionaryEntry + case structure + case variant +} + +internal final class IteratorState { + + /// The message the iterator reads from or writes to. + /// + /// `unowned` because the message owns the state, and an owning reference + /// back would keep both alive forever. + unowned let box: MessageBox + + let isAppend: Bool + + /// Reading: the contents of this container. Appending: what has been + /// written to it so far. + var items: [IteratorItem] = [] + + var index: Int = 0 + + /// Entries accumulated by an append iterator over a dictionary. + var entries: [DBusMessageArgument.Dictionary.Entry] = [] + + /// What this append iterator is building, or `nil` at the top level. + var container: IteratorContainer? + + var parent: IteratorState? + + init(box: MessageBox, isAppend: Bool) { + + self.box = box + self.isAppend = isAppend + } + + /// The arguments written to an append iterator, in order. + var writtenArguments: [DBusMessageArgument] { + + return items.compactMap { + guard case let .argument(argument) = $0 else { return nil } + return argument + } + } + + /// Record an argument, either into the message or into this container. + func emit(_ argument: DBusMessageArgument) { + + if container == nil, parent == nil { + box.message.arguments.append(argument) + } else { + items.append(.argument(argument)) + } + } +} + +// MARK: - Attaching state to the C struct + +/// Marks a `DBusMessageIter` as initialized by this implementation. +/// +/// The reference leaves the fields undefined until an `init` call; a caller +/// that reads an uninitialized iterator gets undefined behavior there and a +/// safe refusal here. +private let iteratorMagic: dbus_uint32_t = 0x44_42_49_54 + +private func attach(_ state: IteratorState, to iterator: UnsafeMutablePointer) { + + // Unretained: `box.iterators` is what keeps the state alive. + iterator.pointee.dummy1 = Unmanaged.passUnretained(state).toOpaque() + iterator.pointee.dummy3 = iteratorMagic + state.box.iterators.append(state) +} + +private func state(_ iterator: UnsafeMutablePointer?) -> IteratorState? { + + guard let iterator = iterator, + iterator.pointee.dummy3 == iteratorMagic, + let raw = iterator.pointee.dummy1 + else { return nil } + + return Unmanaged.fromOpaque(raw).takeUnretainedValue() +} + +// MARK: - Type codes + +internal extension DBusSignature.ValueType { + + /// The single character type code, as the C ABI reports it. + var typeCode: Int32 { + + switch self { + case .byte: return Int32(DBUS_TYPE_BYTE) + case .boolean: return Int32(DBUS_TYPE_BOOLEAN) + case .int16: return Int32(DBUS_TYPE_INT16) + case .uint16: return Int32(DBUS_TYPE_UINT16) + case .int32: return Int32(DBUS_TYPE_INT32) + case .uint32: return Int32(DBUS_TYPE_UINT32) + case .int64: return Int32(DBUS_TYPE_INT64) + case .uint64: return Int32(DBUS_TYPE_UINT64) + case .double: return Int32(DBUS_TYPE_DOUBLE) + case .fileDescriptor: return Int32(DBUS_TYPE_UNIX_FD) + case .string: return Int32(DBUS_TYPE_STRING) + case .objectPath: return Int32(DBUS_TYPE_OBJECT_PATH) + case .signature: return Int32(DBUS_TYPE_SIGNATURE) + case .variant: return Int32(DBUS_TYPE_VARIANT) + case .array: return Int32(DBUS_TYPE_ARRAY) + case .struct: return Int32(DBUS_TYPE_STRUCT) + case .dictionary: return Int32(DBUS_TYPE_ARRAY) + } + } +} + +private extension IteratorItem { + + var typeCode: Int32 { + + switch self { + case let .argument(argument): return argument.type.typeCode + case .dictionaryEntry: return Int32(DBUS_TYPE_DICT_ENTRY) + } + } +} + +/// The single value type a signature string denotes, if it denotes exactly one. +private func valueType(_ signature: String) -> DBusSignature.ValueType? { + + let parsed = DBusSignature(rawValue: signature).map { Array($0) } + + guard let types = parsed, types.count == 1 + else { return nil } + + return types[0] +} + +// MARK: - Reading + +/// `dbus_bool_t dbus_message_iter_init(DBusMessage *message, DBusMessageIter *iter)` +/// +/// Returns FALSE, and leaves an iterator that reports `DBUS_TYPE_INVALID`, when +/// the message has no arguments. +@_cdecl("dbus_message_iter_init") +public func abi_dbus_message_iter_init(_ message: OpaquePointer?, + _ iterator: UnsafeMutablePointer?) -> dbus_bool_t { + + guard let box = DBusABI.message(message), let iterator = iterator + else { return false.cBool } + + let state = IteratorState(box: box, isAppend: false) + state.items = box.message.arguments.map { .argument($0) } + attach(state, to: iterator) + + return (state.items.isEmpty == false).cBool +} + +/// `int dbus_message_iter_get_arg_type(DBusMessageIter *iter)` +@_cdecl("dbus_message_iter_get_arg_type") +public func abi_dbus_message_iter_get_arg_type(_ iterator: UnsafeMutablePointer?) -> Int32 { + + guard let state = state(iterator), state.index < state.items.count + else { return Int32(DBUS_TYPE_INVALID) } + + return state.items[state.index].typeCode +} + +/// `int dbus_message_iter_get_element_type(DBusMessageIter *iter)` +@_cdecl("dbus_message_iter_get_element_type") +public func abi_dbus_message_iter_get_element_type(_ iterator: UnsafeMutablePointer?) -> Int32 { + + guard let state = state(iterator), state.index < state.items.count, + case let .argument(argument) = state.items[state.index] + else { return Int32(DBUS_TYPE_INVALID) } + + switch argument { + case let .array(array): return array.type.typeCode + case .dictionary: return Int32(DBUS_TYPE_DICT_ENTRY) + default: return Int32(DBUS_TYPE_INVALID) + } +} + +/// `dbus_bool_t dbus_message_iter_has_next(DBusMessageIter *iter)` +@_cdecl("dbus_message_iter_has_next") +public func abi_dbus_message_iter_has_next(_ iterator: UnsafeMutablePointer?) -> dbus_bool_t { + + guard let state = state(iterator) + else { return false.cBool } + + return (state.index + 1 < state.items.count).cBool +} + +/// `dbus_bool_t dbus_message_iter_next(DBusMessageIter *iter)` +@_cdecl("dbus_message_iter_next") +public func abi_dbus_message_iter_next(_ iterator: UnsafeMutablePointer?) -> dbus_bool_t { + + guard let state = state(iterator), state.index < state.items.count + else { return false.cBool } + + state.index += 1 + return (state.index < state.items.count).cBool +} + +/// `void dbus_message_iter_recurse(DBusMessageIter *iter, DBusMessageIter *sub)` +@_cdecl("dbus_message_iter_recurse") +public func abi_dbus_message_iter_recurse(_ iterator: UnsafeMutablePointer?, + _ sub: UnsafeMutablePointer?) { + + guard let state = state(iterator), let sub = sub, state.index < state.items.count + else { return } + + let child = IteratorState(box: state.box, isAppend: false) + + switch state.items[state.index] { + + case let .dictionaryEntry(entry): + child.items = [.argument(entry.key), .argument(entry.value)] + + case let .argument(argument): + switch argument { + case let .array(array): + child.items = array.map { .argument($0) } + case let .struct(structure): + child.items = structure.map { .argument($0) } + case let .variant(variant): + child.items = [.argument(variant.element)] + case let .dictionary(dictionary): + child.items = dictionary.map { .dictionaryEntry($0) } + default: + child.items = [] + } + } + + attach(child, to: sub) +} + +/// `char *dbus_message_iter_get_signature(DBusMessageIter *iter)` +/// +/// The caller releases the result with `dbus_free`. +@_cdecl("dbus_message_iter_get_signature") +public func abi_dbus_message_iter_get_signature(_ iterator: UnsafeMutablePointer?) -> UnsafeMutablePointer? { + + guard let state = state(iterator) + else { return nil } + + var signature = "" + + for item in state.items[state.index...] { + guard case let .argument(argument) = item else { continue } + signature += DBusSignature([argument.type]).rawValue + } + + return signature.copiedCString() +} + +/// `void dbus_message_iter_get_basic(DBusMessageIter *iter, void *value)` +/// +/// Writing through an untyped pointer, so the caller must have checked +/// `dbus_message_iter_get_arg_type` first — as the reference requires. A +/// mismatch writes nothing rather than reinterpreting the value. +@_cdecl("dbus_message_iter_get_basic") +public func abi_dbus_message_iter_get_basic(_ iterator: UnsafeMutablePointer?, + _ value: UnsafeMutableRawPointer?) { + + guard let state = state(iterator), let value = value, state.index < state.items.count, + case let .argument(argument) = state.items[state.index] + else { return } + + switch argument { + + case let .byte(byte): + value.assumingMemoryBound(to: UInt8.self).pointee = byte + + case let .boolean(boolean): + value.assumingMemoryBound(to: dbus_bool_t.self).pointee = boolean.cBool + + case let .int16(number): + value.assumingMemoryBound(to: Int16.self).pointee = number + + case let .uint16(number): + value.assumingMemoryBound(to: UInt16.self).pointee = number + + case let .int32(number): + value.assumingMemoryBound(to: Int32.self).pointee = number + + case let .uint32(number): + value.assumingMemoryBound(to: UInt32.self).pointee = number + + case let .int64(number): + value.assumingMemoryBound(to: Int64.self).pointee = number + + case let .uint64(number): + value.assumingMemoryBound(to: UInt64.self).pointee = number + + case let .double(number): + value.assumingMemoryBound(to: Double.self).pointee = number + + case let .fileDescriptor(descriptor): + value.assumingMemoryBound(to: Int32.self).pointee = descriptor.rawValue + + case let .string(text): + value.assumingMemoryBound(to: UnsafePointer?.self).pointee = state.box.borrowedCopy(text) + + case let .objectPath(path): + value.assumingMemoryBound(to: UnsafePointer?.self).pointee = state.box.borrowedCopy(path.rawValue) + + case let .signature(signature): + value.assumingMemoryBound(to: UnsafePointer?.self).pointee = state.box.borrowedCopy(signature.rawValue) + + case .array, .struct, .dictionary, .variant: + break // not a basic type; the caller must recurse + } +} + +// MARK: - Appending + +/// `void dbus_message_iter_init_append(DBusMessage *message, DBusMessageIter *iter)` +@_cdecl("dbus_message_iter_init_append") +public func abi_dbus_message_iter_init_append(_ message: OpaquePointer?, + _ iterator: UnsafeMutablePointer?) { + + guard let box = DBusABI.message(message), let iterator = iterator + else { return } + + attach(IteratorState(box: box, isAppend: true), to: iterator) +} + +/// Build an argument of the given type code from a caller supplied pointer. +private func argument(type: Int32, value: UnsafeRawPointer) -> DBusMessageArgument? { + + switch Int(type) { + + case Int(DBUS_TYPE_BYTE): + return .byte(value.assumingMemoryBound(to: UInt8.self).pointee) + + case Int(DBUS_TYPE_BOOLEAN): + return .boolean(value.assumingMemoryBound(to: dbus_bool_t.self).pointee != 0) + + case Int(DBUS_TYPE_INT16): + return .int16(value.assumingMemoryBound(to: Int16.self).pointee) + + case Int(DBUS_TYPE_UINT16): + return .uint16(value.assumingMemoryBound(to: UInt16.self).pointee) + + case Int(DBUS_TYPE_INT32): + return .int32(value.assumingMemoryBound(to: Int32.self).pointee) + + case Int(DBUS_TYPE_UINT32): + return .uint32(value.assumingMemoryBound(to: UInt32.self).pointee) + + case Int(DBUS_TYPE_INT64): + return .int64(value.assumingMemoryBound(to: Int64.self).pointee) + + case Int(DBUS_TYPE_UINT64): + return .uint64(value.assumingMemoryBound(to: UInt64.self).pointee) + + case Int(DBUS_TYPE_DOUBLE): + return .double(value.assumingMemoryBound(to: Double.self).pointee) + + case Int(DBUS_TYPE_UNIX_FD): + return .fileDescriptor(.init(rawValue: value.assumingMemoryBound(to: Int32.self).pointee)) + + case Int(DBUS_TYPE_STRING): + guard let text = string(value.assumingMemoryBound(to: UnsafePointer?.self).pointee) + else { return nil } + return .string(text) + + case Int(DBUS_TYPE_OBJECT_PATH): + guard let text = string(value.assumingMemoryBound(to: UnsafePointer?.self).pointee), + let path = DBusObjectPath(rawValue: text) + else { return nil } + return .objectPath(path) + + case Int(DBUS_TYPE_SIGNATURE): + guard let text = string(value.assumingMemoryBound(to: UnsafePointer?.self).pointee), + let signature = DBusSignature(rawValue: text) + else { return nil } + return .signature(signature) + + default: + return nil + } +} + +/// `dbus_bool_t dbus_message_iter_append_basic(DBusMessageIter *iter, int type, const void *value)` +@_cdecl("dbus_message_iter_append_basic") +public func abi_dbus_message_iter_append_basic(_ iterator: UnsafeMutablePointer?, + _ type: Int32, + _ value: UnsafeRawPointer?) -> dbus_bool_t { + + guard let state = state(iterator), state.isAppend, let value = value, + let argument = argument(type: type, value: value) + else { return false.cBool } + + state.emit(argument) + return true.cBool +} + +/// `dbus_bool_t dbus_message_iter_open_container(DBusMessageIter *, int, const char *, DBusMessageIter *)` +@_cdecl("dbus_message_iter_open_container") +public func abi_dbus_message_iter_open_container(_ iterator: UnsafeMutablePointer?, + _ type: Int32, + _ containedSignature: UnsafePointer?, + _ sub: UnsafeMutablePointer?) -> dbus_bool_t { + + guard let state = state(iterator), state.isAppend, let sub = sub + else { return false.cBool } + + let signature = string(containedSignature) + let container: IteratorContainer + + switch Int(type) { + + case Int(DBUS_TYPE_ARRAY): + guard let signature = signature + else { return false.cBool } + + // A dictionary is opened as an array whose element signature is a + // dictionary entry, which is not itself a complete type. + if signature.hasPrefix("{"), signature.hasSuffix("}") { + + let inner = String(signature.dropFirst().dropLast()) + + guard let parsed = DBusSignature(rawValue: inner).map({ Array($0) }), + parsed.count == 2 + else { return false.cBool } + + container = .dictionary(key: parsed[0], value: parsed[1]) + } + else { + guard let element = valueType(signature) + else { return false.cBool } + + container = .array(element) + } + + case Int(DBUS_TYPE_STRUCT): + container = .structure + + case Int(DBUS_TYPE_DICT_ENTRY): + container = .dictionaryEntry + + case Int(DBUS_TYPE_VARIANT): + container = .variant + + default: + return false.cBool + } + + let child = IteratorState(box: state.box, isAppend: true) + child.container = container + child.parent = state + attach(child, to: sub) + + return true.cBool +} + +/// `dbus_bool_t dbus_message_iter_close_container(DBusMessageIter *iter, DBusMessageIter *sub)` +@_cdecl("dbus_message_iter_close_container") +public func abi_dbus_message_iter_close_container(_ iterator: UnsafeMutablePointer?, + _ sub: UnsafeMutablePointer?) -> dbus_bool_t { + + guard let parent = state(iterator), + let child = state(sub), + let container = child.container, + child.parent === parent + else { return false.cBool } + + let written = child.writtenArguments + + switch container { + + case let .array(element): + guard let array = DBusMessageArgument.Array(type: element, written) + else { return false.cBool } + parent.emit(.array(array)) + + case let .dictionary(key, value): + guard let dictionary = DBusMessageArgument.Dictionary(keyType: key, + valueType: value, + child.entries) + else { return false.cBool } + parent.emit(.dictionary(dictionary)) + + case .dictionaryEntry: + guard written.count == 2 + else { return false.cBool } + parent.entries.append(.init(key: written[0], value: written[1])) + + case .structure: + guard let structure = DBusMessageArgument.Structure(written) + else { return false.cBool } + parent.emit(.struct(structure)) + + case .variant: + guard written.count == 1 + else { return false.cBool } + parent.emit(.variant(.init(written[0]))) + } + + return true.cBool +} + +/// `void dbus_message_iter_abandon_container(DBusMessageIter *iter, DBusMessageIter *sub)` +/// +/// Discards whatever was written to the sub-iterator, leaving the parent as it +/// was before the container was opened. +@_cdecl("dbus_message_iter_abandon_container") +public func abi_dbus_message_iter_abandon_container(_ iterator: UnsafeMutablePointer?, + _ sub: UnsafeMutablePointer?) { + + guard let child = state(sub) + else { return } + + child.items.removeAll() + child.entries.removeAll() + child.container = nil + child.parent = nil +} diff --git a/Sources/DBusABI/Runtime.swift b/Sources/DBusABI/Runtime.swift new file mode 100644 index 0000000..fe2d540 --- /dev/null +++ b/Sources/DBusABI/Runtime.swift @@ -0,0 +1,218 @@ +// +// Runtime.swift +// DBus +// +// The machinery every C entry point relies on: object boxing, the bridge +// from this package's `async` API to the blocking calls the C ABI promises, +// and error translation. +// +// - Note: `DBusError`, `DBusMessage` and `DBusConnection` each name both a C +// type here and a Swift type in the `DBus` module, so every reference to one +// of them in this target is written module qualified. The C opaque structs +// (`DBusConnection`, `DBusMessage`) arrive in Swift as `OpaquePointer`. +// + +import Foundation +import CDBusABI +import DBus + +// MARK: - Boxing + +/// A reference counted Swift object addressed from C as an opaque pointer. +/// +/// The C ABI is explicitly reference counted — `dbus_message_ref` and friends — +/// so the counts are kept by `Unmanaged` rather than by ARC: a pointer handed +/// to C carries a retain that only the matching `unref` releases. +internal class Box { + + /// Take an unbalanced retain and return the pointer to hand to C. + func retainedPointer() -> OpaquePointer { + + return OpaquePointer(Unmanaged.passRetained(self).toOpaque()) + } + + /// Add one to the count, for `dbus_*_ref`. + static func retain(_ pointer: OpaquePointer) { + + _ = Unmanaged.fromOpaque(UnsafeRawPointer(pointer)).retain() + } + + /// Subtract one from the count, for `dbus_*_unref`. + static func release(_ pointer: OpaquePointer) { + + Unmanaged.fromOpaque(UnsafeRawPointer(pointer)).release() + } + + /// The object a C pointer refers to, without changing the count. + static func unretained(_ pointer: OpaquePointer) -> T? { + + return Unmanaged.fromOpaque(UnsafeRawPointer(pointer)).takeUnretainedValue() as? T + } +} + +// MARK: - Blocking + +/// Run an asynchronous operation to completion, blocking the calling thread. +/// +/// The C ABI has no way to express `async`, and the entry points implemented +/// here are the blocking ones by definition — `..._and_block` is in the name. +/// A C caller arrives on its own thread, never on a Swift concurrency +/// cooperative thread, so parking that thread on a semaphore does not starve +/// the executor the detached task runs on. +internal func blocking(_ body: @escaping @Sendable () async throws -> T) throws -> T { + + let semaphore = DispatchSemaphore(value: 0) + let result = ResultBox() + + Task.detached { + + do { result.value = .success(try await body()) } + catch { result.value = .failure(error) } + + semaphore.signal() + } + + semaphore.wait() + + switch result.value { + case let .success(value): return value + case let .failure(error): throw error + case nil: throw DBusABIError.internalFailure + } +} + +/// Carries a result across the semaphore. +/// +/// Written once by the task before `signal()` and read once after `wait()`, +/// which orders the two accesses, so no further synchronization is needed. +private final class ResultBox: @unchecked Sendable { + + var value: Result? +} + +internal enum DBusABIError: Error, CustomStringConvertible { + + case internalFailure + + var description: String { "The operation could not be completed" } +} + +// MARK: - Strings + +/// The Swift string a C argument refers to, or `nil` for a null pointer. +internal func string(_ pointer: UnsafePointer?) -> String? { + + guard let pointer = pointer + else { return nil } + + return String(cString: pointer) +} + +internal extension String { + + /// A copy owned by the caller, released with `dbus_free`. + func copiedCString() -> UnsafeMutablePointer? { + + return strdup(self) + } +} + +// MARK: - Errors + +/// Marks a `DBusError` whose strings were allocated by this library. +/// +/// The reference uses one of the `dummy` bitfields for this. A bitfield is +/// awkward to address from Swift and its import is an implementation detail, +/// so the spare `padding1` pointer carries the flag instead: it is private to +/// the implementation in the reference too, and callers only ever read `name` +/// and `message`. +private nonisolated(unsafe) let ownedStringsMarker = UnsafeMutableRawPointer(bitPattern: 0x1) + +internal extension DBus.DBusError.Name { + + /// The nearest error name for a failure that did not come from the bus. + static func from(_ error: Error) -> DBus.DBusError.Name { + + if let busError = error as? DBus.DBusError { + return busError.name + } + + guard let protocolError = error as? DBusProtocolError + else { return .failed } + + switch protocolError { + case .invalidAddress: + return .badAddress + case .endOfStream: + return .disconnected + case .authenticationFailed, .authenticationRejected: + return .accessDenied + case .messageTooLarge: + return .limitsExceeded + case .invalidSignature: + return .invalidSignature + default: + return .failed + } + } +} + +/// Fill in a caller supplied `DBusError` from a thrown Swift error. +internal func setError(_ error: UnsafeMutablePointer?, from thrown: Error) { + + guard let error = error + else { return } + + let message: String + + if let busError = thrown as? DBus.DBusError { + message = busError.message + } else if let described = thrown as? CustomStringConvertible { + message = described.description + } else { + message = "\(thrown)" + } + + setError(error, name: DBus.DBusError.Name.from(thrown).rawValue, message: message) +} + +/// Fill in a caller supplied `DBusError` from two strings. +internal func setError(_ error: UnsafeMutablePointer?, + name: String, + message: String) { + + guard let error = error + else { return } + + // Replacing an error that is already set would leak the strings it holds. + freeError(error) + + error.pointee.name = UnsafePointer(strdup(name)) + error.pointee.message = UnsafePointer(strdup(message)) + error.pointee.padding1 = ownedStringsMarker +} + +/// Release the strings a `DBusError` owns and return it to the unset state. +internal func freeError(_ error: UnsafeMutablePointer?) { + + guard let error = error + else { return } + + if error.pointee.padding1 == ownedStringsMarker { + + free(UnsafeMutableRawPointer(mutating: error.pointee.name)) + free(UnsafeMutableRawPointer(mutating: error.pointee.message)) + } + + error.pointee.name = nil + error.pointee.message = nil + error.pointee.padding1 = nil +} + +// MARK: - Booleans + +internal extension Bool { + + /// `dbus_bool_t` is a 32 bit integer, not a C `_Bool`. + var cBool: dbus_bool_t { self ? 1 : 0 } +} diff --git a/Tests/CABI/smoke.c b/Tests/CABI/smoke.c new file mode 100644 index 0000000..676490b --- /dev/null +++ b/Tests/CABI/smoke.c @@ -0,0 +1,283 @@ +/* + * smoke.c + * DBus + * + * A C program linked against the built `libdbus-swift.so`, exercising the + * ABI the way a consumer would. + * + * This is not a duplicate of the SwiftPM suite. It covers two things that + * suite structurally cannot: + * + * - the variadic entry points (`dbus_message_append_args` and + * `dbus_message_get_args`), because Swift cannot call a C variadic + * function at all; + * - that the shared object actually links and loads, with every symbol a + * caller needs resolvable from outside. + * + * Nothing here touches a bus, so it runs anywhere. + */ + +#include + +#include +#include + +static int failures = 0; + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, \ + #condition); \ + failures++; \ + } \ + } while (0) + +static void test_error(void) +{ + DBusError error; + dbus_error_init(&error); + + CHECK(!dbus_error_is_set(&error)); + + dbus_set_error_const(&error, DBUS_ERROR_FAILED, "constant"); + CHECK(dbus_error_is_set(&error)); + CHECK(dbus_error_has_name(&error, DBUS_ERROR_FAILED)); + CHECK(strcmp(error.message, "constant") == 0); + + dbus_error_free(&error); + CHECK(!dbus_error_is_set(&error)); + + /* The variadic formatter. */ + dbus_set_error(&error, DBUS_ERROR_INVALID_ARGS, "expected %d, got %s", 42, "seven"); + CHECK(dbus_error_is_set(&error)); + CHECK(strcmp(error.message, "expected 42, got seven") == 0); + + /* Moving hands ownership over and clears the source. */ + DBusError destination; + dbus_error_init(&destination); + dbus_move_error(&error, &destination); + + CHECK(!dbus_error_is_set(&error)); + CHECK(dbus_error_has_name(&destination, DBUS_ERROR_INVALID_ARGS)); + + dbus_error_free(&destination); +} + +static void test_message_headers(void) +{ + DBusMessage *message = dbus_message_new_method_call("org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "ListNames"); + CHECK(message != NULL); + if (message == NULL) { + return; + } + + CHECK(dbus_message_get_type(message) == DBUS_MESSAGE_TYPE_METHOD_CALL); + CHECK(dbus_message_is_method_call(message, "org.freedesktop.DBus", "ListNames")); + CHECK(dbus_message_has_path(message, "/org/freedesktop/DBus")); + CHECK(strcmp(dbus_message_get_member(message), "ListNames") == 0); + + dbus_message_unref(message); + + /* A malformed field yields NULL rather than a broken message. */ + CHECK(dbus_message_new_method_call(NULL, "bad path", NULL, "Method") == NULL); +} + +static void test_append_and_get_args(void) +{ + DBusMessage *message = dbus_message_new(DBUS_MESSAGE_TYPE_METHOD_CALL); + CHECK(message != NULL); + if (message == NULL) { + return; + } + + dbus_int32_t number = 1234; + const char *text = "variadic"; + double fraction = 0.5; + + CHECK(dbus_message_append_args(message, + DBUS_TYPE_INT32, &number, + DBUS_TYPE_STRING, &text, + DBUS_TYPE_DOUBLE, &fraction, + DBUS_TYPE_INVALID)); + + CHECK(strcmp(dbus_message_get_signature(message), "isd") == 0); + + DBusError error; + dbus_error_init(&error); + + dbus_int32_t readNumber = 0; + const char *readText = NULL; + double readFraction = 0; + + CHECK(dbus_message_get_args(message, &error, + DBUS_TYPE_INT32, &readNumber, + DBUS_TYPE_STRING, &readText, + DBUS_TYPE_DOUBLE, &readFraction, + DBUS_TYPE_INVALID)); + + CHECK(!dbus_error_is_set(&error)); + CHECK(readNumber == 1234); + CHECK(readText != NULL && strcmp(readText, "variadic") == 0); + CHECK(readFraction == 0.5); + + dbus_error_free(&error); + + /* Asking for the wrong type must fail and say so. */ + const char *wrong = NULL; + CHECK(!dbus_message_get_args(message, &error, + DBUS_TYPE_STRING, &wrong, + DBUS_TYPE_INVALID)); + CHECK(dbus_error_is_set(&error)); + dbus_error_free(&error); + + dbus_message_unref(message); +} + +static void test_string_array_args(void) +{ + DBusMessage *message = dbus_message_new(DBUS_MESSAGE_TYPE_METHOD_RETURN); + CHECK(message != NULL); + if (message == NULL) { + return; + } + + const char *values[] = { "alpha", "beta", "gamma" }; + const char **pointer = values; + + CHECK(dbus_message_append_args(message, + DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, &pointer, 3, + DBUS_TYPE_INVALID)); + + CHECK(strcmp(dbus_message_get_signature(message), "as") == 0); + + DBusError error; + dbus_error_init(&error); + + char **read = NULL; + int count = 0; + + CHECK(dbus_message_get_args(message, &error, + DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, &read, &count, + DBUS_TYPE_INVALID)); + + CHECK(!dbus_error_is_set(&error)); + CHECK(count == 3); + + if (read != NULL && count == 3) { + CHECK(strcmp(read[0], "alpha") == 0); + CHECK(strcmp(read[1], "beta") == 0); + CHECK(strcmp(read[2], "gamma") == 0); + CHECK(read[3] == NULL); + } + + dbus_free_string_array(read); + dbus_error_free(&error); + dbus_message_unref(message); +} + +static void test_iterators(void) +{ + DBusMessage *message = dbus_message_new(DBUS_MESSAGE_TYPE_SIGNAL); + CHECK(message != NULL); + if (message == NULL) { + return; + } + + /* Build `a{sv}` by hand, the shape org.freedesktop.DBus.Properties uses. */ + DBusMessageIter iter; + dbus_message_iter_init_append(message, &iter); + + DBusMessageIter dictionary; + CHECK(dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &dictionary)); + + DBusMessageIter entry; + CHECK(dbus_message_iter_open_container(&dictionary, DBUS_TYPE_DICT_ENTRY, NULL, &entry)); + + const char *key = "Volume"; + CHECK(dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key)); + + DBusMessageIter variant; + CHECK(dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "u", &variant)); + dbus_uint32_t volume = 11; + CHECK(dbus_message_iter_append_basic(&variant, DBUS_TYPE_UINT32, &volume)); + CHECK(dbus_message_iter_close_container(&entry, &variant)); + + CHECK(dbus_message_iter_close_container(&dictionary, &entry)); + CHECK(dbus_message_iter_close_container(&iter, &dictionary)); + + CHECK(strcmp(dbus_message_get_signature(message), "a{sv}") == 0); + + /* Read it back. */ + DBusMessageIter reader; + CHECK(dbus_message_iter_init(message, &reader)); + CHECK(dbus_message_iter_get_arg_type(&reader) == DBUS_TYPE_ARRAY); + CHECK(dbus_message_iter_get_element_type(&reader) == DBUS_TYPE_DICT_ENTRY); + + DBusMessageIter entries; + dbus_message_iter_recurse(&reader, &entries); + CHECK(dbus_message_iter_get_arg_type(&entries) == DBUS_TYPE_DICT_ENTRY); + + DBusMessageIter pair; + dbus_message_iter_recurse(&entries, &pair); + + const char *readKey = NULL; + dbus_message_iter_get_basic(&pair, &readKey); + CHECK(readKey != NULL && strcmp(readKey, "Volume") == 0); + + CHECK(dbus_message_iter_next(&pair)); + CHECK(dbus_message_iter_get_arg_type(&pair) == DBUS_TYPE_VARIANT); + + DBusMessageIter contained; + dbus_message_iter_recurse(&pair, &contained); + CHECK(dbus_message_iter_get_arg_type(&contained) == DBUS_TYPE_UINT32); + + dbus_uint32_t readVolume = 0; + dbus_message_iter_get_basic(&contained, &readVolume); + CHECK(readVolume == 11); + + dbus_message_unref(message); +} + +static void test_memory(void) +{ + CHECK(dbus_threads_init_default()); + + void *block = dbus_malloc(64); + CHECK(block != NULL); + dbus_free(block); + + unsigned char *zeroed = dbus_malloc0(16); + CHECK(zeroed != NULL); + if (zeroed != NULL) { + for (int index = 0; index < 16; index++) { + CHECK(zeroed[index] == 0); + } + } + dbus_free(zeroed); + + /* A zero byte request yields NULL, and freeing NULL is valid. */ + CHECK(dbus_malloc(0) == NULL); + dbus_free(NULL); +} + +int main(void) +{ + test_error(); + test_message_headers(); + test_append_and_get_args(); + test_string_array_args(); + test_iterators(); + test_memory(); + + if (failures == 0) { + printf("C ABI smoke test: all checks passed\n"); + return 0; + } + + fprintf(stderr, "C ABI smoke test: %d check(s) failed\n", failures); + return 1; +} diff --git a/Tests/DBusABITests/MessageABITests.swift b/Tests/DBusABITests/MessageABITests.swift new file mode 100644 index 0000000..549005e --- /dev/null +++ b/Tests/DBusABITests/MessageABITests.swift @@ -0,0 +1,404 @@ +// +// MessageABITests.swift +// DBusABITests +// +// Exercises the C entry points the way a C caller would: through the +// declarations in `dbus.h`, with stack allocated `DBusError` and +// `DBusMessageIter` values, and with every returned object released by its +// matching `unref` or `dbus_free`. +// + +import Testing +import Foundation +import CDBusABI +@testable import DBusABI + +@Suite struct MessageABITests { + + // MARK: Construction + + @Test func createsAndReleasesAMethodCall() throws { + + let message = try #require(dbus_message_new_method_call("org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "ListNames")) + defer { dbus_message_unref(message) } + + #expect(dbus_message_get_type(message) == Int32(DBUS_MESSAGE_TYPE_METHOD_CALL)) + #expect(String(cString: dbus_message_get_destination(message)!) == "org.freedesktop.DBus") + #expect(String(cString: dbus_message_get_path(message)!) == "/org/freedesktop/DBus") + #expect(String(cString: dbus_message_get_interface(message)!) == "org.freedesktop.DBus") + #expect(String(cString: dbus_message_get_member(message)!) == "ListNames") + } + + /// A malformed path, interface or member yields NULL, as in the reference. + @Test(arguments: [ + ("org.freedesktop.DBus", "not-a-path", "org.example.Thing", "Method"), + ("org.freedesktop.DBus", "/valid/path", "0bad.interface", "Method"), + ("org.freedesktop.DBus", "/valid/path", "org.example.Thing", "not.a.member"), + ("not a bus name", "/valid/path", "org.example.Thing", "Method") + ]) + func rejectsMalformedFields(destination: String, + path: String, + interface: String, + member: String) { + + let message = dbus_message_new_method_call(destination, path, interface, member) + + #expect(message == nil) + + if let message = message { dbus_message_unref(message) } + } + + @Test func createsASignal() throws { + + let message = try #require(dbus_message_new_signal("/com/example/Object", + "com.example.Interface", + "Changed")) + defer { dbus_message_unref(message) } + + #expect(dbus_message_get_type(message) == Int32(DBUS_MESSAGE_TYPE_SIGNAL)) + #expect(dbus_message_is_signal(message, "com.example.Interface", "Changed") != 0) + #expect(dbus_message_is_signal(message, "com.example.Interface", "Other") == 0) + } + + /// A reply must carry the serial of the call it answers. + @Test func createsAMethodReturn() throws { + + let call = try #require(dbus_message_new_method_call(nil, + "/com/example/Object", + "com.example.Interface", + "Method")) + defer { dbus_message_unref(call) } + + message(call)?.message.serial = 42 + + let reply = try #require(dbus_message_new_method_return(call)) + defer { dbus_message_unref(reply) } + + #expect(dbus_message_get_type(reply) == Int32(DBUS_MESSAGE_TYPE_METHOD_RETURN)) + #expect(dbus_message_get_reply_serial(reply) == 42) + } + + @Test func createsAnError() throws { + + let call = try #require(dbus_message_new_method_call(nil, + "/com/example/Object", + "com.example.Interface", + "Method")) + defer { dbus_message_unref(call) } + + let reply = try #require(dbus_message_new_error(call, + DBUS_ERROR_FAILED, + "It did not work")) + defer { dbus_message_unref(reply) } + + #expect(dbus_message_is_error(reply, DBUS_ERROR_FAILED) != 0) + #expect(String(cString: dbus_message_get_error_name(reply)!) == DBUS_ERROR_FAILED) + } + + // MARK: Reference counting + + /// `ref` and `unref` must balance: the object survives the first release + /// and dies on the second. + @Test func referenceCountingKeepsTheMessageAlive() throws { + + let message = try #require(dbus_message_new(Int32(DBUS_MESSAGE_TYPE_METHOD_CALL))) + + #expect(dbus_message_ref(message) == message) + + dbus_message_unref(message) + + // Still valid: the extra reference is still held. + #expect(dbus_message_get_type(message) == Int32(DBUS_MESSAGE_TYPE_METHOD_CALL)) + + dbus_message_unref(message) + } + + // MARK: Header accessors + + /// The reference returns a borrowed pointer that stays valid, so a second + /// read of an unchanged field must not hand back freed memory. + @Test func headerStringsRemainValid() throws { + + let message = try #require(dbus_message_new(Int32(DBUS_MESSAGE_TYPE_SIGNAL))) + defer { dbus_message_unref(message) } + + #expect(dbus_message_set_path(message, "/com/example/One") != 0) + + let first = try #require(dbus_message_get_path(message)) + let second = try #require(dbus_message_get_path(message)) + + #expect(first == second, "An unchanged field should not be reallocated") + #expect(String(cString: second) == "/com/example/One") + + // Changing it must be reflected, and the old copy released. + #expect(dbus_message_set_path(message, "/com/example/Two") != 0) + #expect(String(cString: dbus_message_get_path(message)!) == "/com/example/Two") + + // An invalid value is refused and leaves the field alone. + #expect(dbus_message_set_path(message, "not a path") == 0) + #expect(String(cString: dbus_message_get_path(message)!) == "/com/example/Two") + } + + @Test func noReplyFlagRoundTrips() throws { + + let message = try #require(dbus_message_new(Int32(DBUS_MESSAGE_TYPE_METHOD_CALL))) + defer { dbus_message_unref(message) } + + #expect(dbus_message_get_no_reply(message) == 0) + + dbus_message_set_no_reply(message, 1) + #expect(dbus_message_get_no_reply(message) != 0) + + dbus_message_set_no_reply(message, 0) + #expect(dbus_message_get_no_reply(message) == 0) + } + + // MARK: Arguments + + @Test func appendsAndReadsBasicTypes() throws { + + let message = try #require(dbus_message_new(Int32(DBUS_MESSAGE_TYPE_METHOD_CALL))) + defer { dbus_message_unref(message) } + + var iterator = DBusMessageIter() + dbus_message_iter_init_append(message, &iterator) + + var byte: UInt8 = 7 + var boolean: dbus_bool_t = 1 + var int32: Int32 = -12345 + var uint64: UInt64 = 0xDEAD_BEEF + var double: Double = 2.5 + let text = strdup("hello") + defer { free(text) } + var textPointer = UnsafePointer(text) + + #expect(dbus_message_iter_append_basic(&iterator, Int32(DBUS_TYPE_BYTE), &byte) != 0) + #expect(dbus_message_iter_append_basic(&iterator, Int32(DBUS_TYPE_BOOLEAN), &boolean) != 0) + #expect(dbus_message_iter_append_basic(&iterator, Int32(DBUS_TYPE_INT32), &int32) != 0) + #expect(dbus_message_iter_append_basic(&iterator, Int32(DBUS_TYPE_UINT64), &uint64) != 0) + #expect(dbus_message_iter_append_basic(&iterator, Int32(DBUS_TYPE_DOUBLE), &double) != 0) + #expect(dbus_message_iter_append_basic(&iterator, Int32(DBUS_TYPE_STRING), &textPointer) != 0) + + #expect(String(cString: dbus_message_get_signature(message)!) == "ybitds") + + var reader = DBusMessageIter() + #expect(dbus_message_iter_init(message, &reader) != 0) + + #expect(dbus_message_iter_get_arg_type(&reader) == Int32(DBUS_TYPE_BYTE)) + var readByte: UInt8 = 0 + dbus_message_iter_get_basic(&reader, &readByte) + #expect(readByte == 7) + + #expect(dbus_message_iter_next(&reader) != 0) + var readBoolean: dbus_bool_t = 0 + dbus_message_iter_get_basic(&reader, &readBoolean) + #expect(readBoolean != 0) + + #expect(dbus_message_iter_next(&reader) != 0) + var readInt32: Int32 = 0 + dbus_message_iter_get_basic(&reader, &readInt32) + #expect(readInt32 == -12345) + + #expect(dbus_message_iter_next(&reader) != 0) + var readUInt64: UInt64 = 0 + dbus_message_iter_get_basic(&reader, &readUInt64) + #expect(readUInt64 == 0xDEAD_BEEF) + + #expect(dbus_message_iter_next(&reader) != 0) + var readDouble: Double = 0 + dbus_message_iter_get_basic(&reader, &readDouble) + #expect(readDouble == 2.5) + + #expect(dbus_message_iter_next(&reader) != 0) + var readText: UnsafePointer? + dbus_message_iter_get_basic(&reader, &readText) + #expect(String(cString: try #require(readText)) == "hello") + + // Past the last argument. + #expect(dbus_message_iter_next(&reader) == 0) + #expect(dbus_message_iter_get_arg_type(&reader) == Int32(DBUS_TYPE_INVALID)) + } + + /// An empty array must keep its declared element type, which is the case a + /// marshaller that infers the type from the elements gets wrong. + @Test func appendsAnEmptyArray() throws { + + let message = try #require(dbus_message_new(Int32(DBUS_MESSAGE_TYPE_METHOD_CALL))) + defer { dbus_message_unref(message) } + + var iterator = DBusMessageIter() + dbus_message_iter_init_append(message, &iterator) + + var sub = DBusMessageIter() + #expect(dbus_message_iter_open_container(&iterator, Int32(DBUS_TYPE_ARRAY), "s", &sub) != 0) + #expect(dbus_message_iter_close_container(&iterator, &sub) != 0) + + #expect(String(cString: dbus_message_get_signature(message)!) == "as") + } + + @Test func appendsAndReadsAnArrayOfStrings() throws { + + let message = try #require(dbus_message_new(Int32(DBUS_MESSAGE_TYPE_METHOD_CALL))) + defer { dbus_message_unref(message) } + + var iterator = DBusMessageIter() + dbus_message_iter_init_append(message, &iterator) + + var sub = DBusMessageIter() + #expect(dbus_message_iter_open_container(&iterator, Int32(DBUS_TYPE_ARRAY), "s", &sub) != 0) + + for value in ["one", "two", "three"] { + let copy = strdup(value) + defer { free(copy) } + var pointer = UnsafePointer(copy) + #expect(dbus_message_iter_append_basic(&sub, Int32(DBUS_TYPE_STRING), &pointer) != 0) + } + + #expect(dbus_message_iter_close_container(&iterator, &sub) != 0) + #expect(String(cString: dbus_message_get_signature(message)!) == "as") + + var reader = DBusMessageIter() + #expect(dbus_message_iter_init(message, &reader) != 0) + #expect(dbus_message_iter_get_arg_type(&reader) == Int32(DBUS_TYPE_ARRAY)) + #expect(dbus_message_iter_get_element_type(&reader) == Int32(DBUS_TYPE_STRING)) + + var element = DBusMessageIter() + dbus_message_iter_recurse(&reader, &element) + + var values = [String]() + while dbus_message_iter_get_arg_type(&element) != Int32(DBUS_TYPE_INVALID) { + var pointer: UnsafePointer? + dbus_message_iter_get_basic(&element, &pointer) + values.append(String(cString: try #require(pointer))) + _ = dbus_message_iter_next(&element) + } + + #expect(values == ["one", "two", "three"]) + } + + /// `a{sv}` is the shape every service uses through `org.freedesktop.DBus.Properties`. + @Test func appendsAndReadsADictionaryOfVariants() throws { + + let message = try #require(dbus_message_new(Int32(DBUS_MESSAGE_TYPE_METHOD_CALL))) + defer { dbus_message_unref(message) } + + var iterator = DBusMessageIter() + dbus_message_iter_init_append(message, &iterator) + + var dictionary = DBusMessageIter() + #expect(dbus_message_iter_open_container(&iterator, Int32(DBUS_TYPE_ARRAY), "{sv}", &dictionary) != 0) + + var entry = DBusMessageIter() + #expect(dbus_message_iter_open_container(&dictionary, Int32(DBUS_TYPE_DICT_ENTRY), nil, &entry) != 0) + + let key = strdup("Count") + defer { free(key) } + var keyPointer = UnsafePointer(key) + #expect(dbus_message_iter_append_basic(&entry, Int32(DBUS_TYPE_STRING), &keyPointer) != 0) + + var variant = DBusMessageIter() + #expect(dbus_message_iter_open_container(&entry, Int32(DBUS_TYPE_VARIANT), "u", &variant) != 0) + var count: UInt32 = 99 + #expect(dbus_message_iter_append_basic(&variant, Int32(DBUS_TYPE_UINT32), &count) != 0) + #expect(dbus_message_iter_close_container(&entry, &variant) != 0) + + #expect(dbus_message_iter_close_container(&dictionary, &entry) != 0) + #expect(dbus_message_iter_close_container(&iterator, &dictionary) != 0) + + #expect(String(cString: dbus_message_get_signature(message)!) == "a{sv}") + + // Read it back the way a C caller would. + var reader = DBusMessageIter() + #expect(dbus_message_iter_init(message, &reader) != 0) + #expect(dbus_message_iter_get_arg_type(&reader) == Int32(DBUS_TYPE_ARRAY)) + #expect(dbus_message_iter_get_element_type(&reader) == Int32(DBUS_TYPE_DICT_ENTRY)) + + var entries = DBusMessageIter() + dbus_message_iter_recurse(&reader, &entries) + #expect(dbus_message_iter_get_arg_type(&entries) == Int32(DBUS_TYPE_DICT_ENTRY)) + + var pair = DBusMessageIter() + dbus_message_iter_recurse(&entries, &pair) + + var readKey: UnsafePointer? + dbus_message_iter_get_basic(&pair, &readKey) + #expect(String(cString: try #require(readKey)) == "Count") + + #expect(dbus_message_iter_next(&pair) != 0) + #expect(dbus_message_iter_get_arg_type(&pair) == Int32(DBUS_TYPE_VARIANT)) + + var contained = DBusMessageIter() + dbus_message_iter_recurse(&pair, &contained) + #expect(dbus_message_iter_get_arg_type(&contained) == Int32(DBUS_TYPE_UINT32)) + + var readCount: UInt32 = 0 + dbus_message_iter_get_basic(&contained, &readCount) + #expect(readCount == 99) + } + + @Test func appendsAStructure() throws { + + let message = try #require(dbus_message_new(Int32(DBUS_MESSAGE_TYPE_METHOD_CALL))) + defer { dbus_message_unref(message) } + + var iterator = DBusMessageIter() + dbus_message_iter_init_append(message, &iterator) + + var structure = DBusMessageIter() + #expect(dbus_message_iter_open_container(&iterator, Int32(DBUS_TYPE_STRUCT), nil, &structure) != 0) + + var number: Int32 = 5 + #expect(dbus_message_iter_append_basic(&structure, Int32(DBUS_TYPE_INT32), &number) != 0) + + let text = strdup("inner") + defer { free(text) } + var pointer = UnsafePointer(text) + #expect(dbus_message_iter_append_basic(&structure, Int32(DBUS_TYPE_STRING), &pointer) != 0) + + #expect(dbus_message_iter_close_container(&iterator, &structure) != 0) + + #expect(String(cString: dbus_message_get_signature(message)!) == "(is)") + } + + /// Abandoning a container must leave the message as it was. + @Test func abandonsAContainer() throws { + + let message = try #require(dbus_message_new(Int32(DBUS_MESSAGE_TYPE_METHOD_CALL))) + defer { dbus_message_unref(message) } + + var iterator = DBusMessageIter() + dbus_message_iter_init_append(message, &iterator) + + var sub = DBusMessageIter() + #expect(dbus_message_iter_open_container(&iterator, Int32(DBUS_TYPE_ARRAY), "s", &sub) != 0) + + let text = strdup("discarded") + defer { free(text) } + var pointer = UnsafePointer(text) + #expect(dbus_message_iter_append_basic(&sub, Int32(DBUS_TYPE_STRING), &pointer) != 0) + + dbus_message_iter_abandon_container(&iterator, &sub) + + #expect(String(cString: dbus_message_get_signature(message)!) == "") + } + + /// An uninitialized iterator must be refused rather than read as garbage. + @Test func rejectsAnUninitializedIterator() { + + var iterator = DBusMessageIter() + + #expect(dbus_message_iter_get_arg_type(&iterator) == Int32(DBUS_TYPE_INVALID)) + #expect(dbus_message_iter_next(&iterator) == 0) + #expect(dbus_message_iter_has_next(&iterator) == 0) + } + + // MARK: Variadic helpers + // + // `dbus_message_append_args` and `dbus_message_get_args` are C variadics, + // which Swift cannot call at all. They are covered instead by the C smoke + // test that CMake builds and runs against the installed shared library — + // a real C caller, which is the only thing that can exercise them. +} diff --git a/cmake/dbus-swift.pc.in b/cmake/dbus-swift.pc.in new file mode 100644 index 0000000..fea4ed5 --- /dev/null +++ b/cmake/dbus-swift.pc.in @@ -0,0 +1,10 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@ +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ + +Name: dbus-swift +Description: @PROJECT_DESCRIPTION@ +Version: @DBUS_SWIFT_VERSION@ +Libs: -L${libdir} -ldbus-swift +Cflags: -I${includedir}/dbus-swift diff --git a/cmake/empty.swift b/cmake/empty.swift new file mode 100644 index 0000000..716925a --- /dev/null +++ b/cmake/empty.swift @@ -0,0 +1,20 @@ +// +// empty.swift +// DBus +// +// Placeholder translation unit. +// +// CMake requires a shared library target to have at least one source of its +// own; every exported symbol actually arrives from the Swift and C static +// archives, linked whole (see CMakeLists.txt). +// +// It is Swift rather than C, and it imports, because swiftc decides which +// runtime libraries to record as dependencies of the output from the modules +// it sees imported while linking. Whole-archive input does not count: with a +// C placeholder the library came out referencing Foundation, Dispatch and the +// concurrency runtime without declaring a dependency on any of them, and +// every consumer failed to link. +// + +import Foundation +import Dispatch diff --git a/cmake/libdbus-swift.map b/cmake/libdbus-swift.map new file mode 100644 index 0000000..fd3580e --- /dev/null +++ b/cmake/libdbus-swift.map @@ -0,0 +1,114 @@ +/* + * Version script for libdbus-swift.so.0. + * + * The reference libdbus exports every symbol it defines and carries no version + * script. The list is pinned here instead, so that the Swift runtime's symbols + * and this library's internals stay local and the ABI surface is asserted + * rather than incidental. Keep in sync with cmake/symbols.txt, which + * Scripts/check-exports.sh verifies against the built library. + */ + +LIBDBUS_SWIFT_0 { +global: + /* Errors */ + dbus_error_free; + dbus_error_has_name; + dbus_error_init; + dbus_error_is_set; + dbus_move_error; + dbus_set_error; + dbus_set_error_const; + + /* Memory */ + dbus_free; + dbus_free_string_array; + dbus_malloc; + dbus_malloc0; + dbus_shutdown; + dbus_threads_init_default; + + /* Messages */ + dbus_message_append_args; + dbus_message_append_args_valist; + dbus_message_copy; + dbus_message_get_args; + dbus_message_get_args_valist; + dbus_message_get_destination; + dbus_message_get_error_name; + dbus_message_get_interface; + dbus_message_get_member; + dbus_message_get_no_reply; + dbus_message_get_path; + dbus_message_get_reply_serial; + dbus_message_get_sender; + dbus_message_get_serial; + dbus_message_get_signature; + dbus_message_get_type; + dbus_message_has_destination; + dbus_message_has_interface; + dbus_message_has_member; + dbus_message_has_path; + dbus_message_has_sender; + dbus_message_is_error; + dbus_message_is_method_call; + dbus_message_is_signal; + dbus_message_new; + dbus_message_new_error; + dbus_message_new_method_call; + dbus_message_new_method_return; + dbus_message_new_signal; + dbus_message_ref; + dbus_message_set_destination; + dbus_message_set_error_name; + dbus_message_set_interface; + dbus_message_set_member; + dbus_message_set_no_reply; + dbus_message_set_path; + dbus_message_set_sender; + dbus_message_unref; + + /* Message iteration */ + dbus_message_iter_abandon_container; + dbus_message_iter_append_basic; + dbus_message_iter_close_container; + dbus_message_iter_get_arg_type; + dbus_message_iter_get_basic; + dbus_message_iter_get_element_type; + dbus_message_iter_get_signature; + dbus_message_iter_has_next; + dbus_message_iter_init; + dbus_message_iter_init_append; + dbus_message_iter_next; + dbus_message_iter_open_container; + dbus_message_iter_recurse; + + /* Connections */ + dbus_connection_close; + dbus_connection_flush; + dbus_connection_get_is_authenticated; + dbus_connection_get_is_connected; + dbus_connection_get_server_id; + dbus_connection_open; + dbus_connection_open_private; + dbus_connection_ref; + dbus_connection_send; + dbus_connection_send_with_reply_and_block; + dbus_connection_unref; + + /* Bus operations */ + dbus_bus_add_match; + dbus_bus_get; + dbus_bus_get_id; + dbus_bus_get_private; + dbus_bus_get_unique_name; + dbus_bus_get_unix_user; + dbus_bus_name_has_owner; + dbus_bus_register; + dbus_bus_release_name; + dbus_bus_remove_match; + dbus_bus_request_name; + dbus_bus_start_service_by_name; + +local: + *; +}; diff --git a/cmake/symbols.txt b/cmake/symbols.txt new file mode 100644 index 0000000..f373796 --- /dev/null +++ b/cmake/symbols.txt @@ -0,0 +1,107 @@ +# Exported symbols of libdbus-swift.so. +# +# Scripts/check-exports.sh diffs the built library against this list and fails +# on a symbol in either direction: one listed but not exported means something +# was never implemented or the version script does not match, and one exported +# but not listed means an internal detail leaked into the ABI surface. +# +# Keep in sync with cmake/libdbus-swift.map. + +# Errors +dbus_error_free +dbus_error_has_name +dbus_error_init +dbus_error_is_set +dbus_move_error +dbus_set_error +dbus_set_error_const + +# Memory +dbus_free +dbus_free_string_array +dbus_malloc +dbus_malloc0 +dbus_shutdown +dbus_threads_init_default + +# Messages +dbus_message_append_args +dbus_message_append_args_valist +dbus_message_copy +dbus_message_get_args +dbus_message_get_args_valist +dbus_message_get_destination +dbus_message_get_error_name +dbus_message_get_interface +dbus_message_get_member +dbus_message_get_no_reply +dbus_message_get_path +dbus_message_get_reply_serial +dbus_message_get_sender +dbus_message_get_serial +dbus_message_get_signature +dbus_message_get_type +dbus_message_has_destination +dbus_message_has_interface +dbus_message_has_member +dbus_message_has_path +dbus_message_has_sender +dbus_message_is_error +dbus_message_is_method_call +dbus_message_is_signal +dbus_message_new +dbus_message_new_error +dbus_message_new_method_call +dbus_message_new_method_return +dbus_message_new_signal +dbus_message_ref +dbus_message_set_destination +dbus_message_set_error_name +dbus_message_set_interface +dbus_message_set_member +dbus_message_set_no_reply +dbus_message_set_path +dbus_message_set_sender +dbus_message_unref + +# Message iteration +dbus_message_iter_abandon_container +dbus_message_iter_append_basic +dbus_message_iter_close_container +dbus_message_iter_get_arg_type +dbus_message_iter_get_basic +dbus_message_iter_get_element_type +dbus_message_iter_get_signature +dbus_message_iter_has_next +dbus_message_iter_init +dbus_message_iter_init_append +dbus_message_iter_next +dbus_message_iter_open_container +dbus_message_iter_recurse + +# Connections +dbus_connection_close +dbus_connection_flush +dbus_connection_get_is_authenticated +dbus_connection_get_is_connected +dbus_connection_get_server_id +dbus_connection_open +dbus_connection_open_private +dbus_connection_ref +dbus_connection_send +dbus_connection_send_with_reply_and_block +dbus_connection_unref + +# Bus operations +dbus_bus_add_match +dbus_bus_get +dbus_bus_get_id +dbus_bus_get_private +dbus_bus_get_unique_name +dbus_bus_get_unix_user +dbus_bus_name_has_owner +dbus_bus_register +dbus_bus_release_name +dbus_bus_remove_match +dbus_bus_request_name +dbus_bus_start_service_by_name