Skip to content

Commit e7c23c6

Browse files
committed
Add feature Interposable (#31)
Adds a feature flag called 'Interposable' which allows clients to replace the memory interface of register banks and registers with a custom interposer. Interposers must conform the `MMIOInterposer` protocol which has two simple requirements: `load(from:)` and `store(_:to:)`. Interposers can be used to unit test the correctness of drivers and other structures which abstract over MMIO operations. Internally MMIO uses a new type called MMIOTracingInterposer to test the interposer functionality. This type may become public API in the future to help clients test their code without needing to make their own type conforming to `MMIOInterposer`.
1 parent cdd54fc commit e7c23c6

22 files changed

+897
-24
lines changed

Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ test: build
4949
@swift test \
5050
--parallel \
5151
--explicit-target-dependency-import-check error
52+
@SWIFT_MMIO_FEATURE_INTERPOSABLE=1 swift test \
53+
--parallel \
54+
--explicit-target-dependency-import-check error
5255

5356
clean:
5457
@echo "cleaning..."

Package.swift

+36-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// swift-tools-version: 5.9
22

33
import CompilerPluginSupport
4+
import Foundation
45
import PackageDescription
56

6-
let package = Package(
7+
var package = Package(
78
name: "swift-mmio",
89
platforms: [
910
.macOS(.v10_15),
@@ -51,3 +52,37 @@ let package = Package(
5152

5253
.target(name: "MMIOVolatile"),
5354
])
55+
56+
// Replace this with a native spm feature flag if/when supported
57+
let interposable = "FEATURE_INTERPOSABLE"
58+
try package.defineFeature(named: interposable, override: nil) { package in
59+
let targetAllowSet = Set(["MMIO", "MMIOVolatile", "MMIOMacros"])
60+
package.targets = package.targets.filter { targetAllowSet.contains($0.name) }
61+
package.targets.append(
62+
.testTarget(name: "MMIOInterposableTests", dependencies: ["MMIO"]))
63+
for target in package.targets {
64+
target.swiftDefine(interposable)
65+
}
66+
}
67+
68+
extension Package {
69+
func defineFeature(
70+
named featureName: String,
71+
override: Bool?,
72+
body: (Package) throws -> Void
73+
) throws {
74+
let key = "SWIFT_MMIO_\(featureName)"
75+
let environment = ProcessInfo.processInfo.environment[key] != nil
76+
if override ?? environment {
77+
try body(self)
78+
}
79+
}
80+
}
81+
82+
extension Target {
83+
func swiftDefine(_ value: String) {
84+
var swiftSettings = self.swiftSettings ?? []
85+
swiftSettings.append(.define(value))
86+
self.swiftSettings = swiftSettings
87+
}
88+
}

Sources/MMIO/BitFieldProjectable.swift

+6-6
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,32 @@ public protocol BitFieldProjectable {
1313
static var bitWidth: Int { get }
1414

1515
init<Storage>(storage: Storage)
16-
where Storage: FixedWidthInteger, Storage: UnsignedInteger
16+
where Storage: FixedWidthInteger & UnsignedInteger
1717

1818
func storage<Storage>(_: Storage.Type) -> Storage
19-
where Storage: FixedWidthInteger, Storage: UnsignedInteger
19+
where Storage: FixedWidthInteger & UnsignedInteger
2020
}
2121

2222
extension Never: BitFieldProjectable {
2323
public static var bitWidth: Int { fatalError() }
2424

2525
public init<Storage>(storage: Storage)
26-
where Storage: FixedWidthInteger, Storage: UnsignedInteger { fatalError() }
26+
where Storage: FixedWidthInteger & UnsignedInteger { fatalError() }
2727

2828
public func storage<Storage>(_: Storage.Type) -> Storage
29-
where Storage: FixedWidthInteger, Storage: UnsignedInteger { fatalError() }
29+
where Storage: FixedWidthInteger & UnsignedInteger { fatalError() }
3030
}
3131

3232
extension Bool: BitFieldProjectable {
3333
public static let bitWidth = 1
3434

3535
public init<Storage>(storage: Storage)
36-
where Storage: FixedWidthInteger, Storage: UnsignedInteger {
36+
where Storage: FixedWidthInteger & UnsignedInteger {
3737
self = storage != 0b0
3838
}
3939

4040
public func storage<Storage>(_: Storage.Type) -> Storage
41-
where Storage: FixedWidthInteger, Storage: UnsignedInteger {
41+
where Storage: FixedWidthInteger & UnsignedInteger {
4242
self ? 0b1 : 0b0
4343
}
4444
}

Sources/MMIO/MMIOInterposer.swift

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//===----------------------------------------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift MMIO open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
#if FEATURE_INTERPOSABLE
13+
/// An object which can modify the behavior of register reads and writes for the
14+
/// purpose of unit testing.
15+
///
16+
/// MMIOInterposers must provide methods for registers to load and store values.
17+
/// However, conforming types may perform arbitrary logic within these methods.
18+
/// For example, an interposer may adjust the address before performing a load
19+
/// or store, it may load or store from a custom side allocation, or even simply
20+
/// track load and store counts and discard the actual values.
21+
public protocol MMIOInterposer: AnyObject {
22+
/// An interposition function to modify the behavior of a register read.
23+
///
24+
/// - Returns: A `Value` from the address referenced by `pointer`.
25+
func load<Value>(
26+
from pointer: UnsafePointer<Value>
27+
) -> Value where Value: FixedWidthInteger & UnsignedInteger & _RegisterStorage
28+
29+
/// An interposition function to modify the behavior of a register write.
30+
func store<Value>(
31+
_ value: Value,
32+
to pointer: UnsafeMutablePointer<Value>
33+
) where Value: FixedWidthInteger & UnsignedInteger & _RegisterStorage
34+
}
35+
#endif

Sources/MMIO/MMIOMacros.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
// RegisterBank macros
13-
@attached(member, names: named(unsafeAddress), named(init))
13+
@attached(member, names: named(unsafeAddress), named(init), named(interposer))
1414
public macro RegisterBank() =
1515
#externalMacro(module: "MMIOMacros", type: "RegisterBankMacro")
1616

Sources/MMIO/Register.swift

+40-3
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
public struct Register<Value> where Value: RegisterValue {
1515
public let unsafeAddress: UInt
1616

17+
#if FEATURE_INTERPOSABLE
18+
public var interposer: (any MMIOInterposer)?
19+
#endif
20+
1721
@inlinable @inline(__always)
18-
public init(unsafeAddress: UInt) {
22+
static func preconditionAligned(unsafeAddress: UInt) {
1923
let alignment = MemoryLayout<Value.Raw.Storage>.alignment
2024
#if hasFeature(Embedded)
2125
// FIXME: Embedded doesn't have static interpolated strings yet
@@ -27,8 +31,22 @@ public struct Register<Value> where Value: RegisterValue {
2731
unsafeAddress.isMultiple(of: UInt(alignment)),
2832
"Misaligned address '\(unsafeAddress)' for data of type '\(Value.self)'")
2933
#endif
34+
}
35+
36+
#if FEATURE_INTERPOSABLE
37+
@inlinable @inline(__always)
38+
public init(unsafeAddress: UInt, interposer: (any MMIOInterposer)?) {
39+
Self.preconditionAligned(unsafeAddress: unsafeAddress)
3040
self.unsafeAddress = unsafeAddress
41+
self.interposer = interposer
3142
}
43+
#else
44+
@inlinable @inline(__always)
45+
public init(unsafeAddress: UInt) {
46+
Self.preconditionAligned(unsafeAddress: unsafeAddress)
47+
self.unsafeAddress = unsafeAddress
48+
}
49+
#endif
3250
}
3351

3452
extension Register {
@@ -39,12 +57,31 @@ extension Register {
3957

4058
@inlinable @inline(__always)
4159
public func read() -> Value.Read {
42-
Value.Read(Value.Raw(Value.Raw.Storage.load(from: self.pointer)))
60+
let storage: Value.Raw.Storage
61+
#if FEATURE_INTERPOSABLE
62+
if let interposer = self.interposer {
63+
storage = interposer.load(from: self.pointer)
64+
} else {
65+
storage = Value.Raw.Storage.load(from: self.pointer)
66+
}
67+
#else
68+
storage = Value.Raw.Storage.load(from: self.pointer)
69+
#endif
70+
return Value.Read(Value.Raw(storage))
4371
}
4472

4573
@inlinable @inline(__always)
4674
public func write(_ newValue: Value.Write) {
47-
Value.Raw.Storage.store(Value.Raw(newValue).storage, to: self.pointer)
75+
let storage = Value.Raw(newValue).storage
76+
#if FEATURE_INTERPOSABLE
77+
if let interposer = self.interposer {
78+
interposer.store(storage, to: self.pointer)
79+
} else {
80+
Value.Raw.Storage.store(storage, to: self.pointer)
81+
}
82+
#else
83+
Value.Raw.Storage.store(storage, to: self.pointer)
84+
#endif
4885
}
4986

5087
@inlinable @inline(__always) @_disfavoredOverload

Sources/MMIO/RegisterValue.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ extension RegisterValueRead {
3535
/// manipulation of the register's bits.
3636
///
3737
/// Mutation through the raw view are unchecked. The user is responsible for
38-
/// ensuring the bit pattern is valid.
38+
/// ensuring the bit pattern yielded back to the read view is valid.
3939
@_disfavoredOverload
4040
@inlinable @inline(__always)
4141
public var raw: Value.Raw {
@@ -61,7 +61,7 @@ extension RegisterValueWrite {
6161
/// manipulation of the register's bits.
6262
///
6363
/// Mutation through the raw view are unchecked. The user is responsible for
64-
/// ensuring the bit pattern is valid.
64+
/// ensuring the bit pattern yielded back to the write view is valid.
6565
@inlinable @inline(__always)
6666
public var raw: Value.Raw {
6767
_read {

Sources/MMIOMacros/Macros/RegisterBankMacro.swift

+14
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,23 @@ extension RegisterBankMacro: MMIOMemberMacro {
6565
return [
6666
"\(acl)private(set) var unsafeAddress: UInt",
6767
"""
68+
#if FEATURE_INTERPOSABLE
69+
var interposer: (any MMIOInterposer)?
70+
#endif
71+
""",
72+
"""
73+
#if FEATURE_INTERPOSABLE
74+
@inlinable @inline(__always)
75+
\(acl)init(unsafeAddress: UInt, interposer: (any MMIOInterposer)?) {
76+
self.unsafeAddress = unsafeAddress
77+
self.interposer = interposer
78+
}
79+
#else
80+
@inlinable @inline(__always)
6881
\(acl)init(unsafeAddress: UInt) {
6982
self.unsafeAddress = unsafeAddress
7083
}
84+
#endif
7185
""",
7286
]
7387
}

Sources/MMIOMacros/Macros/RegisterBankOffsetMacro.swift

+7-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,13 @@ extension RegisterBankOffsetMacro: MMIOAccessorMacro {
121121

122122
return [
123123
"""
124-
@inlinable @inline(__always) get { .init(unsafeAddress: self.unsafeAddress + (\(raw: self.offset))) }
124+
@inlinable @inline(__always) get {
125+
#if FEATURE_INTERPOSABLE
126+
return .init(unsafeAddress: self.unsafeAddress + (\(raw: self.$offset)), interposer: self.interposer)
127+
#else
128+
return .init(unsafeAddress: self.unsafeAddress + (\(raw: self.$offset)))
129+
#endif
130+
}
125131
"""
126132
]
127133
}

Sources/MMIOMacros/Macros/RegisterDescription.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ extension RegisterDescription {
9999
"""
100100
\(self.accessLevel)struct Raw: RegisterValueRaw {
101101
\(self.accessLevel)typealias Value = \(self.name)
102-
\(self.accessLevel)var storage: UInt\(raw: self.bitWidth)
102+
\(self.accessLevel)typealias Storage = UInt\(raw: self.bitWidth)
103+
\(self.accessLevel)var storage: Storage
103104
\(self.accessLevel)init(_ storage: Storage) {
104105
self.storage = storage
105106
}

Tests/MMIOFileCheckTests/MMIOFileCheckTestCase.swift

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ final class MMIOFileCheckTests: XCTestCase {
6767
self.record(issue)
6868
}
6969
}
70+
71+
print("Finished running \(tests.count) tests")
7072
}
7173
}
7274

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//===----------------------------------------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift MMIO open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
extension String.StringInterpolation {
13+
mutating func appendInterpolation(hexNibble value: UInt8) {
14+
let ascii: UInt8
15+
switch value {
16+
case 0..<10:
17+
ascii = UInt8(ascii: "0") + value
18+
case 10..<16:
19+
ascii = UInt8(ascii: "a") + (value - 10)
20+
default:
21+
preconditionFailure("Invalid hexNibble \(value)")
22+
}
23+
let character = Character(UnicodeScalar(ascii))
24+
self.appendInterpolation(character)
25+
}
26+
27+
mutating func appendInterpolation<Value>(
28+
hex value: Value,
29+
bytes size: Int? = nil
30+
) where Value: FixedWidthInteger {
31+
let valueSize = MemoryLayout<Value>.size
32+
precondition((size ?? 0) <= valueSize)
33+
let size = size ?? valueSize
34+
let sizeIsEven = size.isMultiple(of: 2)
35+
36+
// Big endian so we can iterate from high to low byte
37+
var value = value.bigEndian
38+
39+
let droppedBytes = valueSize - size
40+
value >>= 8 * droppedBytes
41+
42+
self.appendLiteral("0x")
43+
for offset in 0..<size {
44+
if offset != 0, offset.isMultiple(of: 2) == sizeIsEven {
45+
self.appendLiteral("_")
46+
}
47+
let byte = UInt8(truncatingIfNeeded: value)
48+
let highNibble = byte >> 4
49+
let lowNibble = byte & 0xf
50+
self.appendInterpolation(hexNibble: highNibble)
51+
self.appendInterpolation(hexNibble: lowNibble)
52+
value >>= 8
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)