Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
86f0aa1
Add Parcel read and write
colemancda Aug 2, 2026
f50b7c1
Add Parcel tests
colemancda Aug 2, 2026
ab9edf7
Add the mmap of the driver's receive buffer
colemancda Aug 2, 2026
6c7f374
Add tests for the mapped buffer size
colemancda Aug 2, 2026
c08417d
Add BINDER_SET_MAX_THREADS
colemancda Aug 2, 2026
37bba90
Add a transaction-capable connection
colemancda Aug 2, 2026
20d081d
Skip the device tests when there is no binder device
colemancda Aug 2, 2026
f2fdc62
Add the BC_* command stream
colemancda Aug 2, 2026
3202834
Add the BR_* return stream
colemancda Aug 2, 2026
b3c9d25
Add driver stream tests
colemancda Aug 2, 2026
ff5b21a
Add an interface-mismatch parcel error
colemancda Aug 2, 2026
f1241cf
Allow a command stream to be rebuilt from bytes
colemancda Aug 2, 2026
79509e4
Add the binder error type
colemancda Aug 2, 2026
edc251f
Add synchronous transactions
colemancda Aug 2, 2026
ea63388
Add the AIDL interface token
colemancda Aug 2, 2026
784ac60
Add remote object references
colemancda Aug 2, 2026
3585b58
Add the service manager client
colemancda Aug 2, 2026
c044b81
Add service manager and interface token tests
colemancda Aug 2, 2026
8aca946
Make BinderConnection a class
colemancda Aug 2, 2026
494d53e
Add async transactions and lookups
colemancda Aug 2, 2026
7ea5b6e
Add connection tests and surface compile checks
colemancda Aug 2, 2026
2e2d2d3
Track binder object offsets in a parcel
colemancda Aug 3, 2026
c054835
Record an object's offset when appending it, and enumerate a parcel's…
colemancda Aug 3, 2026
d427de1
Add reference counting on remote handles
colemancda Aug 3, 2026
22fad17
Acquire a reply's objects before releasing its buffer
colemancda Aug 3, 2026
a8d8867
Add an owning strong reference
colemancda Aug 3, 2026
387fa42
Return owning references, and enable addService
colemancda Aug 3, 2026
b6d698e
Drop the offsets-unsupported error
colemancda Aug 3, 2026
bc66591
Follow the new transaction signatures
colemancda Aug 3, 2026
5d5e4b7
Add reference counting and offset tests
colemancda Aug 3, 2026
6994e6b
Cover the proxy and reference-counting surfaces in the compile checks
colemancda Aug 3, 2026
f204f7f
Add objects this process hosts
colemancda Aug 3, 2026
07ae60d
Add the serving loop
colemancda Aug 3, 2026
f020762
Hold the hosted objects and the serving flag
colemancda Aug 3, 2026
5f2ee1f
Add local object and looper tests
colemancda Aug 3, 2026
e7c41d2
Cover hosting and serving in the compile checks
colemancda Aug 3, 2026
47c6fa4
Add death notifications and liveness checks
colemancda Aug 3, 2026
11f477e
Deliver death notifications from the serving loop
colemancda Aug 3, 2026
d84885c
Hold the death notification registry
colemancda Aug 3, 2026
6746ee7
Add death notification tests
colemancda Aug 3, 2026
7e81b54
Cover death notifications and ping in the compile checks
colemancda Aug 3, 2026
c9cd47a
Register hosted objects with the service manager
colemancda Aug 3, 2026
a440887
Add BINDER_THREAD_EXIT
colemancda Aug 3, 2026
d8f679e
Send BINDER_THREAD_EXIT on loop exit, and decode BR_TRANSACTION_SEC_CTX
colemancda Aug 3, 2026
fda5615
Raise platform minimums for Synchronization.Mutex
colemancda Aug 3, 2026
6e4a4d7
Run the test suite on Linux
colemancda Aug 3, 2026
30d1afe
Document the Swift API
colemancda Aug 3, 2026
604b9c9
Add tests for hosted registration, thread exit and secctx
colemancda Aug 3, 2026
bf4f1ee
Cover hosted registration and thread exit in the compile checks
colemancda Aug 3, 2026
4fbfb23
Add file-descriptor passing
colemancda Aug 3, 2026
2bae898
Add BINDER_GET_EXTENDED_ERROR
colemancda Aug 3, 2026
b0b9f14
Add weak references and promotion
colemancda Aug 3, 2026
f455315
Add weak() and expose the connection to the weak proxy
colemancda Aug 3, 2026
fae819f
Add file-descriptor passing tests
colemancda Aug 3, 2026
ba781fe
Add weak reference and extended error tests
colemancda Aug 3, 2026
43f24dd
Cover fd passing, weak references and lastError in the compile checks
colemancda Aug 3, 2026
38eb7ca
Add out-of-line buffer objects
colemancda Aug 3, 2026
2634fac
Store scatter-gather buffers on the parcel
colemancda Aug 3, 2026
c901da8
Send BC_TRANSACTION_SG and copy received buffers out
colemancda Aug 3, 2026
3d6449e
Reply with BC_REPLY_SG when a reply carries buffers
colemancda Aug 3, 2026
ba887f7
Add buffer object tests
colemancda Aug 3, 2026
5f9adeb
Cover a scatter-gather request in the compile checks
colemancda Aug 3, 2026
0d518c4
Add end-to-end round-trip integration tests
colemancda Aug 3, 2026
3b808ad
Add a wrong-interface parcel error
colemancda Aug 3, 2026
329d7ff
Add call and reply helpers
colemancda Aug 3, 2026
4a29ccf
Add call helper tests
colemancda Aug 3, 2026
856c38a
Cover the call helpers in the compile checks
colemancda Aug 3, 2026
d036926
Show the call and reply helpers in the examples
colemancda Aug 3, 2026
8fd8016
Add becoming the context manager
colemancda Aug 3, 2026
e625616
Discard the unused removeValue result in the registry
colemancda Aug 3, 2026
dc25243
Add context manager tests
colemancda Aug 3, 2026
5e2b695
Cover becoming the context manager in the compile checks
colemancda Aug 3, 2026
9eeacc1
Add nested buffer objects
colemancda Aug 3, 2026
21136a7
Add nested buffer tests
colemancda Aug 3, 2026
bc0f160
Update the limitations for nested buffers and single fds
colemancda Aug 3, 2026
016fa62
Build CI against Swift 6.3.3 on macOS, Linux and Android
colemancda Aug 3, 2026
187f131
Import the Bionic C library on Android
colemancda Aug 3, 2026
73678a8
Drop Hashable from BinderError for Swift 6.0 compatibility
colemancda Aug 3, 2026
133ac84
Drop Hashable from ReturnStream.Element for Swift 6.0 compatibility
colemancda Aug 3, 2026
0eebd8e
Install the macOS toolchain with SwiftyLab/setup-swift
colemancda Aug 3, 2026
fa1e11f
Require Swift 6.3 as the minimum toolchain
colemancda Aug 3, 2026
6abaf78
Restore Hashable on BinderError and ReturnStream.Element
colemancda Aug 3, 2026
00cbec1
Support musl libc in the C shim
colemancda Aug 3, 2026
c130c5e
Add a musl cross-compile CI job
colemancda Aug 3, 2026
c71b1cb
Build the ARM CI against Swift 6.3.3
colemancda Aug 3, 2026
a485d18
Build and test on native arm64 Linux
colemancda Aug 3, 2026
7a69903
Fix the mmap failure check on Bionic
colemancda Aug 3, 2026
35d7c73
Fix the mocking-path build on macOS
colemancda Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/swift-arm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
config: ["debug" , "release"]
swift: ["6.1.2"]
swift: ["6.3.3"]
linux: ["raspios"]
release: ["bookworm"]
arch: ["armv6", "armv7"]
Expand All @@ -20,7 +20,7 @@ jobs:
run: apt update -y; apt install wget -y
- name: Install SDK
run: |
wget https://github.com/xtremekforever/swift-armv7/releases/download/${{ matrix.swift }}/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz
wget https://github.com/swift-embedded-linux/armhf-debian/releases/download/${{ matrix.swift }}/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz
tar -xvf swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz
mv swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }} /opt/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}
- name: Swift Version
Expand All @@ -34,7 +34,7 @@ jobs:
strategy:
matrix:
config: ["debug" , "release"]
swift: ["6.1.2"]
swift: ["6.3.3"]
linux: ["debian"]
release: ["bookworm", "bullseye"]
arch: ["armv7"]
Expand All @@ -46,7 +46,7 @@ jobs:
run: apt update -y; apt install wget -y
- name: Install SDK
run: |
wget https://github.com/xtremekforever/swift-armv7/releases/download/${{ matrix.swift }}/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz
wget https://github.com/swift-embedded-linux/armhf-debian/releases/download/${{ matrix.swift }}/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz
tar -xvf swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}-sdk.tar.gz
mv swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }} /opt/swift-${{ matrix.swift }}-RELEASE-${{ matrix.linux }}-${{ matrix.release }}-${{ matrix.arch }}
- name: Swift Version
Expand All @@ -64,7 +64,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
swift: ["6.0.3"]
swift: ["6.3.3"]
container: swift:${{ matrix.swift }}
steps:
- name: Checkout
Expand All @@ -73,7 +73,7 @@ jobs:
run: apt update -y; apt install wget -y
- name: Install SDK
run: |
wget https://github.com/xtremekforever/swift-armv7/releases/download/${{ matrix.swift }}/swift-${{ matrix.swift }}-RELEASE-debian-bookworm-armv7-sdk.tar.gz
wget https://github.com/swift-embedded-linux/armhf-debian/releases/download/${{ matrix.swift }}/swift-${{ matrix.swift }}-RELEASE-debian-bookworm-armv7-sdk.tar.gz
tar -xvf swift-${{ matrix.swift }}-RELEASE-debian-bookworm-armv7-sdk.tar.gz
mv swift-${{ matrix.swift }}-RELEASE-debian-bookworm-armv7 /opt/swift-${{ matrix.swift }}-RELEASE-debian-bookworm-armv7
- name: Swift Version
Expand Down
93 changes: 73 additions & 20 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
name: Swift
on: [push]

