History
Swift's reflection capabilities were fundamentally redesigned during the ABI stability initiative in Swift 5.0. Prior to this, reflection relied on unstable compiler internals that changed with every toolchain release. The Mirror API was introduced to provide a stable, public interface for runtime type inspection, enabling debugging tools and generic logging without compile-time type knowledge. This required a metadata format capable of surviving library evolution where struct layouts might change between versions.
Problem
When a struct is marked as resilient (the default for public types in library evolution mode), the compiler cannot hardcode fixed memory offsets for its stored properties. Hardcoding would break binary compatibility if the library author adds, removes, or reorders fields in a future release. Additionally, the reflection system must expose enough metadata to reconstruct the type's field names and types at runtime, while respecting the resilient boundary that hides implementation details from direct access.
Solution
The Swift compiler emits field descriptors into the __swift5_fieldmd section of the binary's metadata. These descriptors do not contain static offsets; instead, they store relative offset accessors or instantiation-time layout calculations that resolve the actual memory location at runtime. For resilient types, the metadata includes a field offset vector that is populated when the type is instantiated in the current process. This indirection allows the Mirror API to traverse properties using computed offsets that adapt to the specific version of the library loaded at runtime, preserving both ABI stability and reflection capabilities.
import Foundation struct ResilientConfig { let timeout: Double private let apiKey: String // Accessible to Mirror despite 'private' } let config = ResilientConfig(timeout: 30.0, apiKey: "secret") let mirror = Mirror(reflecting: config) for child in mirror.children { print("Property: \(child.label ?? "unnamed"), Value: \(child.value)") }
A modular iOS application architecture separates the Networking module (closed-source SDK) from the Analytics module (in-house). The Networking module returns complex configuration structs containing private authentication tokens that should not be exposed through public getters, yet the Analytics team requires logging of all configuration parameters for debugging intermittent timeouts.
Solution 1: Public Dictionary Conversion
The Networking team could expose a method toDictionary() that manually maps fields to strings.
Pros: Compile-time type safety, explicit control over exposed data, fast performance.
Cons: Requires maintenance every time the struct changes; cannot reflect new fields added in SDK updates without recompiling the client; exposes sensitive fields if the developer forgets to filter them.
Solution 2: Objective-C Runtime Introspection
Leveraging valueForKey: via NSObject bridging.
Pros: Familiar to developers with Objective-C backgrounds.
Cons: Swift structs are not NSObject subclasses; forcing @objc conformance changes value semantics to reference semantics and increases binary size significantly; does not work with native Swift types.
Solution 3: Swift Reflection via Mirror
Implementing a generic logger using Mirror(reflecting:) to iterate over all stored properties regardless of access control.
Pros: Automatically adapts to new properties in SDK updates without recompilation; respects resilience boundaries; works with value types and generic code.
Cons: Mirror allocates heap memory for its internal storage, making it unsuitable for high-frequency logging; bypasses access control, potentially exposing private secrets if not filtered via CustomReflectable; cannot reflect C bitfields or computed properties.
Chosen Solution
The team adopted Solution 3 with a wrapper that checks for CustomReflectable conformance to allow the Networking SDK to provide a sanitized view. The Networking module implemented customMirror to exclude the apiKey while exposing the timeout and other safe fields.
Result
The Analytics module successfully logged configuration states across three major SDK updates without breaking changes. However, when the Networking team added a C struct wrapper for low-level socket options containing bitfields, those specific fields appeared as empty in logs. This required documentation to explain the Mirror limitation, while the rest of the configuration continued to reflect automatically.
How does Mirror prevent infinite recursion when reflecting self-referential data structures, and what responsibility falls on the developer when implementing CustomReflectable?
Mirror detects reference cycles by tracking the identity of class instances during the reflection walk. When encountering a class instance, it checks if that object is already present in the current recursion stack; if so, it stops traversal to prevent stack overflow. For value types, recursion only occurs if they contain references that form cycles. However, when a developer implements CustomReflectable and manually constructs a Mirror with children, the runtime cannot detect cycles in that custom construction. The developer must ensure that the children sequence does not create infinite loops, for example, by checking a recursion depth limit or maintaining their own visited-set when building custom reflection for graph-like structures.
Why does reflecting on a struct via Mirror sometimes report different memory layouts compared to the actual compiled layout, particularly with C structs containing bitfields or unions?
Swift's reflection metadata is designed for Swift types and uses Clang importer metadata for C interoperability. C bitfields and unions do not map to distinct Swift stored properties with stable addresses; they are represented as opaque storage or inline padding within the Clang importer's type translation. The Mirror API requires addressable fields to construct its children collection. Consequently, bitfields are invisible to reflection because they lack field descriptors in the __swift5_fieldmd section, and union members may appear as overlapping or incorrectly typed because the metadata describes the union container rather than individual cases. This is a fundamental limitation: Mirror reflects the Swift view of the type, not the underlying C layout.
What is the performance cost of property access through Mirror compared to direct access, and why is the cost asymmetrical between reading the property count versus reading the property values?
Accessing properties through Mirror is orders of magnitude slower than direct access because it involves runtime metadata lookups, heap allocation for the Mirror instance, and indirect calls through field accessor functions stored in the type metadata. Reading the children count requires parsing the field descriptor metadata to determine the number of stored properties, which is a relatively fast scan of the __swift5_fieldmd section. However, accessing the actual values requires calling value witnesses or specialized accessor functions for each field, which may involve copying data, managing reference counts for ARC types, and crossing resilience boundaries. For classes, this cost includes Objective-C runtime checks. Therefore, iterating over mirror.children to extract values has higher overhead than simply checking mirror.children.count, making Mirror unsuitable for hot paths despite its utility for debugging.