diff --git a/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicPropertyBuffer.swift b/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicPropertyBuffer.swift index c6f1379cb..1b94495be 100644 --- a/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicPropertyBuffer.swift +++ b/Sources/OpenSwiftUICore/Data/DynamicProperty/DynamicPropertyBuffer.swift @@ -4,7 +4,8 @@ // // Audited for iOS 15.5 // Status: Complete -// ID: 68550FF604D39F05971FE35A26EE75B0 +// ID: 68550FF604D39F05971FE35A26EE75B0 (SwiftUI) +// ID: F3A89CF4357225EF49A7DD673FDFEE02 (SwiftUICore) import OpenGraphShims diff --git a/Sources/OpenSwiftUICore/Data/DynamicProperty/UnsafeHeterogeneousBuffer.swift b/Sources/OpenSwiftUICore/Data/DynamicProperty/UnsafeHeterogeneousBuffer.swift new file mode 100644 index 000000000..cc43c6fd8 --- /dev/null +++ b/Sources/OpenSwiftUICore/Data/DynamicProperty/UnsafeHeterogeneousBuffer.swift @@ -0,0 +1,218 @@ +// +// UnsafeHeterogeneousBuffer.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete +// ID: 568350FE259575B5E1AAA52AD722AAAC (SwiftUICore) + +package struct UnsafeHeterogeneousBuffer: Collection { + var buf: UnsafeMutableRawPointer! + var available: Int32 + var _count: Int32 + + package typealias VTable = _UnsafeHeterogeneousBuffer_VTable + package typealias Element = _UnsafeHeterogeneousBuffer_Element + + package struct Index: Equatable, Comparable { + var index: Int32 + var offset: Int32 + + package static func < (lhs: Index, rhs: Index) -> Bool { + lhs.index < rhs.index + } + + package static func == (a: Index, b: Index) -> Bool { + a.index == b.index && a.offset == b.offset + } + } + + package struct Item { + let vtable: _UnsafeHeterogeneousBuffer_VTable.Type + let size: Int32 + var flags: UInt32 + } + + package var count: Int { Int(_count) } + package var isEmpty: Bool { _count == 0 } + + package var startIndex: Index { + Index(index: 0, offset: 0) + } + package var endIndex: Index { + Index(index: _count, offset: 0) + } + + package init() { + buf = nil + available = 0 + _count = 0 + } + + private mutating func allocate(_ bytes: Int) -> UnsafeMutableRawPointer { + var count = _count + var offset = 0 + var size = 0 + while count != 0 { + let itemSize = buf + .advanced(by: offset) + .assumingMemoryBound(to: Item.self) + .pointee + .size + offset &+= Int(itemSize) + count &-= 1 + offset = count == 0 ? 0 : offset + size &+= Int(itemSize) + } + // Grow buffer if needed + if Int(available) < bytes { + growBuffer(by: bytes, capacity: size + Int(available)) + } + let ptr = buf.advanced(by: size) + available = available - Int32(bytes) + return ptr + } + + private mutating func growBuffer(by size: Int, capacity: Int) { + let expectedSize = size + capacity + var allocSize = Swift.max(capacity &* 2, 64) + while allocSize < expectedSize { + allocSize &*= 2 + } + let allocatedBuffer = UnsafeMutableRawPointer.allocate( + byteCount: allocSize, + alignment: .zero + ) + if let buf { + var count = _count + if count != 0 { + var itemSize: Int32 = 0 + var oldBuffer = buf + var newBuffer = allocatedBuffer + repeat { + count &-= 1 + let newItemPointer = newBuffer.assumingMemoryBound(to: Item.self) + let oldItemPointer = oldBuffer.assumingMemoryBound(to: Item.self) + + if count == 0 { + itemSize = 0 + } else { + itemSize &+= oldItemPointer.pointee.size + } + newItemPointer.initialize(to: oldItemPointer.pointee) + oldItemPointer.pointee.vtable.moveInitialize( + elt: .init(item: newItemPointer), + from: .init(item: oldItemPointer) + ) + let size = Int(oldItemPointer.pointee.size) + oldBuffer += size + newBuffer += size + } while count != 0 || itemSize != 0 + + } + buf.deallocate() + } + buf = allocatedBuffer + available += Int32(allocSize - capacity) + } + + package func destroy() { + defer { buf?.deallocate() } + guard _count != 0 else { + return + } + var count = _count + var offset = 0 + while count != 0 { + let itemPointer = buf + .advanced(by: offset) + .assumingMemoryBound(to: Item.self) + itemPointer.pointee.vtable.deinitialize(elt: .init(item: itemPointer)) + offset &+= Int(itemPointer.pointee.size) + count &-= 1 + } + } + + package func formIndex(after index: inout Index) { + index = self.index(after: index) + } + + package func index(after index: Index) -> Index { + let item = self[index].item.pointee + let newIndex = index.index &+ 1 + if newIndex == _count { + return Index(index: newIndex, offset: 0) + } else { + let newOffset = index.offset &+ item.size + return Index(index: newIndex, offset: newOffset) + } + } + + package subscript(index: Index) -> Element { + .init(item: buf + .advanced(by: Int(index.offset)) + .assumingMemoryBound(to: Item.self) + ) + } + + @discardableResult + package mutating func append(_ value: T, vtable: VTable.Type) -> Index { + let bytes = MemoryLayout.size + MemoryLayout.size + let pointer = allocate(bytes) + let element = _UnsafeHeterogeneousBuffer_Element(item: pointer.assumingMemoryBound(to: Item.self)) + element.item.initialize(to: Item(vtable: vtable, size: Int32(bytes), flags: 0)) + element.body(as: T.self).initialize(to: value) + let index = Index(index: _count, offset: Int32(pointer - buf)) + _count += 1 + return index + } +} + +@_spi(ForOpenSwiftUIOnly) +public struct _UnsafeHeterogeneousBuffer_Element { + var item: UnsafeMutablePointer + + package func hasType(_ type: T.Type) -> Bool { + item.pointee.vtable.hasType(type) + } + + package func vtable(as type: T.Type) -> T.Type where T: _UnsafeHeterogeneousBuffer_VTable { + address.assumingMemoryBound(to: Swift.type(of: type)).pointee + } + + package func body(as type: T.Type) -> UnsafeMutablePointer { + UnsafeMutableRawPointer(item.advanced(by: 1)).assumingMemoryBound(to: type) + } + + package var flags: UInt32 { + get { item.pointee.flags } + nonmutating set { item.pointee.flags = newValue } + } + + package var address: UnsafeRawPointer { + UnsafeRawPointer(item) + } +} + +@_spi(ForOpenSwiftUIOnly) +@available(*, unavailable) +extension _UnsafeHeterogeneousBuffer_Element: Sendable {} + +@_spi(ForOpenSwiftUIOnly) +open class _UnsafeHeterogeneousBuffer_VTable { + open class func hasType(_ type: T.Type) -> Bool { + false + } + + open class func moveInitialize(elt: _UnsafeHeterogeneousBuffer_Element, from: _UnsafeHeterogeneousBuffer_Element) { + preconditionFailure("") + } + + open class func deinitialize(elt: _UnsafeHeterogeneousBuffer_Element) { + preconditionFailure("") + } +} + +@_spi(ForOpenSwiftUIOnly) +@available(*, unavailable) +extension _UnsafeHeterogeneousBuffer_VTable: Sendable {} diff --git a/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/UnsafeHeterogeneousBufferTests.swift b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/UnsafeHeterogeneousBufferTests.swift new file mode 100644 index 000000000..0faea46c0 --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Data/DynamicProperty/UnsafeHeterogeneousBufferTests.swift @@ -0,0 +1,94 @@ +// +// UnsafeHeterogeneousBufferTests.swift +// OpenSwiftUICoreTests + +@_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore +@testable import OpenSwiftUICore +import Testing + +struct UnsafeHeterogeneousBufferTests { + private final class VTable: _UnsafeHeterogeneousBuffer_VTable { + override class func hasType(_ type: T.Type) -> Bool { + Value.self == T.self + } + + override class func moveInitialize(elt: _UnsafeHeterogeneousBuffer_Element, from: _UnsafeHeterogeneousBuffer_Element) { + let dest = elt.body(as: Value.self) + let source = from.body(as: Value.self) + dest.initialize(to: source.move()) + } + + override class func deinitialize(elt: _UnsafeHeterogeneousBuffer_Element) { + elt.body(as: Value.self).deinitialize(count: 1) + } + } + + @Test + func structBuffer() { + var buffer = UnsafeHeterogeneousBuffer() + defer { buffer.destroy() } + #expect(buffer.isEmpty == true) + + do { + let index = buffer.append(UInt32(1), vtable: VTable.self) + #expect(buffer.isEmpty == false) + #expect(index == buffer.index(atOffset: 0)) + #expect(index.index == 0) + #expect(index.offset == 0) + #expect(buffer.available == 44) + #expect(buffer.count == 1) + let element = buffer[index] + #expect(element.body(as: UInt32.self).pointee == 1) + } + + do { + let index = buffer.append(Int(-1), vtable: VTable.self) + #expect(buffer.isEmpty == false) + #expect(index == buffer.index(atOffset: 1)) + #expect(index.index == 1) + #expect(index.offset == 16 + 4) + #expect(buffer.available == 20) + #expect(buffer.count == 2) + let element = buffer[index] + #expect(element.body(as: Int.self).pointee == -1) + } + + do { + let index = buffer.append(Double.infinity, vtable: VTable.self) + #expect(buffer.isEmpty == false) + #expect(index == buffer.index(atOffset: 2)) + #expect(index.index == 2) + #expect(index.offset == 16 + 4 + 16 + 8) + #expect(buffer.available == 60) + #expect(buffer.count == 3) + let element = buffer[index] + #expect(element.body(as: Double.self).pointee == Double.infinity) + } + } + + @Test + func classBuffer() async throws { + final class DeinitBox { + let deinitBlock: () -> Void + + init(deinitBlock: @escaping () -> Void) { + self.deinitBlock = deinitBlock + } + + deinit { + deinitBlock() + } + } + + + await confirmation { confirm in + var buffer = UnsafeHeterogeneousBuffer() + defer { buffer.destroy() } + #expect(buffer.isEmpty == true) + let box = DeinitBox { confirm() } + let index = buffer.append(box, vtable: VTable.self) + let element = buffer[index] + #expect(element.body(as: DeinitBox.self).pointee === box) + } + } +}