diff --git a/Sources/SwiftCrossUI/State/DynamicKeyPath.swift b/Sources/SwiftCrossUI/State/DynamicKeyPath.swift
deleted file mode 100644
index ea3a247c526..00000000000
--- a/Sources/SwiftCrossUI/State/DynamicKeyPath.swift
+++ /dev/null
@@ -1,90 +0,0 @@
-#if canImport(Darwin)
- import func Darwin.memcmp
-#elseif canImport(Glibc)
- import func Glibc.memcmp
-#elseif canImport(WinSDK)
- import func WinSDK.memcmp
-#elseif canImport(Android)
- import func Android.memcmp
-#endif
-
-/// A type similar to KeyPath, but that can be constructed at run time given
-/// an instance of a struct, and the value of the desired property. Construction
-/// fails if the property's in-memory representation is not unique within the
-/// struct. SwiftCrossUI only uses ``DynamicKeyPath`` in situations where it is
-/// highly likely for properties to have unique in-memory representations, such
-/// as when properties have internal storage pointers.
-struct DynamicKeyPath {
- /// The property's offset within instances of ``T``.
- var offset: Int
-
- /// Constructs a key path given an instance of the base type, and the
- /// value of the desired property. The initializer will search through
- /// the base instance's in-memory representation to find the unique offset
- /// that matches the representation of the given property value. If such an
- /// offset can't be found or isn't unique, then the initialiser returns `nil`.
- init?(
- forProperty value: Value,
- of base: Base,
- label: String? = nil
- ) {
- let propertyAlignment = MemoryLayout.alignment
- let propertySize = MemoryLayout.size
- let baseStructSize = MemoryLayout.size
-
- var index = 0
- var matches: [Int] = []
- while index + propertySize <= baseStructSize {
- let isMatch =
- withUnsafeBytes(of: base) { viewPointer in
- withUnsafeBytes(of: value) { valuePointer in
- memcmp(
- viewPointer.baseAddress!.advanced(by: index),
- valuePointer.baseAddress!,
- propertySize
- )
- }
- } == 0
- if isMatch {
- matches.append(index)
- }
- index += propertyAlignment
- }
-
- guard let offset = matches.first else {
- logger.warning(
- "no offset found for dynamic property",
- metadata: ["property": "\(label ?? "")"]
- )
- return nil
- }
-
- guard matches.count == 1 else {
- logger.warning(
- "multiple offsets found for dynamic property",
- metadata: ["property": "\(label ?? "")"]
- )
- return nil
- }
-
- self.offset = offset
- }
-
- /// Gets the property's value on the given instance.
- func get(_ base: Base) -> Value {
- withUnsafeBytes(of: base) { buffer in
- buffer.baseAddress!.advanced(by: offset)
- .assumingMemoryBound(to: Value.self)
- .pointee
- }
- }
-
- /// Sets the property's value to a new value on the given instance.
- func set(_ base: inout Base, _ newValue: Value) {
- withUnsafeMutableBytes(of: &base) { buffer in
- buffer.baseAddress!.advanced(by: offset)
- .assumingMemoryBound(to: Value.self)
- .pointee = newValue
- }
- }
-}
diff --git a/Sources/SwiftCrossUI/State/DynamicPropertyUpdater.swift b/Sources/SwiftCrossUI/State/DynamicPropertyUpdater.swift
index bab949376d4..f0281855f9f 100644
--- a/Sources/SwiftCrossUI/State/DynamicPropertyUpdater.swift
+++ b/Sources/SwiftCrossUI/State/DynamicPropertyUpdater.swift
@@ -1,211 +1,62 @@
-/// A cache for dynamic property updaters. The keys are the ObjectIdentifiers of
-/// various Base types that we have already computed dynamic property updaters
+/// A cache for dynamic property updaters. The keys are the `ObjectIdentifier`s of
+/// various `Base` types that we have already computed dynamic property updaters
/// for, and the elements are corresponding cached instances of
-/// DynamicPropertyUpdater.
+/// `DynamicPropertyUpdater`.
///
/// From some basic testing, this caching seems to reduce layout times by 5-10%
/// (at the time of implementation).
@MainActor
-var updaterCache: [ObjectIdentifier: Any] = [:]
+private var updaterCache: [ObjectIdentifier: Any] = [:]
/// A helper for updating the dynamic properties of a stateful struct (e.g.
/// a View or App conforming struct). Dynamic properties are those that conform
/// to ``DynamicProperty``, e.g. properties annotated with `@State`.
///
-/// At initialisation the updater will attempt to determine the byte offset of
-/// each stateful property in the struct. This is guaranteed to succeed if every
-/// dynamic property in the provided struct instance contains internal mutable
-/// storage, because the storage pointers will provide unique byte sequences.
-/// Otherwise, offset discovery will fail when two dynamic properties share the
-/// same pattern in memory. When offset discovery fails the updater will fall
-/// back to using Mirrors each time `update` gets called, which can be 1500x
-/// times slower when the view has 0 state properties, and 9x slower when the
-/// view has 4 properties, with the factor slowly dropping as the number of
-/// properties increases.
+/// At initialisation the updater will determine the byte offset of each
+/// stateful property in the struct.
struct DynamicPropertyUpdater {
- typealias PropertyUpdater = (
- _ old: Base?,
- _ new: Base,
- _ environment: EnvironmentValues
- ) -> Void
-
- /// The updaters for each of Base's dynamic properties. If `nil`, then we
- /// failed to compute
- let propertyUpdaters: [PropertyUpdater]?
+ /// The offsets and types of each of `Base`'s dynamic properties.
+ private var propertyOffsets: [(offset: Int, type: any DynamicProperty.Type)]
/// Creates a new dynamic property updater which can efficiently update
- /// all dynamic properties on any value of type Base without creating
- /// any mirrors after the initial creation of the updater. Pass in a
- /// `mirror` of base if you already have one to save us creating another one.
+ /// all dynamic properties on any value of type `Base` without creating
+ /// any mirrors.
@MainActor
- init(for base: Base, mirror: Mirror? = nil) {
+ init(for _: Base.Type) {
+ self.propertyOffsets = []
+
// Unlikely shortcut, but worthwhile when we can.
if MemoryLayout.size == 0 {
- self.propertyUpdaters = []
return
}
- if let cachedUpdater = updaterCache[ObjectIdentifier(Base.self)],
- let cachedUpdater = cachedUpdater as? Self
- {
- self = cachedUpdater
+ if let cachedUpdater = updaterCache[ObjectIdentifier(Base.self)] {
+ self = cachedUpdater as! Self
return
}
- var propertyUpdaters: [PropertyUpdater] = []
-
- let mirror = mirror ?? Mirror(reflecting: base)
- for child in mirror.children {
- let label = child.label ?? ""
- let value = child.value
-
- guard let value = value as? any DynamicProperty else {
- continue
+ forEachField(of: Base.self) { _, offset, type in
+ if let type = type as? any DynamicProperty.Type {
+ propertyOffsets.append((offset, type))
}
-
- guard let updater = Self.getUpdater(for: value, base: base, label: label) else {
- // We have failed to create the required property updaters. Fallback
- // to using Mirrors to update all properties.
- logger.warning(
- """
- failed to produce DynamicPropertyUpdater; falling back to \
- slower Mirror-based property updating approach
- """,
- metadata: ["type": "\(Base.self)"]
- )
- self.propertyUpdaters = nil
-
- // We intentionally return without caching the updaters here so
- // that we if this failure is a fluke we can recover on a
- // subsequent attempt for the same type. It may turn out that in
- // practice types that fail are ones that always fail, in which
- // case we should update this code to add the current updater to
- // the cache.
- return
- }
-
- propertyUpdaters.append(updater)
}
- self.propertyUpdaters = propertyUpdaters
-
updaterCache[ObjectIdentifier(Base.self)] = self
}
/// Updates each dynamic property of the given value.
func update(_ value: Base, with environment: EnvironmentValues, previousValue: Base?) {
- guard let propertyUpdaters else {
- // Fall back to our old dynamic property updating approach which involves a lot of
- // Mirror overhead. This should be rare.
- Self.updateFallback(of: value, previousValue: previousValue, environment: environment)
- return
- }
-
- for updater in propertyUpdaters {
- updater(previousValue, value, environment)
- }
- }
-
- /// Gets an updater for the property of base with the given value. If multiple
- /// properties exist matching the byte pattern of `value`, then `nil` is returned.
- ///
- /// The returned updater is reusable and doesn't use Mirror.
- private static func getUpdater(
- for value: T,
- base: Base,
- label: String
- ) -> PropertyUpdater? {
- guard let keyPath = DynamicKeyPath(forProperty: value, of: base, label: label) else {
- return nil
- }
-
- let updater = { (old: Base?, new: Base, environment: EnvironmentValues) in
- let property = keyPath.get(new)
- property.update(
- with: environment,
- previousValue: old.map(keyPath.get)
- )
- }
-
- return updater
- }
-
- /// Updates the dynamic properties of a value given a previous instance of the
- /// type (if one exists) and the current environment.
- private static func updateFallback(
- of value: T,
- previousValue: T?,
- environment: EnvironmentValues
- ) {
- let newMirror = Mirror(reflecting: value)
- let previousMirror = previousValue.map(Mirror.init(reflecting:))
- if let previousChildren = previousMirror?.children {
- let propertySequence = zip(newMirror.children, previousChildren)
- for (newProperty, previousProperty) in propertySequence {
- guard
- let newValue = newProperty.value as? any DynamicProperty,
- let previousValue = previousProperty.value as? any DynamicProperty
- else {
- continue
- }
-
- updateDynamicPropertyFallback(
- newProperty: newValue,
- previousProperty: previousValue,
- environment: environment,
- enclosingTypeName: "\(T.self)",
- propertyName: newProperty.label
- )
- }
- } else {
- for property in newMirror.children {
- guard let newValue = property.value as? any DynamicProperty else {
- continue
- }
-
- updateDynamicPropertyFallback(
- newProperty: newValue,
- previousProperty: nil,
- environment: environment,
- enclosingTypeName: "\(T.self)",
- propertyName: property.label
- )
- }
- }
- }
-
- /// Updates a dynamic property. Required to unmask the concrete type of the
- /// property. Since the two properties can technically be two different
- /// types, Swift correctly wouldn't allow us to assume they're both the
- /// same. So we unwrap one and then dynamically check whether the other
- /// matches using a type cast.
- private static func updateDynamicPropertyFallback(
- newProperty: Property,
- previousProperty: (any DynamicProperty)?,
- environment: EnvironmentValues,
- enclosingTypeName: String,
- propertyName: String?
- ) {
- let castedPreviousProperty: Property?
- if let previousProperty {
- guard let previousProperty = previousProperty as? Property else {
- fatalError(
- """
- Supposedly unreachable... previous and current types of \
- \(enclosingTypeName).\(propertyName ?? "") \
- don't match.
- """
+ for (offset, type) in propertyOffsets {
+ update(type)
+
+ func update(_: Property.Type) {
+ getProperty(Property.self, of: value, at: offset).update(
+ with: environment,
+ previousValue: previousValue.map {
+ getProperty(Property.self, of: $0, at: offset)
+ }
)
}
-
- castedPreviousProperty = previousProperty
- } else {
- castedPreviousProperty = nil
}
-
- newProperty.update(
- with: environment,
- previousValue: castedPreviousProperty
- )
}
}
diff --git a/Sources/SwiftCrossUI/State/ForEachField.swift b/Sources/SwiftCrossUI/State/ForEachField.swift
new file mode 100644
index 00000000000..756b2855233
--- /dev/null
+++ b/Sources/SwiftCrossUI/State/ForEachField.swift
@@ -0,0 +1,74 @@
+// Adapted from https://github.com/swiftlang/swift/blob/swift-6.2.3-RELEASE/stdlib/public/core/ReflectionMirror.swift
+
+// NB: I have absolutely zero clue why this is importable here. Xcode just shows
+// me an empty file when I command-click it. Nevertheless, it works just fine and
+// SourceKit seems to have no trouble finding stuff from here, so I'll roll
+// with it for now.
+private import SwiftShims
+
+@_silgen_name("swift_reflectionMirror_recursiveCount")
+private func getRecursiveChildCount(of: Any.Type) -> Int
+
+@_silgen_name("swift_reflectionMirror_recursiveChildMetadata")
+private func getChildMetadata(
+ of: Any.Type,
+ index: Int,
+ fieldMetadata: UnsafeMutablePointer<_FieldReflectionMetadata>
+) -> Any.Type
+
+@_silgen_name("swift_reflectionMirror_recursiveChildOffset")
+private func getChildOffset(of: Any.Type, index: Int) -> Int
+
+/// Calls the given closure on every field of the specified type.
+///
+/// The standard library exposes a function named `_forEachField(of:options:body:)`,
+/// which calls directly into the runtime's reflection facilities in order to
+/// get the names and types of the provided value's stored properties, bypassing
+/// most of the `Mirror` overhead. However, it's annotated with `@_spi(Reflection)`,
+/// and most people's toolchain installations don't include the stdlib's SPI
+/// interfaces. So we have to reimplement this function ourselves.
+///
+/// There are three runtime functions used to implement this:
+/// - `swift_reflectionMirror_recursiveCount(_:)`
+/// - `swift_reflectionMirror_recursiveChildMetadata(_:index:fieldMetadata:)`
+/// - `swift_reflectionMirror_recursiveChildOffset(_:index:)`
+///
+/// All three of these have been present in the runtime for at least 6 years (probably
+/// longer), and since `_forEachField(of:options:body:)` is used [within Combine] (and
+/// presumably other Apple frameworks), it's fairly unlikely that either it or the
+/// functions it depends on (which are the same as shown above) will be removed any
+/// time soon.
+///
+/// - SeeAlso: The [original implementation] as of Swift 6.2.3.
+///
+/// [within Combine]: https://forums.swift.org/t/how-is-the-published-property-wrapper-implemented/58223/11
+/// [original implementation]: https://github.com/swiftlang/swift/blob/swift-6.2.3-RELEASE/stdlib/public/core/ReflectionMirror.swift
+///
+/// - Parameters:
+/// - type: The type to inspect.
+/// - body: A closure to call with information about each field in `type`.
+/// The parameters to `body` are the name of the field, the offset of the
+/// field, and the type of the field.
+func forEachField(
+ of type: Any.Type,
+ body: (_ name: String?, _ offset: Int, _ type: Any.Type) -> Void
+) {
+ let childCount = getRecursiveChildCount(of: type)
+ for index in 0..(_: Property.Type, of base: Base, at offset: Int) -> Property {
+ withUnsafeBytes(of: base) { buffer in
+ buffer.baseAddress!.advanced(by: offset)
+ .assumingMemoryBound(to: Property.self)
+ .pointee
+ }
+}
diff --git a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift
index b84caa1a568..ac7c1aa10ac 100644
--- a/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift
+++ b/Sources/SwiftCrossUI/ViewGraph/ViewGraphNode.swift
@@ -83,8 +83,7 @@ public class ViewGraphNode: Sendable {
parentEnvironment = environment
cancellables = []
- let mirror = Mirror(reflecting: view)
- dynamicPropertyUpdater = DynamicPropertyUpdater(for: view, mirror: mirror)
+ dynamicPropertyUpdater = DynamicPropertyUpdater(for: NodeView.self)
let viewEnvironment = updateEnvironment(environment)
@@ -108,6 +107,7 @@ public class ViewGraphNode: Sendable {
backend.tag(widget: widget, as: tag)
// Update the view and its children when state changes (children are always updated first).
+ let mirror = Mirror(reflecting: view)
for property in mirror.children {
if property.label == "state" && property.value is ObservableObject {
logger.warning(
@@ -123,13 +123,10 @@ public class ViewGraphNode: Sendable {
continue
}
- cancellables.append(
- value.didChange
- .observeAsUIUpdater(backend: backend) { [weak self] in
- guard let self else { return }
- self.bottomUpUpdate()
- }
- )
+ let cancellable = value.didChange.observeAsUIUpdater(backend: backend) { [weak self] in
+ self?.bottomUpUpdate()
+ }
+ cancellables.append(cancellable)
}
}
diff --git a/Sources/SwiftCrossUI/_App.swift b/Sources/SwiftCrossUI/_App.swift
index f5afa9ab4c4..5f047d2ec12 100644
--- a/Sources/SwiftCrossUI/_App.swift
+++ b/Sources/SwiftCrossUI/_App.swift
@@ -28,7 +28,7 @@ class _App {
)
self.cancellables = []
- dynamicPropertyUpdater = DynamicPropertyUpdater(for: app)
+ dynamicPropertyUpdater = DynamicPropertyUpdater(for: AppRoot.self)
}
func refreshSceneGraph() {