Skip to content

Commit

Permalink
Add feature Interposable
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
rauhul committed Nov 10, 2023
1 parent 4dd2bc2 commit 9a4b7d9
Show file tree
Hide file tree
Showing 21 changed files with 886 additions and 24 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ test: build
@swift test \
--parallel \
--explicit-target-dependency-import-check error
@SWIFT_MMIO_FEATURE_INTERPOSABLE=1 swift test \
--parallel \
--explicit-target-dependency-import-check error

clean:
@echo "cleaning..."
Expand Down
37 changes: 36 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// swift-tools-version: 5.9

import CompilerPluginSupport
import Foundation
import PackageDescription

let package = Package(
var package = Package(
name: "swift-mmio",
platforms: [
.macOS(.v10_15),
Expand Down Expand Up @@ -51,3 +52,37 @@ let package = Package(

.target(name: "MMIOVolatile"),
])

// Replace this with a native spm feature flag if/when supported
let interposable = "FEATURE_INTERPOSABLE"
try package.defineFeature(named: interposable, override: nil) { package in
let targetAllowSet = Set(["MMIO", "MMIOVolatile", "MMIOMacros"])
package.targets = package.targets.filter { targetAllowSet.contains($0.name) }
package.targets.append(
.testTarget(name: "MMIOInterposableTests", dependencies: ["MMIO"]))
for target in package.targets {
target.swiftDefine(interposable)
}
}

extension Package {
func defineFeature(
named featureName: String,
override: Bool?,
body: (Package) throws -> Void
) throws {
let key = "SWIFT_MMIO_\(featureName)"
let environment = ProcessInfo.processInfo.environment[key] != nil
if override ?? environment {
try body(self)
}
}
}

extension Target {
func swiftDefine(_ value: String) {
var swiftSettings = self.swiftSettings ?? []
swiftSettings.append(.define(value))
self.swiftSettings = swiftSettings
}
}
12 changes: 6 additions & 6 deletions Sources/MMIO/BitFieldProjectable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,32 @@ public protocol BitFieldProjectable {
static var bitWidth: Int { get }

init<Storage>(storage: Storage)
where Storage: FixedWidthInteger, Storage: UnsignedInteger
where Storage: FixedWidthInteger & UnsignedInteger

func storage<Storage>(_: Storage.Type) -> Storage
where Storage: FixedWidthInteger, Storage: UnsignedInteger
where Storage: FixedWidthInteger & UnsignedInteger
}

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

public init<Storage>(storage: Storage)
where Storage: FixedWidthInteger, Storage: UnsignedInteger { fatalError() }
where Storage: FixedWidthInteger & UnsignedInteger { fatalError() }

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

extension Bool: BitFieldProjectable {
public static let bitWidth = 1

public init<Storage>(storage: Storage)
where Storage: FixedWidthInteger, Storage: UnsignedInteger {
where Storage: FixedWidthInteger & UnsignedInteger {
self = storage != 0b0
}

public func storage<Storage>(_: Storage.Type) -> Storage
where Storage: FixedWidthInteger, Storage: UnsignedInteger {
where Storage: FixedWidthInteger & UnsignedInteger {
self ? 0b1 : 0b0
}
}
Expand Down
26 changes: 26 additions & 0 deletions Sources/MMIO/MMIOInterposer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//===----------------------------------------------------------*- swift -*-===//
//
// This source file is part of the Swift MMIO open source project
//
// Copyright (c) 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

#if FEATURE_INTERPOSABLE
public protocol MMIOInterposer: AnyObject {
// FIXME: Documentation is wrong
/// Loads an instance of `value` from the address pointed to by pointer.
func load<Value>(
from pointer: UnsafePointer<Value>
) -> Value where Value: FixedWidthInteger & UnsignedInteger & _RegisterStorage

/// Stores an instance of `value` to the address pointed to by pointer.
func store<Value>(
_ value: Value,
to pointer: UnsafeMutablePointer<Value>
) where Value: FixedWidthInteger & UnsignedInteger & _RegisterStorage
}
#endif
2 changes: 1 addition & 1 deletion Sources/MMIO/MMIOMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//===----------------------------------------------------------------------===//

// RegisterBank macros
@attached(member, names: named(unsafeAddress), named(init))
@attached(member, names: named(unsafeAddress), named(init), named(interposer))
public macro RegisterBank() =
#externalMacro(module: "MMIOMacros", type: "RegisterBankMacro")

Expand Down
43 changes: 40 additions & 3 deletions Sources/MMIO/Register.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@
public struct Register<Value> where Value: RegisterValue {
public let unsafeAddress: UInt

#if FEATURE_INTERPOSABLE
public var interposer: (any MMIOInterposer)?
#endif

@inlinable @inline(__always)
public init(unsafeAddress: UInt) {
static func preconditionAligned(unsafeAddress: UInt) {
let alignment = MemoryLayout<Value.Raw.Storage>.alignment
#if hasFeature(Embedded)
// FIXME: Embedded doesn't have static interpolated strings yet
Expand All @@ -27,8 +31,22 @@ public struct Register<Value> where Value: RegisterValue {
unsafeAddress.isMultiple(of: UInt(alignment)),
"Misaligned address '\(unsafeAddress)' for data of type '\(Value.self)'")
#endif
}

#if FEATURE_INTERPOSABLE
@inlinable @inline(__always)
public init(unsafeAddress: UInt, interposer: (any MMIOInterposer)?) {
Self.preconditionAligned(unsafeAddress: unsafeAddress)
self.unsafeAddress = unsafeAddress
self.interposer = interposer
}
#else
@inlinable @inline(__always)
public init(unsafeAddress: UInt) {
Self.preconditionAligned(unsafeAddress: unsafeAddress)
self.unsafeAddress = unsafeAddress
}
#endif
}

extension Register {
Expand All @@ -39,12 +57,31 @@ extension Register {

@inlinable @inline(__always)
public func read() -> Value.Read {
Value.Read(Value.Raw(Value.Raw.Storage.load(from: self.pointer)))
let storage: Value.Raw.Storage
#if FEATURE_INTERPOSABLE
if let interposer = self.interposer {
storage = interposer.load(from: self.pointer)
} else {
storage = Value.Raw.Storage.load(from: self.pointer)
}
#else
storage = Value.Raw.Storage.load(from: self.pointer)
#endif
return Value.Read(Value.Raw(storage))
}

@inlinable @inline(__always)
public func write(_ newValue: Value.Write) {
Value.Raw.Storage.store(Value.Raw(newValue).storage, to: self.pointer)
let storage = Value.Raw(newValue).storage
#if FEATURE_INTERPOSABLE
if let interposer = self.interposer {
interposer.store(storage, to: self.pointer)
} else {
Value.Raw.Storage.store(storage, to: self.pointer)
}
#else
Value.Raw.Storage.store(storage, to: self.pointer)
#endif
}

@inlinable @inline(__always) @_disfavoredOverload
Expand Down
4 changes: 2 additions & 2 deletions Sources/MMIO/RegisterValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ extension RegisterValueRead {
/// manipulation of the register's bits.
///
/// Mutation through the raw view are unchecked. The user is responsible for
/// ensuring the bit pattern is valid.
/// ensuring the bit pattern yielded back to the read view is valid.
@_disfavoredOverload
@inlinable @inline(__always)
public var raw: Value.Raw {
Expand All @@ -61,7 +61,7 @@ extension RegisterValueWrite {
/// manipulation of the register's bits.
///
/// Mutation through the raw view are unchecked. The user is responsible for
/// ensuring the bit pattern is valid.
/// ensuring the bit pattern yielded back to the write view is valid.
@inlinable @inline(__always)
public var raw: Value.Raw {
_read {
Expand Down
14 changes: 14 additions & 0 deletions Sources/MMIOMacros/Macros/RegisterBankMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,23 @@ extension RegisterBankMacro: MMIOMemberMacro {
return [
"\(acl)private(set) var unsafeAddress: UInt",
"""
#if FEATURE_INTERPOSABLE
var interposer: (any MMIOInterposer)?
#endif
""",
"""
#if FEATURE_INTERPOSABLE
@inlinable @inline(__always)
\(acl)init(unsafeAddress: UInt, interposer: (any MMIOInterposer)?) {
self.unsafeAddress = unsafeAddress
self.interposer = interposer
}
#else
@inlinable @inline(__always)
\(acl)init(unsafeAddress: UInt) {
self.unsafeAddress = unsafeAddress
}
#endif
""",
]
}
Expand Down
8 changes: 7 additions & 1 deletion Sources/MMIOMacros/Macros/RegisterBankOffsetMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,13 @@ extension RegisterBankOffsetMacro: MMIOAccessorMacro {

return [
"""
@inlinable @inline(__always) get { .init(unsafeAddress: self.unsafeAddress + (\(raw: self.offset))) }
@inlinable @inline(__always) get {
#if FEATURE_INTERPOSABLE
return .init(unsafeAddress: self.unsafeAddress + (\(raw: self.$offset)), interposer: self.interposer)
#else
return .init(unsafeAddress: self.unsafeAddress + (\(raw: self.$offset)))
#endif
}
"""
]
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/MMIOMacros/Macros/RegisterDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ extension RegisterDescription {
"""
\(self.accessLevel)struct Raw: RegisterValueRaw {
\(self.accessLevel)typealias Value = \(self.name)
\(self.accessLevel)var storage: UInt\(raw: self.bitWidth)
\(self.accessLevel)typealias Storage = UInt\(raw: self.bitWidth)
\(self.accessLevel)var storage: Storage
\(self.accessLevel)init(_ storage: Storage) {
self.storage = storage
}
Expand Down
55 changes: 55 additions & 0 deletions Tests/MMIOInterposableTests/Extensions/StringInterpolation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//===----------------------------------------------------------*- swift -*-===//
//
// This source file is part of the Swift MMIO open source project
//
// Copyright (c) 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

extension String.StringInterpolation {
mutating func appendInterpolation(hexNibble value: UInt8) {
let ascii: UInt8
switch value {
case 0..<10:
ascii = UInt8(ascii: "0") + value
case 10..<16:
ascii = UInt8(ascii: "a") + (value - 10)
default:
preconditionFailure("Invalid hexNibble \(value)")
}
let character = Character(UnicodeScalar(ascii))
self.appendInterpolation(character)
}

mutating func appendInterpolation<Value>(
hex value: Value,
bytes size: Int? = nil
) where Value: FixedWidthInteger {
let valueSize = MemoryLayout<Value>.size
precondition((size ?? 0) <= valueSize)
let size = size ?? valueSize
let sizeIsEven = size.isMultiple(of: 2)

// Big endian so we can iterate from high to low byte
var value = value.bigEndian

let droppedBytes = valueSize - size
value >>= 8 * droppedBytes

self.appendLiteral("0x")
for offset in 0..<size {
if offset != 0, offset.isMultiple(of: 2) == sizeIsEven {
self.appendLiteral("_")
}
let byte = UInt8(truncatingIfNeeded: value)
let highNibble = byte >> 4
let lowNibble = byte & 0xf
self.appendInterpolation(hexNibble: highNibble)
self.appendInterpolation(hexNibble: lowNibble)
value >>= 8
}
}
}
Loading

0 comments on commit 9a4b7d9

Please sign in to comment.