Created
January 28, 2018 17:16
-
-
Save helje5/e5e6e50af60d0ee62169ac815f60d6d3 to your computer and use it in GitHub Desktop.
Swift Key Path Memory Layout Decoding
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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