Unsafe memory mutation
October 24, 2023Sometimes your memory can play nasty tricks on you…
Today let’s have fun with memory management by having a look on how we can mutate… immutable properties!
How is it possible?
Swift is a safe language. One such example is when declaring immutable properties: when doing so Swift compiler then provides restrictions/verifications to make sure those data stay indeed immutable.
However those restrictions are true only while compiling. During runtime you are “free” of playing with memory however you want! Using tools you can read/write it and of course… making your app crash 😉💥
Swift provides such tools. Compared to some other languages they are very interesting because they provide two abstraction level:
- First level preserves memory typing while manipulating memory, still providing a very safe API
- Second one provides raw pointers
So we could say that in addition of classic/high level API Swift also provides some mid and low level functions.
What’s the purpose?
Let’s be honest… not much 😂. At least there’s very few chance you ever need to use such an API one day (if you stick to building high level apps).
However not only can it be fun to study it might come handy in some libraries. For example we may need at some point to modify… an immutable property 🤪
struct MyStruct {
let isImmutable = true
}
var value = MyStruct(isImmutable: true) // let's try mutate... isImmutable 🤪
Let there be carnage var
Let’s first get a UnsafeMutablePointer<MyStruct>
from our objects using withUnsafeMutablePointer
:
withUnsafeMutablePointer(to: &value) { }
This pointer indicates where our object is located in memory. Now we need to locate isImmutable
property itself… and mutate it.
We could do some hazardous maths 🧮 to get memory offset but Swift does already provide everything we need with MemoryLayout
: given a KeyPath it gives us its corresponding offset 💪.
We can then combine it with UnsafeMutableRawPointer
. As its name implies we lose typing but we gain access to advanced(by:)
allowing us to move to a memory specific position. Once there we just have to do the mutation 😎
withUnsafeMutablePointer(to: &value) {
let pointer = UnsafeMutableRawPointer($0)
// get isImmutable offset in memory
guard let offset = MemoryLayout<MyStruct>.offset(of: \.isImmutable) else {
fatalError("Cannot use KeyPath<\(MyStruct.self), \(Value.self)> which is a computed KeyPath")
}
advanced(by: offset) // move to isImmutable in memory
.assumingMemoryBound(to: Bool.self) // cast memory to Bool
.pointee = false // change the memory!
}
print(value.isImmutable) // prints "false"
Et voila! Our property now equals false even though it was initially declared as let
and supposed to be immutable! 🧙♂️
While the code is not very complex we can actually do even simpler. Swift already provides a method to point directly to a KeyPath position. Best thing: it’s available on UnsafeMutablePointer
, so no typing loss!
withUnsafeMutablePointer(to: &value) {
/// get memory pointer related to our keypath
guard let keyPathPointer = $0.pointer(to: \.isImmutable) else {
fatalError("Cannot use KeyPath<\(MyStruct.self), \(Value.self)> which is a computed KeyPath")
}
/// get a mutable pointer from our (non mutable) pointer
let pointer = UnsafeMutablePointer(mutating: keyPathPointer)
/// mutate memory
pointer.pointee = false
}
Thanks to pointer(to:)
we got a (non mutable) pointer to our property. We then changed it to a mutable one to mutate the memory.
Pretty simple? But believe me before getting to this… I spent countless hours reading the documentation 📚
Array modification
While modifying a property is easy, modifying an array is one magnitude more complex.
If we apply same code on Wrapper
it will just crash:
struct Wrapper {
let array = ["a", "b"]
}
withUnsafeMutablePointer(to: &value) {
UnsafeMutablePointer(mutating: $0.pointer(to: \.array[0])!) // crash
pointer.pointee = "c"
}
In order to mutate Wrapper.array
we first we need to get array
property and then change data in index 0. Don’t expect help on UnsafeMutable*Pointer. You’ll need to use an obscure method from MutableCollection: withContiguousMutableStorageIfAvailable
!
withUnsafeMutablePointer(to: &value) {
let pointer = UnsafeMutablePointer(mutating: $0.pointer(to: \.array)!)
pointer.pointee.withContiguousMutableStorageIfAvailable {
$0[0] = "c"
}
}
print(value) // prints ["c", "b"]
And now we can mutate our marvelous array!
Generic
We can make those two samples more generic. First for a non collection value:
extension UnsafeMutablePointer {
func assign<Value>(to keyPath: KeyPath<Pointee, Value>, value: Value) {
guard let keyPathPointer = pointer(to: keyPath) else {
fatalError("Cannot use KeyPath<\(Pointee.self), \(Value.self)> which is a computed KeyPath")
}
let pointer = UnsafeMutablePointer<Value>(mutating: keyPathPointer)
pointer.pointee = value
}
}
var value = MyStruct()
withUnsafeMutablePointer(&value) {
$0.assign(to: \.isImmutable, value: false)
}
For collection we need to be careful to constrain to MutableCollection! Indeed withContiguousMutableStorageIfAvailable
also exists on Collection but does… nothing. Yes I got tricked by this one 😬
extension UnsafeMutablePointer {
func assign<C: MutableCollection>(to keyPath: KeyPath<Pointee, C>, index: C.Index, value: C.Element) where C.Index == Int {
guard let keyPathPointer = pointer(to: keyPath) else {
fatalError("Cannot use KeyPath<\(Pointee.self), \(C.self)> which is a computed KeyPath")
}
let pointer = UnsafeMutablePointer<C>(mutating: keyPathPointer)
pointer.pointee.withContiguousMutableStorageIfAvailable {
$0[index] = value
}
}
}
var value = Wrapper()
withUnsafeMutablePointer(&value) {
$0.assign(to: \.array, index: 0, value: "c")
}
Conclusion
This is probably the most enlightening yet non useful article you’ll ever read 😄.
Still now you know it’s perfectly possible to alter memory in Swift including non mutable properties 🙂