env:
# The single toolchain the whole matrix builds against. Bump here.
SWIFT_VERSION: "6.3.3"

jobs:

macos:
Expand All @@ -12,43 +17,91 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
# The macos-15 runner's bundled Swift trails the pinned version, so the
# toolchain is installed rather than taken from Xcode. SwiftyLab/setup-swift
# resolves versions from swift.org dynamically via swiftly, so it can
# install a release the older swift-actions/setup-swift does not yet list.
- name: Install Swift ${{ env.SWIFT_VERSION }}
uses: SwiftyLab/setup-swift@latest
with:
swift-version: ${{ env.SWIFT_VERSION }}
- name: Swift Version
run: swift --version
- name: Build
run: ${{ matrix.options }} swift build -c ${{ matrix.config }}
- name: Test
run: ${{ matrix.options }} swift test -c ${{ matrix.config }}

linux:
name: Linux
strategy:
fail-fast: false
matrix:
container: ["swift:6.0.3", "swift:6.1.2"]
# x86_64 and native arm64. arm64 is the architecture Android and most
# binder targets actually run on, so testing there rather than only
# cross-compiling to it is worth a runner.
os: ["ubuntu-latest", "ubuntu-24.04-arm"]
config: ["debug", "release"]
options: ["", "SWIFT_BUILD_DYNAMIC_LIBRARY=1"]
runs-on: ubuntu-latest
container: ${{ matrix.container }}-jammy
runs-on: ${{ matrix.os }}
container: swift:6.3.3
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Swift Version
run: swift --version
- name: Build
run: ${{ matrix.options }} swift build -c ${{ matrix.config }}
# The suite runs the whole protocol layer without a binder device: the
# two tests that need /dev/binder skip when it is absent, which it is on
# a CI runner. So Linux, the platform this library actually targets, now
# tests rather than only building.
- name: Test
run: ${{ matrix.options }} swift test -c ${{ matrix.config }}

