Skip to content

Instantly share code, notes, and snippets.

@helje5
Created January 28, 2018 17:16
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save helje5/e5e6e50af60d0ee62169ac815f60d6d3 to your computer and use it in GitHub Desktop.
Save helje5/e5e6e50af60d0ee62169ac815f60d6d3 to your computer and use it in GitHub Desktop.
Swift Key Path Memory Layout Decoding
import Foundation
// https://github.com/apple/swift/blob/master/docs/ABI/KeyPaths.md
// https://bugs.swift.org/browse/SR-5689
public struct KeyPathMirror<T: AnyKeyPath> : CustomStringConvertible {
public let subject : T
public let isa : UnsafeRawPointer
public let kvcPath : String?
public let bufferHeader : UInt32
public var components : [ String ] {
// TODO
return [ "hello" ]
}
public init(reflecting subject: T) {
self.subject = subject
// https://github.com/apple/swift/blob/master/docs/ABI/KeyPaths.md
// https://stackoverflow.com/questions/31144748/
var ptr = UnsafeRawPointer(Unmanaged.passUnretained(subject).toOpaque())
// Key path objects begin with the standard Swift heap object header,
// https://academy.realm.io/posts/goto-mike-ash-exploring-swift-memory-layout/
isa = ptr.read()
let strongRC : Int32 = ptr.read()
let weakRC : Int32 = ptr.read()
print("Decode:")
print(" isa:", isa)
print(" isa:", unsafeBitCast(isa, to: AnyObject.self))
// _TtGCs24ReferenceWritableKeyPathC18TestKeyPathRuntime6PersonSS_
// _TtGC s24 ReferenceWritableKeyPath
// C18 TestKeyPathRuntime
// 6 PersonSS_
print(" RC: ", strongRC, weakRC)
// .. followed by a key path object header.
// Relative to the start of the heap object header:
// 0 Pointer to KVC compatibility C string, or null
// 1*sizeof(Int) Key path buffer header (32 bits)
// If the key path is Cocoa KVC-compatible, the first word will be a
// pointer to the equivalent KVC string as a null-terminated UTF-8 C string.
// It will be null otherwise. The key path buffer header in the second word
// contains the following bit fields:
kvcPath = ptr.read()
if let cstr = kvcPath { print(" cstr:", cstr) }
else { print(" NO CSTR") }
// declare a mirror
bufferHeader = ptr.read() // UInt32
print(" bufhdr:", bufferHeader)
// After the buffer header, one or more key path components appear in
// memory in sequence. Each component begins with a 32-bit key path
// component header.
// Components are always pointer-aligned, so the first component always
// starts at offset 2*sizeof(Int). On 64-bit platforms, this leaves four
// bytes of padding.
ptr.pointerAlign()
func readComponent() {
let header : UInt32 = ptr.read()
let endOfRefPrefix = (header | 0x80000000) == 0x80000000
let payload = Int(bufferHeader & 0x1FFFFFFF) // 0..28
let kind = Component.Kind(type: (header >> 29) & 0x3,
payload: payload)
print("comp:", header, "endOfRef:", endOfRefPrefix)
print(" kind", kind)
// After the header, the component contains the following word-aligned fields
/* offset-from-header
1*sizeof(Int) The identifier of the component.
2*sizeof(Int) The getter function for the component.
3*sizeof(Int) (if settable) The setter function for the component
*/
// a pointer-aligned pointer to the metadata for the type of the
// projected component is stored
}
readComponent()
}
public struct Component {
public enum Kind : CustomStringConvertible {
public var description : String {
var ms = "<Kind:"
switch self {
case .stored(let offset):
if let offset = offset { ms += " stored@\(offset)" }
else { ms += " stored[LARGE]" }
case .classStored(let offset):
if let offset = offset { ms += " class-stored@\(offset)" }
else { ms += " class-stored[LARGE]" }
case .computed:
ms += " computed"
case .optional:
ms += " optional"
}
ms += ">"
return ms
}
init(type: UInt32, payload: Int) {
switch type {
case 0x0:
if payload == 0x1FFF_FFFF { self = .stored(offset: nil) }
else { self = .stored(offset: payload) }
case 0x1:
// TODO
self = .computed
case 0x2:
if payload == 0x1FFF_FFFF { self = .classStored(offset: nil) }
else { self = .classStored(offset: payload) }
case 0x3:
// TODO
self = .optional
default: fatalError("unexpected type value: \(type)")
}
}
/**
* A struct stored property component, when given a value of the base
* type in memory, can project the component value in-place at a fixed
* offset within the base value.
* This applies for struct stored properties, tuple fields, and the .self
* identity component (which trivially projects at offset zero).
* The payload contains the offset in bytes of the projected field in the
* aggregate, or the special value 0x1FFF_FFFF, which indicates that the
* offset is too large to pack into the payload and is stored in the next
* 32 bits after the header.
*/
case stored(offset: Int?)
/**
* A computed component uses the conservative access pattern of
* get/set /materializeForSet to project from the base value.
* This is used as a general fallback component for any key path
* component without a more specialized representation, including not
* only computed properties but also subscripts, stored properties that
* require reabstraction, properties with behaviors or custom key path
* components (when we get those), and weak or unowned properties.
* The payload contains additional bitfields describing the component:
* 24 1 = Has captured arguments, 0 = no captures
* 25...26 Identifier kind
* 27 1 = Settable, 0 = Get-Only
* 28 1 = Mutating (implies settable), 0 = Nonmutating
*/
case computed
/**
* A class stored property component, when given a reference to a class
* instance, can project the component value inside the class instance at
* a fixed offset.
* The payload payload contains the offset in bytes of the projected
* field from the address point of the object, or the special value
* 0x1FFF_FFFF, which indicates that the offset is too large to pack into
* the payload and is stored in the next 32 bits after the header.
*/
case classStored(offset: Int?)
/**
* An optional component performs an operation involving Optional values.
* The payload contains one of the following values:
* 0 Optional chaining
* 1 Optional wrapping
* 2 Optional force-unwrapping
* A chaining component behaves like the postfix ? operator, immediately
* ending the key path application and returning nil when the base value
* is nil, or unwrapping the base value and continuing projection on the
* non-optional payload when non-nil. If an optional chain ends in a
* non-optional value, an implicit wrapping component is inserted to wrap
* it up in an optional value.
* A force-unwrapping operator behaves like the postfix ! operator,
* trapping if the base value is nil, or unwrapping the value inside the
* optional if not.
*/
case optional
}
}
// total size in bytes of the components following the key path buffer
// header
var componentSizeInBytes : Int {
return Int(bufferHeader & 0xFFFFFF)
}
// A ReferenceWritableKeyPath may have a reference prefix of read-only
// components that can be projected before initiating mutation.
var hasReferencePrefix : Bool {
return (bufferHeader | 0x40000000) == 0x40000000
}
// Key path does NOT capture values that require cleanup when the key
// path object is deallocated
var isTrivial : Bool {
return (bufferHeader | 0x80000000) == 0x80000000
}
public var description : String {
var ms = "<\(type(of: self)):"
ms += " csize=\(componentSizeInBytes)"
if hasReferencePrefix { ms += " ref-prefix" }
if isTrivial { ms += " trivial" }
if let kvc = kvcPath { ms += " kvc='\(kvc)'" }
ms += ">"
return ms
}
}
fileprivate extension UnsafeRawPointer {
mutating func wordAlign() {
// I guess this is the same on iOS/macOS platforms, right?
pointerAlign()
}
mutating func pointerAlign() {
let ptrValue = UInt(bitPattern: self)
let remainder = Int(ptrValue % UInt(MemoryLayout<UnsafeRawPointer>.size))
if remainder == 0 { return }
// TODO: is this right? (or size-remainder?) :->
self = self.advanced(by: remainder)
}
mutating func read<T>() -> T {
let v = assumingMemoryBound(to: T.self).pointee
self = self.advanced(by: MemoryLayout<T>.stride)
return v
}
mutating func read() -> String? {
let v = assumingMemoryBound(to: Optional<UnsafeRawPointer>.self).pointee
self = self.advanced(by: MemoryLayout<Optional<UnsafeRawPointer>>.stride)
guard let cstr = v else { return nil }
return String(utf8String: cstr.assumingMemoryBound(to: CChar.self))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment