Context
SwiftCrossUI (based off SwiftUI) needs to discover dynamic properties, such as those annotated with @State, at run time. We do this a lot so it needs to be fast. I originally constructed a Mirror each time that we needed to locate the dynamic properties of a new View-conforming struct instance.
This was taking up 50% of the time spent processing view updates, so I looked into alternative approaches and came up with a hacky technique to compute the offsets of such dynamic properties once they have been located using a Mirror. My approach relies on the in-memory representation of each dynamic property being almost guaranteed to be unique due to containing internal storage pointers, and compared the property value returned by Mirror to the base value at various different offsets until the correct offset was discovered. This significantly improved performance, doesnât rely on platform specific runtime/ABI details, and gracefully falls back to constructing a new Mirror on every update in the case that we donât find exactly 1 candidate offset for the property.
A contributor, Kaleb, recently discovered that the standard library has a _forEachField(of:options:body:) function that does exactly what we need. This method is public, but is guarded behind @_spi(Reflection). @_spi(Reflection) import Swift results in
â@_spi import of âSwiftâ will not include any SPI symbols; Swift was built from the public interface at âŚâ, so it appears that we canât use the method directly. Kaleb has reimplemented the methodâs functionality by importing the relevant runtime methods via @_silgen_name and all seems to be working well.
Question(s)
Is it safe to use swift_reflectionMirror_* runtime functions imported via @_silgen_name from a runtime evolution standpoint? Is it possible that these functions would get pulled out from under us in a future Swift version?
I realise that the runtime is only ABI stable on macOS, but on non-Apple platforms we only need declaration stability. Are runtime functions allowed to be deleted or have their signatures modified on platforms without Swift ABI stability? Itâs of course possible for these runtime methods to be changed on non-ABI stable platforms from a technical standpoint; Iâm asking from a runtime development policy standpoint.
Avoiding the XY problem
What weâre really trying to do is get the offsets of a struct instanceâs properties at runtime, so any forward-compatible platform-agnostic solution to that problem would resolve our issue.
If you know a way to access the stdlibâs public @_spi(Reflection) _forEachField(of:options:body:) method safely, then we wouldnât have to use these runtime methods directly in the first place, and weâd be happy.
That said, Iâd still like to know the answer to the original question about runtime methods for future reference.
Code
@_silgen_name("swift_reflectionMirror_recursiveCount")
private func _getRecursiveChildCount(_: Any.Type) -> Int
@_silgen_name("swift_reflectionMirror_recursiveChildOffset")
private func _getChildOffset(_: Any.Type, index: Int) -> Int
private typealias NameFreeFunc = @convention(c) (UnsafePointer<CChar>?) -> Void
@_silgen_name("swift_reflectionMirror_subscript")
private func _getChild<T>(
of: T,
type: Any.Type,
index: Int,
outName: UnsafeMutablePointer<UnsafePointer<CChar>?>,
outFreeFunc: UnsafeMutablePointer<NameFreeFunc?>
) -> Any
/// Calls the given closure on every field of the specified value.
///
/// - Parameters:
/// - value: The value to inspect.
/// - body: A closure to call with information about each field in `value`.
/// The parameters to `body` are the name of the field, the offset of the
/// field, and the value of the field.
func _forEachField<Value>(of value: Value, body: (String?, Int, Any) -> Void) {
let childCount = _getRecursiveChildCount(Value.self)
for index in 0..<childCount {
let offset = _getChildOffset(Value.self, index: index)
var nameC: UnsafePointer<CChar>? = nil
var freeFunc: NameFreeFunc? = nil
defer { freeFunc?(nameC) }
let childValue = _getChild(
of: value,
type: Value.self,
index: index,
outName: &nameC,
outFreeFunc: &freeFunc
)
let childName = nameC.flatMap(String.init(validatingCString:))
body(childName, offset, childValue)
}
}