android-arm:
name: Android
strategy:
fail-fast: false
matrix:
swift: ['6.1', 'nightly-6.2']
arch: ["aarch64", "x86_64"]
runs-on: macos-15
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: "Build Swift Package for Android"
run: |
brew install skiptools/skip/skip || (brew update && brew install skiptools/skip/skip)
skip android sdk install --version ${{ matrix.swift }}
ANDROID_NDK_ROOT="" skip android build --arch ${{ matrix.arch }}
android:
name: Android
strategy:
fail-fast: false
matrix:
arch: ["aarch64", "x86_64"]
runs-on: macos-15
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: "Build Swift Package for Android"
run: |
brew install skiptools/skip/skip || (brew update && brew install skiptools/skip/skip)
skip android sdk install --version ${{ env.SWIFT_VERSION }}
ANDROID_NDK_ROOT="" skip android build --arch ${{ matrix.arch }}

# Cross-compile against Apple's Static Linux SDK, which is musl-based. This
# is what exercises the musl path in CBinder — the kernel uapi headers are
# absent from the musl sysroot, so the C shim falls back to defining the
# __uN types itself and taking the ioctl macros from <sys/ioctl.h>.
musl:
name: Linux (musl)
strategy:
fail-fast: false
matrix:
arch: ["aarch64", "x86_64"]
runs-on: ubuntu-latest
container: swift:6.3.3
env:
# The Static Linux SDK bundle for SWIFT_VERSION. The checksum is
# published in swift.org's releases.json and must be updated alongside
# SWIFT_VERSION — it is specific to the exact bundle.
STATIC_SDK_URL: "https://download.swift.org/swift-6.3.3-release/static-sdk/swift-6.3.3-RELEASE/swift-6.3.3-RELEASE_static-linux-0.1.0.artifactbundle.tar.gz"
STATIC_SDK_CHECKSUM: "87c3eaf908e67c0e13a84367119e12273cec1d2cd3d81f7d74bb36722d6b607b"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Swift Version
run: swift --version
- name: Install the Static Linux SDK
run: swift sdk install "$STATIC_SDK_URL" --checksum "$STATIC_SDK_CHECKSUM"
- name: Build
# A binder client is not statically linkable in practice, but the point
# here is to compile the source against musl, so the ordinary product
# build is what matters.
run: swift build --swift-sdk ${{ matrix.arch }}-swift-linux-musl
15 changes: 10 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 6.0
// swift-tools-version: 6.3
import PackageDescription
import class Foundation.ProcessInfo

