From edb00bd980448f956a07b3d513f27fca32a1bb08 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Tue, 9 Jul 2024 01:47:41 -0700 Subject: [PATCH] Sketch a `Loadable` protocol `Loadable` enables safe loading of values from `RawSpan` and other byte streams. It depends on `AutoLoadable`, which represent `Loadable` types that have no invalid bit pattern. --- Sources/Future/Loadable.swift | 129 ++++++++++++++++++++++++++ Tests/FutureTests/LoadableTests.swift | 127 +++++++++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 Sources/Future/Loadable.swift create mode 100644 Tests/FutureTests/LoadableTests.swift diff --git a/Sources/Future/Loadable.swift b/Sources/Future/Loadable.swift new file mode 100644 index 000000000..041668ef3 --- /dev/null +++ b/Sources/Future/Loadable.swift @@ -0,0 +1,129 @@ +//===--- Loadable.swift ---------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// Indicate that a type can be safely loaded from raw bytes +/// +/// A valid type `T` implementing `Loadable` can be stored into +/// a `UnsafeMutableRawBufferPointer` using the `storeBytes(of:as:)` function, +/// then read out of the same memory by loading the `RawBytes`, +/// then initializing `T` using `T.init(rawBytes:)`. +/// +/// let t1: T = ... +/// assert(t is any Loadable) +/// let c = MemoryLayout.size +/// let b = UnsafeMutableRawBufferPointer.allocate(byteCount: c, alignment: 16) +/// b.storeBytes(of: t1, as: T.self) +/// let raw = b.load(as: T.RawBytes.self) +/// let t2 = T(rawBytes: raw) +/// // t1 and t2 are identical +/// +/// A `Loadable` type is not expected to be usable across executions of a +/// program. Use `Codable` to communicate across program executions. +public protocol Loadable: BitwiseCopyable { + + /// A type that can represent every valid bit pattern our `Loadable` can have. + /// + /// Requirements: + /// - MemoryLayout.size == MemoryLayout.size + /// + /// We'd like this to be either + /// 1. a fixed-size array of an `AutoLoadable` element type, or + /// 2. a parameter pack where every element type is `AutoLoadable`. + associatedtype RawBytes: AutoLoadable + + /// Create an instance of `T` with the bit pattern represented by `rawBytes`. + /// + /// If a valid instance of `T` cannot be represented, then return nil. + init?(rawBytes: RawBytes) +} + +/// A `Loadable` type for which there is no ambiguous bit pattern. +/// +/// In practice, this means a type which has a power-of-two count of states. +public protocol SurjectivelyLoadable: Loadable { + init(rawBytes: RawBytes) +} + +//FIXME: `AutoLoadable` types must be @frozen. +/// Indicate that a type represents every bit pattern. +/// +/// Every bit vector of `size` bytes must be a valid instance. +public protocol AutoLoadable: SurjectivelyLoadable where RawBytes == Self {} + +extension AutoLoadable { + public init(rawBytes: Self) { + self = rawBytes + } +} + +extension Int: AutoLoadable {} +extension UInt: AutoLoadable {} + +extension Int8: AutoLoadable {} +extension UInt8: AutoLoadable {} +extension Int16: AutoLoadable {} +extension UInt16: AutoLoadable {} +extension Int32: AutoLoadable {} +extension UInt32: AutoLoadable {} +extension Int64: AutoLoadable {} +extension UInt64: AutoLoadable {} + +extension Float32: AutoLoadable {} +extension Float64: AutoLoadable {} + +//FIXME: add [U]Int128, Float16, SIMD types + +extension RawSpan { + public func load( + fromByteOffset offset: Int = 0, as: T.Type + ) -> T? { + let raw = unsafeLoad(fromByteOffset: offset, as: T.RawBytes.self) + return T(rawBytes: raw) + } + + public func loadUnaligned( + fromByteOffset offset: Int = 0, as: T.Type + ) -> T? { + let raw = unsafeLoadUnaligned(fromByteOffset: offset, as: T.RawBytes.self) + return T(rawBytes: raw) + } +} + +extension Bool: SurjectivelyLoadable { + public typealias RawBytes = Int8 + + public init(rawBytes: Int8) { + self = (rawBytes & 1) == 1 + } +} + +extension RawSpan { + public func load( + fromByteOffset offset: Int = 0, as: T.Type + ) -> T { + let raw = unsafeLoad(fromByteOffset: offset, as: T.RawBytes.self) + return T(rawBytes: raw) + } + + public func loadUnaligned( + fromByteOffset offset: Int = 0, as: T.Type + ) -> T { + let raw = unsafeLoadUnaligned(fromByteOffset: offset, as: T.RawBytes.self) + return T(rawBytes: raw) + } +} + +extension RawSpan { + public func view(as: T.Type) -> Span { + unsafeView(as: T.self) + } +} diff --git a/Tests/FutureTests/LoadableTests.swift b/Tests/FutureTests/LoadableTests.swift new file mode 100644 index 000000000..15b32afe3 --- /dev/null +++ b/Tests/FutureTests/LoadableTests.swift @@ -0,0 +1,127 @@ +//===--- LoadableTests.swift ----------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import XCTest +import Future + +final class LoadableTests: XCTestCase { + + func testLoadInt() { + let b = UnsafeMutableRawBufferPointer.allocate(byteCount: 80, alignment: 8) + defer { b.deallocate() } + let span = RawSpan(unsafeBytes: .init(b), owner: b) + + let i0 = Int.random(in: 0..<999999) + b.storeBytes(of: i0, as: Int.self) + + let i1 = span.load(as: Int.self) + XCTAssertEqual(i1, i0) + + let o0 = Int.random(in: 1..<(80-MemoryLayout.size)) + b.storeBytes(of: i0, toByteOffset: o0, as: Int.self) + + let i2 = span.loadUnaligned(fromByteOffset: o0, as: Int.self) + XCTAssertEqual(i2, i0) + } + + func testViewBytes() { + let b = UnsafeMutableBufferPointer.allocate(capacity: 1) + b.initializeElement(at: 0, to: UInt(0x03020100).littleEndian) + let span = Span(unsafeElements: .init(b), owner: b).rawSpan + let view = span.view(as: UInt8.self) + for i in 0..<4 { + XCTAssertEqual(i, Int(view[i])) + } + } + + func testLoadBool() { + let b = UnsafeMutableRawBufferPointer.allocate(byteCount: 1, alignment: 1) + defer { b.deallocate() } + let span = RawSpan(unsafeBytes: .init(b), owner: b) + + b.storeBytes(of: true, as: Bool.self) + XCTAssertEqual(span.load(as: Bool.self), true) + + b.storeBytes(of: false, as: Bool.self) + XCTAssertEqual(span.load(as: Bool.self), false) + + for i in UInt8.min ... UInt8.max { + b.storeBytes(of: i, as: UInt8.self) + let r = span.load(as: Bool.self) + XCTAssertNotEqual(r, i.isMultiple(of: 2)) + } + } +} + +enum Test: CaseIterable, Equatable, Hashable { + case a, b, c, d +} + +extension Test: Loadable { + typealias RawBytes = UInt8 + + init?(rawBytes: RawBytes) { + for t in Test.allCases { + if unsafeBitCast(t, to: RawBytes.self) == rawBytes { + self = t + return + } + } + return nil + } +} + +extension LoadableTests { + + func testLoadLoadable() { + let b = UnsafeMutableRawBufferPointer.allocate(byteCount: 8, alignment: 1) + defer { b.deallocate() } + let span = RawSpan(unsafeBytes: .init(b), owner: b) + + let i0 = Test.allCases.randomElement()! + b.storeBytes(of: i0, as: Test.self) + + let i1 = span.load(as: Test.self) + XCTAssertEqual(i1, i0) + + let o0 = Int.random(in: 1..<8) + b.storeBytes(of: i0, toByteOffset: o0, as: Test.self) + + let i2 = span.loadUnaligned(fromByteOffset: o0, as: Test.self) + XCTAssertEqual(i2, i0) + } + + func testLoadTest() { + XCTAssertEqual( + MemoryLayout.size, + MemoryLayout.size + ) + + var valid: Set = [] + var wrong: Set = [] + for i in UInt8.zero ... UInt8.max { + let test = withUnsafePointer(to: i) { + let span = RawSpan(unsafeStart: $0, byteCount: 1, owner: $0) + return span.load(as: Test.self) + } + + if let _ = test { + valid.insert(i) + } else { + wrong.insert(i) + } + } + + XCTAssertEqual(valid.count, 4) + XCTAssertEqual(valid.count + wrong.count, 256) + } +}