Hello, Swift Community!
Me and @rauhul would like to make a pitch for adding low-level support for volatile memory operations in Swift, which are very common and necessary in embedded programming for configuring hardware devices. This is not supposed to be a complete user-facing high-level solution for volatile operations, quite the opposite: This should be only the very first step that makes the most primitive low-level unsafe volatile operations available, and assumes that safe(r) layers are built on top of those. Concretely, the swift-mmio is an existing library that provides structured high-level MMIO APIs. It today uses a C header to perform the actual volatile operations using Clang's volatile support. This proposal aims to only improve the internal implementation of libraries like swift-mmio by giving them a way to perform volatile access directly in Swift code, but there's no expectation that the described low-level primitives would end up surfaced in user-facing APIs.
The implementation for this pitch can be seen in this PR: https://github.com/apple/swift/pull/70944.
Introduction
Volatile operations are needed to program registers in environments like firmware for microcontrollers or drivers.
This proposal:
- adds APIs for the most basic load + store volatile operations, on 8, 16, 32 and 64 bit integers,
- assumes the actual volatile semantics are defined by Clang and LLVM, which match the commonly understood behavior (no removal or reordering by the compiler), see the definition at https://llvm.org/docs/LangRef.html#volatile-memory-accesses,
- packages the new APIs into a separate module which must be explicitly imported by the user (
import Volatile
) -- this is intended to prevent accidental usage of volatile operations in e.g. userspace code for inter-thread synchronization (a common misuse of volatile in C).
This proposal:
- doesn't try to provide any safety or structured access to these operations, as that is left for libraries built on top of the low-level APIs,
- doesn't try to make it possible to mark struct members or variables as "volatile" (like C allows), instead only a pointer to storage can be volatile and a load/store using an eligible pointer can be volatile -- this sidesteps many design issues around volatile that C has,
- doesn't try to provide an abstraction over making more types volatile (e.g. how the AtomicValue and AtomicRepresentation protocols in swift-atomics provide an abstraction for making custom types atomic) -- the reasoning is that unlike atomics, volatile operations are only intended for MMIO HW register access and similar usage, which is only meaningful on machine level integer types and allowing custom types to become volatile would encourage misuse for inter-thread synchronization.
Proposed solution
The proposal is to create a new library/module called "Volatile" that would be shipped with the toolchain (given how close the logic is to the compiler implementation) and would define the following API set:
struct UnsafeVolatilePointer<Pointee> {
init(bitPattern: UInt)
}
extension UnsafeVolatilePointer<UInt8> { // also 16, 32, 64
func load() -> UInt8 // with LLVM volatile load semantics, assume natural alignment
func store(_ value: UInt8) // with LLVM volatile store semantics, assume natural alignment
}
For convenience, the module would also define helper extensions on UnsafeMutablePointer:
extension UnsafeMutablePointer<UInt8> { // also 16, 32, 64
func volatileLoad() -> UInt8 // with LLVM volatile load semantics, assume natural alignment
func volatileStore(_ value: UInt8) // with LLVM volatile store semantics, assume natural alignment
}
An example usage of these APIs by e.g. a piece of code running on an embedded device:
import Volatile
func turnLEDOnOrOff(enable: Bool) {
let gpio_a1_enable = UnsafeVolatilePointer<UInt32>(bitPattern: GPIO_BASE + GPIO_A1_ENABLE_OFFSET)
gpio_a1_enable.store(enable ? 0x1 : 0x0)
}
However, as mentioned above, we don't expect application level code to use UnsafeVolatilePointer or volatileLoad/volatileStore directly. Instead a higher-level library should provide a structured and safe access to the semantics of the hardware, for example:
import MMIO
func turnLEDOnOrOff(enable: Bool) {
// pseudo-code, not part of this proposal:
GPIO.a1.modify { $0.enable = enable } // the accessors compute the correct pointee/offset and perform a volatile store
}
Because of the very limited intended usage of these low-level volatile operations, the proposed solution is not trying to add anything beyond the absolute minimum set of primitives. Concretely:
- UnsafeVolatilePointer does not offer any pointer arithmetic facilities.
- There is no conversion APIs between UnsafePointer and UnsafeVolatilePointer.
- There is no structured / offset-based API on UnsafeVolatilePointer.
Detour: The complexity of "volatile" in C
In C, "volatile" is a type qualifier, and it's specifically allowed even on non-pointer types and on aggregate types. Ostensibly correct use of volatile pointers in C may produce operations violating hardware requirements in innocuous settings. See the following examples:
typedef struct {
volatile uint8_t field0;
volatile uint32_t field1;
} device_t;
// User might try to copy the value of one memory-mapped device_t directly to another:
volatile device_t* inst0 = (volatile device_t*)0xfe00;
volatile device_t* inst1 = (volatile device_t*)0xff00;
*inst0 = *inst1; // âś— on 32-bit platforms results in a memcpy optimized to two 32 bit load store pairs
This shows that the convenience of volatile in C has some dangerous sharp edges. This proposal specifically aims to avoid this problem by only allowing volatile operations on machine level integer types.
Conclusion
As mentioned, the intended usage of these low-level primitive operations is very limited, and users who need to access MMIO registers should prefer high-level structured safe abstractions like what swift-mmio provides. For those users, this proposal doesn't change anything. For implementors of library code that implements these abstractions and for code that for some reason needs to perform direct MMIO device accesses, this proposal provides a basic set of primitive operations for volatile loads and stores to that they don't need to resort to workarounds like bridging headers.
Thoughts?