Expand All @@ -9,10 +9,15 @@ let libraryType: PackageDescription.Product.Library.LibraryType? = dynamicLibrar
let package = Package(
name: "Binder",
platforms: [
.macOS(.v10_15),
.iOS(.v13),
.watchOS(.v6),
.tvOS(.v13),
// Raised for Synchronization.Mutex, which the serving loop and the
// object registries use and which is macOS 15 / iOS 18 / watchOS 11 /
// tvOS 18 or newer. These are the Darwin mocking-build minimums; the
// real target is Linux and Android, where the version gate does not
// apply.
.macOS(.v15),
.iOS(.v18),
.watchOS(.v11),
.tvOS(.v18),
],
products: [
.library(
Expand Down
146 changes: 145 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,146 @@
# Binder
Swift Android Binder library

A Swift client for the Android Binder IPC driver (`/dev/binder`), talking the
same protocol as `libbinder` so it can call, and be called by, Android services
— on Android itself or on desktop Linux driving a container such as Waydroid.

Pure Swift, on `apple/swift-system` and `PureSwift/Socket`. No C++, no
`libbinder`.

## Status

The full protocol layer is implemented and unit-tested: the parcel wire format,
the `BINDER_WRITE_READ` command codec, transactions in both directions, the
service manager, reference counting, hosting local objects, a serving loop, and
death notifications.

**None of it has run against a real driver.** The test host has no binder
kernel module, so every ioctl path is reasoned from the kernel source and
`libbinder` rather than observed. The tests cover encoding, decoding, the object
registries and the loop's dispatch decisions — not the kernel conversation. Treat
this as protocol-complete and field-unproven.

## Calling a service

Every AIDL call has the same envelope around its arguments — interface token,
transact, exception header, then the results. The `call` helper writes that
envelope so a method supplies only its code and arguments:

```swift
let binder = try BinderConnection()

guard let activity = try binder.service(named: "activity") else { return }

var reader = try activity.call(1, interface: "android.app.IActivityManager") {
$0.append("com.example")
}
let result = try reader.readString() // reader starts at the results
```

The envelope is still there if you want it. The above is exactly:

```swift
var request = Parcel()
request.appendInterfaceToken("android.app.IActivityManager")
request.append("com.example")

let reply = try activity.transact(code: 1, request: request)
var reader = reply.reader()
let exception = try reader.readInt32() // non-zero throws remoteException in call()
let result = try reader.readString()
```

`post` is the one-way form (no reply); `call` also has an `async` overload.

`transact`, `service`, `checkService` and `services` each have an `async`
overload. They are not natively asynchronous — a binder transaction blocks in the
driver until the peer replies — so the async forms move that blocking call off
the cooperative pool onto the connection's own queue. See
`Transaction+Async.swift` for what that costs.

A looked-up service is a `RemoteProxy`: it owns one strong reference and releases
it on `deinit`, so the driver's reference count and Swift's object graph stay in
step without manual `acquire`/`release`.

## Hosting a service

```swift
let thing = binder.makeLocalObject(interface: "com.example.IThing") { transaction in
guard transaction.senderUserID == 1000 else { // driver-supplied, unforgeable
return Parcel.exception(-1) // EX_SECURITY
}
do {
var args = try transaction.arguments(interface: "com.example.IThing")
let name = try args.readString() ?? ""
return Parcel.reply { $0.append("hello:\(name)") }
} catch {
return Parcel.exception(-1)
}
}

try binder.addService(named: "com.example.thing", object: thing)

try binder.serve() // blocks, dispatching incoming calls
```

`transaction.arguments(interface:)` reads and checks the interface token,
returning a reader at the first argument; `Parcel.reply` / `Parcel.exception`
write the reply's exception header for you.

Register before serving: a caller that finds the service first simply blocks
until the loop starts, rather than failing. Only `transaction.senderProcessID`
and `senderUserID` are trustworthy for a permission check — the driver fills them
in; everything in the request parcel is whatever the caller chose to write.

## Death notifications

The only race-free way to learn a peer has gone — a handle never becomes
detectably invalid on its own, and the driver may reissue a dead handle's number
for a different object.

```swift
let watch = try binder.notifyOnDeath(of: activity.object) {
// runs on a serving thread
}
try binder.cancel(watch) // confirmed by the driver, not retired locally
```

## Design

| Type | Role |
|---|---|
| `BinderConnection` | an open, mapped device; the entry point for everything |
| `Parcel` | the serialised payload — read and write, with object offsets tracked |
| `RemoteObject` / `RemoteProxy` | a handle, and an owning strong reference to one |
| `LocalObject` | an object this process hosts for others to call |
| `ServiceManager` | the context manager at handle 0 |
| `DeathNotification` | a standing watch on a remote object's process |
| `CommandStream` / `ReturnStream` | the `BC_*` / `BR_*` codec under `BINDER_WRITE_READ` |

The layering is deliberate. `Binder` is a bare open device — enough to read the
protocol version, nothing more. `BinderConnection` is the usable thing: it maps
the receive buffer, checks the version, and refuses to continue on a mismatch,
in the order `libbinder`'s `ProcessState` does. Deciding what an incoming command
requires (`action(for:)`) is kept separate from sending the response, so the part
where a serving loop actually goes wrong is a pure, testable function.

## Building

```sh
swift build
swift test
```

Requires Swift 6.0+. The two tests that need `/dev/binder` skip when it is
absent, so the suite passes on any host; the rest exercise the protocol layer
directly.

## Not yet implemented

- Arrays of file descriptors (`BINDER_TYPE_FDA`). Single descriptors
(`BINDER_TYPE_FD`) and scatter-gather buffers, including nested ones, are
supported.
- The freeze and node-debug maintenance ioctls (`BINDER_FREEZE`,
`BINDER_GET_NODE_DEBUG_INFO`, `BINDER_GET_NODE_INFO_FOR_REF`).
- `serve()` cannot interrupt itself while parked in the driver; closing the
connection is what ends a quiet loop.
Loading
Loading