Safe loading API for RawSpan (SE-0525)
(링크)
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0525-rawspan-safe-loading-api.md
swift-evolution/proposals/0525-rawspan-safe-loading-api.md at main · swiftlang/swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution
github.com
Swift 6.4에서 구현되었고 리뷰에서 일부 수정 후 수용되었으면 관련 제안은 SE-0447.
SE-0447에서 RawSpan을 도입하면서 임의 타입의 값을 읽는 함수도 같이 들어왔는데, 그 함수들은 unsafe로 표시되어 있음. 네이티브 정수 타입처럼 실제로는 안전하게 읽을 수 있는 경우가 있는데도 표시가 하나뿐이라 사용자가 어디까지 안전한지 판단할 근거가 없음. 이 제안은 바이트로 읽어도 되는 타입과 바이트에서 만들어도 되는 타입을 프로토콜 두 개로 규정하고, 그 위에 안전한 읽기와 쓰기 API를 얹음.
목차
- Summary of changes
- Motivation
- Proposed solution
- ConvertibleToBytes
- ConvertibleFromBytes
- FullyInhabited
- RawSpan과 MutableRawSpan의 load
- RawSpan 계열의 subscript
- MutableRawSpan과 OutputRawSpan의 쓰기
- 메모리에 있는 타입을 읽고 쓸 때의 범위
- Span과 MutableSpan
- Detailed design
- 표준 라이브러리의 적합성 목록
- 최상위 bitCast 함수
- Source compatibility와 ABI
- Future directions
- Alternatives considered
- 정리
Summary of changes
RawSpan, MutableSpan, MutableRawSpan, OutputRawSpan 인스턴스가 나타내는 메모리에서 특정 안전한 타입의 값을 읽고 쓰는 안전한 API 묶음을 도입함. 타입 없는 버퍼로 다른 프로세스에 데이터를 보내야 하는 상황에서 Swift의 쓸모가 올라가고, 파싱 유틸리티를 만들 때 쓸 재료가 생김.
Motivation
SE-0447의 로드 함수는 임의 타입을 대상으로 하기 때문에 unsafe가 붙어 있음. 정수 타입처럼 어떤 비트 패턴이 와도 유효한 값이 되는 타입은 사실 안전하게 읽을 수 있는데, 표준 라이브러리가 그 구분을 제공하지 않으니 사용자 쪽에 의심이 남음.
안전한 바이트 읽기를 정의하려면 안전한 바이트 쓰기가 무엇인지도 같이 정해야 함. 읽기만 규정하면 애초에 그 메모리가 어떻게 채워졌는지 보장할 수 없기 때문. 그래서 이 제안은 두 방향을 함께 다룸.
Proposed solution
초기화된 타입 값과 초기화된 원시 바이트 사이의 변환을 지원하는 프로토콜 두 개를 제안함. 항상 안전하게 바이트로 읽어낼 수 있는 타입이 ConvertibleToBytes, 항상 안전하게 바이트에서 해석할 수 있는 타입이 ConvertibleFromBytes.
ConvertibleToBytes
@_marker protocol ConvertibleToBytes: Copyable {}
ConvertibleToBytes를 따르는 타입의 값으로 메모리를 초기화하면 그 타입의 stride에 해당하는 모든 바이트가 초기화되어야 함. 즉 메모리 표현에 패딩이 없어야 하고, 저장 프로퍼티 크기의 합이 stride와 같아야 함.
Optional<Int16>은 stride 4바이트 중 3바이트만 사용하므로 적합성을 선언할 수 없음. 반면 struct Pair { var a, b: Int16 }은 size와 stride가 같으므로 가능함.
적합성 조건은 네 가지임.
- 저장 프로퍼티가 하나 이상 있음
- 모든 저장 프로퍼티의 타입이 ConvertibleToBytes를 따름
- 저장 프로퍼티가 패딩 없이 메모리에 연속으로 놓임
- 자기 바이트의 일부를 무시하는 값이 없음. 이 조건 때문에 대부분의 enum은 제외됨
표준 라이브러리의 기본 타입 다수가 이 프로토콜을 따르게 되지만, 표준 라이브러리 밖의 타입은 당장은 적합성을 선언할 수 없음. 적합성은 그 타입을 담고 있는 모듈에서만 선언할 수 있음.
ConvertibleFromBytes
@_marker protocol ConvertibleFromBytes: BitwiseCopyable {}
저장 프로퍼티의 모든 바이트에 대해 모든 비트 패턴이 유효한 값이면 ConvertibleFromBytes를 따를 수 있음. ConvertibleToBytes와 달리 내부 패딩이나 후행 패딩이 있어도 됨. 대신 저장 프로퍼티 값에 의미 제약이 없어야 하고, 모든 저장 프로퍼티가 그 자체로 ConvertibleFromBytes여야 함.
2차원 좌표를 나타내는 struct Point { var x, y: Int }는 적합함. 저장 프로퍼티가 Int이고, x와 y 사이에 의미 제약이 없어서 어떤 조합이든 유효한 Point가 됨.
겉보기 구성이 같은 Range<Int>는 적합하지 않음. lowerBound가 upperBound 이하여야 한다는 제약이 두 저장 프로퍼티 사이에 걸려 있기 때문. 같은 이유로 UnicodeScalar(유효하지 않은 비트 패턴이 있음), UTF8로 인코딩된 가상의 SmallString(구성 바이트의 순서가 유효성을 결정함), UnsafeRawPointer도 제외됨. 포인터의 경우가 특히 분명한데, 유효한 값의 집합을 런타임 환경이 결정하므로 실행 전에는 값의 의미적 유효성을 알 수 없음.
컴파일러는 이 의미 요구사항을 강제할 수 없음. 그래서 표준 라이브러리 밖의 타입은 검사되지 않는 적합성으로만 선언할 수 있음.
extension MyType: @unchecked ConvertibleFromBytes {}
Bool처럼 바이트를 온전히 사용하지 않는 타입도 금지됨. 유효하지 않은 비트 패턴을 그런 값으로 해석하면 정의되지 않은 동작이 나옴.
FullyInhabited
typealias FullyInhabited = ConvertibleToBytes & ConvertibleFromBytes
두 프로토콜의 교집합에 붙인 이름임.
RawSpan과 MutableRawSpan의 load
RawSpan과 MutableRawSpan에 제네릭 load(as:)가 추가됨. 반환 타입이 ConvertibleFromBytes로 제한되고 요청 범위가 경계 검사를 거치므로 이 함수는 안전함. 포인터 정렬 제약은 없음.
extension RawSpan {
func load<T: ConvertibleFromBytes>(
fromByteOffset: Int,
as: T.Type = T.self
) -> T
}
ConvertibleFromBytes와 FixedWidthInteger를 함께 따르는 타입에는 바이트 순서를 지정하는 버전이 따로 있음.
extension RawSpan {
func load<T: ConvertibleFromBytes & FixedWidthInteger>(
fromByteOffset: Int,
as: T.Type = T.self,
_ byteOrder: ByteOrder
) -> T
}
@frozen
enum ByteOrder: Equatable, Hashable, Sendable {
case bigEndian, littleEndian
static var native: Self { get }
}
이 오버로드를 쓸 수 있는 표준 라이브러리 타입은 UInt8, Int8, UInt16, Int16, UInt32, Int32, UInt64, Int64, UInt, Int, UInt128, Int128 열두 개임.
load(as:)는 원자적 연산이 아님. 경계 검사를 생략하는 버전도 제공하지 않음. 그 동작이 필요하면 이미 있는 unsafeLoad(fromUncheckedByteOffset:as:)를 씀.
RawSpan 계열의 subscript
UInt8 한 가지 경우를 위해 RawSpan, MutableRawSpan, OutputRawSpan에 subscript를 정의함. 기존 Span, MutableSpan, OutputSpan의 subscript와 같은 모양임.
extension RawSpan {
subscript(_ byteOffset: Int) -> UInt8 { get }
@unsafe
subscript(unchecked byteOffset: Int) -> UInt8 { get }
}
extension MutableRawSpan {
subscript(_ byteOffset: Int) -> UInt8 { get set }
@unsafe
subscript(unchecked byteOffset: Int) -> UInt8 { get set }
}
extension OutputRawSpan {
subscript(_ byteOffset: Int) -> UInt8 { get set }
@unsafe
subscript(unchecked byteOffset: Int) -> UInt8 { get set }
}
unchecked 레이블이 붙은 쪽은 오프셋을 검증하지 않으므로 @unsafe임.
MutableRawSpan과 OutputRawSpan의 쓰기
MutableRawSpan에 storeBytes() 오버로드가 추가됨.
extension MutableRawSpan {
mutating func storeBytes<T>(
of value: T,
toByteOffset offset: Int,
as type: T.Type,
_ byteOrder: ByteOrder
) where T: ConvertibleToBytes & BitwiseCopyable & FixedWidthInteger
@unsafe
mutating func storeBytes<T>(
repeating repeatedValue: T,
count: Int,
as type: T.Type
) where T: BitwiseCopyable
mutating func storeBytes<T>(
repeating repeatedValue: T,
count: Int,
as type: T.Type,
_ byteOrder: ByteOrder
) where T: ConvertibleToBytes & BitwiseCopyable & FixedWidthInteger
}
여기서 기존 API의 상태가 바뀜. T: BitwiseCopyable로만 제약된 기존 storeBytes는 @unsafe로 표시됨. 컴파일러 최적화의 결과로 초기화되지 않은 바이트가 남을 수 있기 때문. 빠져 있던 repeating 변형을 이번에 채우는데, 같은 제약을 쓰는 쪽은 마찬가지로 @unsafe임.
OutputRawSpan에는 대응하는 append()가 들어감.
extension OutputRawSpan {
mutating func append<T>(
_ value: T,
as type: T.Type
) where T: ConvertibleToBytes & BitwiseCopyable
mutating func append<T>(
_ value: T,
as type: T.Type,
_ byteOrder: ByteOrder
) where T: ConvertibleToBytes & BitwiseCopyable & FixedWidthInteger
mutating func append<T>(
repeating repeatedValue: T,
count: Int,
as type: T.Type
) where T: ConvertibleToBytes & BitwiseCopyable
mutating func append<T>(
repeating repeatedValue: T,
count: Int,
as type: T.Type,
_ byteOrder: ByteOrder
) where T: ConvertibleToBytes & BitwiseCopyable & FixedWidthInteger
}
T: BitwiseCopyable로만 제약된 기존 append도 @unsafe로 표시됨.
메모리에 있는 타입을 읽고 쓸 때의 범위
ConvertibleFromBytes나 ConvertibleToBytes를 따를 수 있는 타입이라고 해서 메모리 레이아웃이 컴파일러 버전과 라이브러리 버전 사이에서 안정적이라는 보장은 없음.
이 API가 상정한 용도, 즉 실행 중인 프로세스끼리 데이터를 주고받거나 같은 프로세스가 나중에 쓰려고 저장해 두는 경우에는 문제가 되지 않음. 네트워크 통신용 직렬화나 파일 시스템 저장처럼 더 복잡한 요구에는 이 API를 재료로만 봐야 함.
Span과 MutableSpan
Span에 init(viewing: RawSpan)이 추가됨. Element가 ConvertibleFromBytes를 따를 때 타입 없는 메모리 구간을 타입이 있는 Span으로 볼 수 있음.
extension Span {
@_lifetime(copy bytes)
init(viewing bytes: RawSpan) where Element: ConvertibleFromBytes
}
이 변환은 정렬과 경계를 검사함. RawSpan의 포인터 정렬이 Element에 맞지 않거나 경계가 stride의 배수가 아니면 트랩함.
MutableSpan에는 MutableRawSpan의 메모리를 타입이 있는 MutableSpan으로 변경할 수 있는 이니셜라이저가 들어감. 조건은 Element가 ConvertibleToBytes와 ConvertibleFromBytes를 모두 따르는 것이고, 검사와 트랩 규칙은 같음.
extension MutableSpan {
@_lifetime(&mutableBytes)
init(mutating mutableBytes: inout MutableRawSpan)
where Element: ConvertibleToBytes & ConvertibleFromBytes
@_lifetime(copy mutableBytes)
init(mutableBytes: consuming MutableRawSpan)
where Element: ConvertibleToBytes & ConvertibleFromBytes
}
RawSpan에서 Span으로 가는 변환은 정렬이 맞고 네이티브 바이트 순서인 경우만 지원함. 메모리를 제자리에서 재해석하는 수준을 넘어서는 용도라면 swift-binary-parsing 패키지의 ParserSpan이 더 맞음. RawSpan 인스턴스의 정렬을 판별하는 기능은 이후 제안에서 다룰 예정임.
기존 bytes와 mutableBytes 접근자에도 안전한 오버로드가 생김. bytes는 Element가 ConvertibleToBytes일 때, mutableBytes는 ConvertibleToBytes와 ConvertibleFromBytes를 모두 따를 때 적용됨.
Detailed design
앞 절에서 설명한 API의 전체 시그니처임. 주석은 원문 문서 주석을 옮긴 것.
extension RawSpan {
/// 지정한 오프셋의 원시 메모리로 값을 만들어 반환함.
///
/// `offset`에서 시작해 타입 `T`의 값을 만드는 데 필요한 바이트 범위가
/// span 안에 완전히 들어 있어야 함. `offset`이 `T`에 정렬될 필요는 없음.
/// `offset`은 음수가 아니어야 함.
func load<T: ConvertibleFromBytes>(
fromByteOffset offset: Int,
as type: T.Type = T.self
) -> T
/// 위와 같고, `byteOrder`로 디코딩할 바이트 순서를 지정함.
func load<T: ConvertibleFromBytes & FixedWidthInteger>(
fromByteOffset offset: Int,
as type: T.Type = T.self,
_ byteOrder: ByteOrder
) -> T
/// 지정한 오프셋의 바이트에 접근함.
/// `byteOffset`은 0 이상 `byteCount` 미만이어야 함.
subscript(_ byteOffset: Int) -> UInt8 { get }
/// `byteOffset`을 검증하지 않음. 유효하지 않은 값을 넘기면
/// 정의되지 않은 동작이 발생함.
@unsafe
subscript(unchecked byteOffset: Int) -> UInt8 { get }
/// 타입이 있는 span을 원시 span으로 봄.
@_lifetime(copy elements)
init<T: ConvertibleToBytes>(elements: consuming Span<T>)
/// 타입이 있는 span을 안전하지 않은 방식으로 원시 span으로 봄.
@unsafe
@_lifetime(copy unsafeElements)
init<T>(unsafeElements: consuming Span<T>)
}
MutableRawSpan은 위의 load와 subscript를 같은 형태로 갖고, 쓰기 쪽과 변환 이니셜라이저가 추가됨.
extension MutableRawSpan {
/// 값의 바이트를 지정한 오프셋에 저장함.
/// 필요한 바이트 범위가 span 안에 완전히 들어 있어야 하고,
/// `offset`이 `T`에 정렬될 필요는 없음.
mutating func storeBytes<T>(
of value: T,
toByteOffset offset: Int,
as type: T.Type,
_ byteOrder: ByteOrder
) where T: ConvertibleToBytes & BitwiseCopyable & FixedWidthInteger
/// 값의 바이트를 반복해서 저장함.
/// span에 최소 `count * MemoryLayout<T>.stride` 바이트가 있어야 함.
@unsafe
mutating func storeBytes<T>(
repeating repeatedValue: T,
count: Int,
as type: T.Type
) where T: BitwiseCopyable
mutating func storeBytes<T>(
repeating repeatedValue: T,
count: Int,
as type: T.Type,
_ byteOrder: ByteOrder
) where T: ConvertibleToBytes & BitwiseCopyable & FixedWidthInteger
/// 타입이 있는 span의 원소를 바이트 단위로 변경함.
@_lifetime(&mutableSpan)
init<T>(mutating mutableSpan: inout MutableSpan<T>)
where T: ConvertibleToBytes & ConvertibleFromBytes
/// 타입이 있는 span을 원시 span으로 변환함.
@_lifetime(copy elements)
init<T>(elements: consuming MutableSpan<T>)
where T: ConvertibleToBytes & ConvertibleFromBytes
@unsafe
@_lifetime(copy unsafeElements)
init<T>(unsafeElements: consuming MutableSpan<T>)
}
OutputRawSpan은 append 네 종류와 subscript, 그리고 유효한 오프셋 범위를 나타내는 프로퍼티를 가짐.
extension OutputRawSpan {
/// span에 최소 `MemoryLayout<T>.size` 바이트가 있어야 함.
mutating func append<T>(
_ value: T,
as type: T.Type
) where T: ConvertibleToBytes & BitwiseCopyable
mutating func append<T>(
_ value: T,
as type: T.Type,
_ byteOrder: ByteOrder
) where T: ConvertibleToBytes & BitwiseCopyable & FixedWidthInteger
/// span에 최소 `count * MemoryLayout<T>.stride` 바이트가 있어야 함.
mutating func append<T>(
repeating repeatedValue: T,
count: Int,
as type: T.Type
) where T: ConvertibleToBytes & BitwiseCopyable
mutating func append<T>(
repeating repeatedValue: T,
count: Int,
as type: T.Type,
_ byteOrder: ByteOrder
) where T: ConvertibleToBytes & BitwiseCopyable & FixedWidthInteger
subscript(_ byteOffset: Int) -> UInt8 { get set }
@unsafe
subscript(unchecked byteOffset: Int) -> UInt8 { get set }
/// subscript에 사용할 수 있는 오프셋을 오름차순으로 나타냄.
public var byteOffsets: Range<Int>
}
ByteOrder의 각 케이스가 가리키는 방향은 아래와 같음.
@frozen
enum ByteOrder: Equatable, Hashable, Sendable {
/// 최상위 비트가 가장 낮은 메모리 주소에서 시작하는 순서.
case bigEndian
/// 최하위 비트가 가장 낮은 메모리 주소에서 시작하는 순서.
case littleEndian
/// 런타임 타겟의 네이티브 바이트 순서.
static var native: Self { get }
}
ConvertibleToBytes 쪽 조건을 코드로 확인하면 패딩의 영향이 분명해짐. struct A { var v: [3 of Int8]; var n: Int64 }는 저장 프로퍼티 둘 다 ConvertibleToBytes지만 패딩이 5바이트 생겨서 적합하지 않음.
표준 라이브러리의 적합성 목록
기반 타입의 플랫폼 가용성에 따라 아래 적합성이 표준 라이브러리에 구현됨.
extension UInt8: ConvertibleToBytes, ConvertibleFromBytes {}
extension Int8: ConvertibleToBytes, ConvertibleFromBytes {}
extension UInt16: ConvertibleToBytes, ConvertibleFromBytes {}
extension Int16: ConvertibleToBytes, ConvertibleFromBytes {}
extension UInt32: ConvertibleToBytes, ConvertibleFromBytes {}
extension Int32: ConvertibleToBytes, ConvertibleFromBytes {}
extension UInt64: ConvertibleToBytes, ConvertibleFromBytes {}
extension Int64: ConvertibleToBytes, ConvertibleFromBytes {}
extension UInt: ConvertibleToBytes, ConvertibleFromBytes {}
extension Int: ConvertibleToBytes, ConvertibleFromBytes {}
extension UInt128: ConvertibleToBytes, ConvertibleFromBytes {}
extension Int128: ConvertibleToBytes, ConvertibleFromBytes {}
extension Float16: ConvertibleToBytes, ConvertibleFromBytes {}
extension Float32: ConvertibleToBytes, ConvertibleFromBytes {} // `Float`
extension Float64: ConvertibleToBytes, ConvertibleFromBytes {} // `Double`
extension Duration: ConvertibleToBytes, ConvertibleFromBytes {}
extension InlineArray: ConvertibleToBytes
where Element: ConvertibleToBytes {}
extension InlineArray: ConvertibleFromBytes
where Element: ConvertibleFromBytes {}
extension CollectionOfOne: ConvertibleToBytes
where Element: ConvertibleToBytes {}
extension CollectionOfOne: ConvertibleFromBytes
where Element: ConvertibleFromBytes {}
extension SIMD2: ConvertibleToBytes where Scalar: ConvertibleToBytes {}
extension SIMD4: ConvertibleToBytes where Scalar: ConvertibleToBytes {}
extension SIMD8: ConvertibleToBytes where Scalar: ConvertibleToBytes {}
extension SIMD16: ConvertibleToBytes where Scalar: ConvertibleToBytes {}
extension SIMD32: ConvertibleToBytes where Scalar: ConvertibleToBytes {}
extension SIMD64: ConvertibleToBytes where Scalar: ConvertibleToBytes {}
extension SIMD2: ConvertibleFromBytes where Scalar: ConvertibleFromBytes {}
extension SIMD3: ConvertibleFromBytes where Scalar: ConvertibleFromBytes {}
extension SIMD4: ConvertibleFromBytes where Scalar: ConvertibleFromBytes {}
extension SIMD8: ConvertibleFromBytes where Scalar: ConvertibleFromBytes {}
extension SIMD16: ConvertibleFromBytes where Scalar: ConvertibleFromBytes {}
extension SIMD32: ConvertibleFromBytes where Scalar: ConvertibleFromBytes {}
extension SIMD64: ConvertibleFromBytes where Scalar: ConvertibleFromBytes {}
extension ClosedRange: ConvertibleToBytes where Bound: ConvertibleToBytes {}
extension Range: ConvertibleToBytes where Bound: ConvertibleToBytes {}
extension PartialRangeFrom: ConvertibleToBytes
where Bound: ConvertibleToBytes {}
extension PartialRangeFrom.Iterator: ConvertibleToBytes
where Bound: ConvertibleToBytes {}
extension PartialRangeThrough: ConvertibleToBytes
where Bound: ConvertibleToBytes {}
extension PartialRangeUpTo: ConvertibleToBytes
where Bound: ConvertibleToBytes {}
extension Bool: ConvertibleToBytes {}
extension ObjectIdentifier: ConvertibleToBytes {}
extension UnsafePointer: ConvertibleToBytes {}
extension UnsafeMutablePointer: ConvertibleToBytes {}
extension UnsafeRawPointer: ConvertibleToBytes {}
extension UnsafeMutableRawPointer: ConvertibleToBytes {}
extension OpaquePointer: ConvertibleToBytes {}
extension UnsafeBufferPointer: ConvertibleToBytes {}
extension UnsafeMutableBufferPointer: ConvertibleToBytes {}
extension UnsafeRawBufferPointer: ConvertibleToBytes {}
extension UnsafeMutableRawBufferPointer: ConvertibleToBytes {}
목록에서 확인할 지점이 셋 있음. 위 타입 중 선행 조건인 BitwiseCopyable 적합성이 없는 것은 이번에 함께 얻게 됨. SIMD3는 SIMD4와 같은 스토리지를 쓰면서 원소 하나 크기의 후행 패딩이 있어 ConvertibleToBytes에서 빠짐. Bool, 포인터 계열, Range 계열은 ConvertibleToBytes에만 있고 반대 방향에는 없음.
최상위 bitCast 함수
프로토콜 두 개가 생기면서 타입을 재해석하는 안전한 함수를 정의할 수 있게 됨.
/// 주어진 인스턴스의 비트를 지정한 타입으로 해석해 반환함.
///
/// `T`와 `U`의 메모리 표현 크기가 같아야 하고, 다르면 트랩함.
func bitCast<T, U>(_ original: T, to type: U.Type) -> U
where T: ConvertibleToBytes, U: ConvertibleFromBytes
읽는 쪽과 쓰는 쪽의 제약이 다르다는 점이 시그니처에 그대로 드러남. 원본은 바이트로 온전히 내보낼 수 있어야 하고, 결과 타입은 어떤 비트 패턴이든 받을 수 있어야 함.
Source compatibility와 ABI
추가만 있는 제안이라 소스 호환성은 유지됨. 다만 오버로드 추가에는 위험이 있어서 기존의 유효한 코드에 영향을 줄 가능성이 있고, 심각한 호환성 문제가 없는지 테스트가 필요하다고 적혀 있음.
ABI는 추가로 만들지 않는 방향으로 구현됨. 이 함수들은 Span을 필요로 하므로, 표준 라이브러리가 운영체제와 함께 배포되는 Darwin 계열에서는 최소 배포 타겟이 생김. ByteOrder는 새 타입이라 가용성이 붙고, 이 인자를 쓰는 함수도 같은 가용성을 따름. 채택하려면 새 버전의 표준 라이브러리가 필요함.
Future directions
- ConvertibleToBytes 검증. 이 프로토콜은 주소 지정 가능한 메모리에서의 레이아웃에만 의존하므로 컴파일 시점에 완전히 검증할 수 있음. BitwiseCopyable처럼 적합성을 자동화하는 방향이 가능함. 검증과 함께, 선택한 타입에 대해 패딩 대신 0 바이트를 저장하는 방안도 고려할 수 있음
- ConvertibleFromBytes 부분 검증. 저장 프로퍼티가 모두 이 프로토콜을 따르는지는 컴파일러가 강제할 수 있음. 의미 제약이 없다는 것은 직접 강제할 수 없어서, 저장 프로퍼티가 모두 public이고 var인 경우처럼 우회 조건을 받아들이는 방안이 거론됨
- C에서 임포트한 타입 지원. Clang 임포터에 기본 C 타입의 적합성을 가르칠 수 있음. 집합체인 C 타입에는 적합성을 선언할 방법이 필요한데, 임포트한 C 타입에 한해 모듈 제한을 완화하는 방안이 있음
- 튜플 지원. ConvertibleToBytes 타입으로 구성된 튜플은 그 자체로 ConvertibleToBytes여야 함. 반대 방향도 같음
- RawSpan의 정렬을 조사하는 유틸리티. Span 이니셜라이저가 정렬된 RawSpan을 요구하므로, 특정 타입에 정렬이 맞는 오프셋을 찾아주는 기능이 필요함
- 안전하지 않음이 이름에 드러나지 않는 @unsafe 심볼의 개명. 이전 제안에서 들어온 뒤 나중에 @unsafe로 표시된 것들이 있음. 개명은 별도 제안에서 다루는 편이 혼란이 적음
- OutputRawSpan으로 OutputSpan에 덧붙이는 기능. 원래 제안에 있었으나 용량 인자의 레이블 결정이 어려워 빠짐. 같은 모양의 API가 UniqueArray에도 필요하고 insert와 replace에도 해당되므로, SE-0527에서 이름을 정한 뒤 다시 제안할 예정임
Alternatives considered
- 로드하는 타입 이름을 함수 이름에 넣기. loadInt32(fromByteOffset:_:) 같은 구체 함수를 나열하면 오버로드 문제를 피해 타입 검사기 부담이 줄어듦
- 컴파일러가 검증하는 ConvertibleToBytes 레이아웃 제약을 기다리기. 이 기능은 시급하고 표준 라이브러리 추가만으로 달성 가능한 반면, 레이아웃 제약 검증에는 상당한 컴파일러 작업이 필요함
- FixedWidthInteger & BitwiseCopyable, BinaryFloatingPoint & BitwiseCopyable로 제네릭화하기. 세 프로토콜 중 어느 것도 적합 타입이 fully inhabited임을 요구하지 않으므로 제약이 충분하지 않음
- FullyInhabited 하나만 추가하기. 2차 피치의 안이었는데, 논의 결과 결국 두 프로토콜이 필요해지고 구현 부담도 비슷하다는 결론이 나옴
- ByteOrder 인자를 빼기. FixedWidthInteger의 .bigEndian, .littleEndian 프로퍼티로 인자나 반환값을 바꾸는 방법이 있지만, 두 프로퍼티가 Self를 반환해서 바이트 순서와 값이 뒤섞임. 이 제안은 바이트 순서를 그것이 속한 연산인 직렬화 쪽에 붙임
- 안전한 load()를 정렬된 연산으로 기본 설정하기. UnsafeRawPointer의 원래 load()가 정렬을 요구했고 덜 제한적인 loadUnaligned()가 나중에 추가됐는데, 이 순서를 아쉬운 것으로 보고 새 load()는 정렬되지 않은 연산으로 감
- toByteOffset과 fromByteOffset에 기본값 0을 주기. 원래 제안에 있었으나, 기본값이 있으면 RawSpan 전체 길이를 써야 하는 것처럼 보인다는 지적이 나와 빠짐. 비슷한 기존 unsafe API에는 이미 기본값이 있어서 나중에 맞출 필요가 있음
정리
- 안전한 바이트 읽기와 쓰기를 타입 조건으로 나눔. 바이트로 내보내려면 패딩이 없어야 하고(ConvertibleToBytes), 바이트에서 만들려면 모든 비트 패턴이 유효해야 함(ConvertibleFromBytes). 조건이 다르므로 프로토콜도 둘임
- 두 조건을 다 만족하는 타입에 FullyInhabited라는 이름을 둠
- load(as:)는 경계를 검사하고 정렬을 요구하지 않아서 안전함. 원자적이지 않고, 경계 검사를 생략하는 버전도 없음
- 정수 타입에는 ByteOrder 인자를 받는 오버로드가 따로 있음. 바이트 순서를 값의 프로퍼티가 아니라 읽고 쓰는 연산의 인자로 둠
- 기존 storeBytes와 append 중 BitwiseCopyable로만 제약된 것은 @unsafe로 바뀜. 최적화 결과로 초기화되지 않은 바이트가 남을 수 있기 때문
- Span(viewing:)과 MutableSpan의 새 이니셜라이저는 정렬과 경계를 검사하고, 맞지 않으면 트랩함
- 타입 레이아웃이 버전 간에 안정적이지 않으므로 네트워크 직렬화나 파일 저장에는 이 API를 재료로만 씀
(참고)
- SE-0525, Safe loading API for RawSpan. https://github.com/swiftlang/swift-evolution/blob/main/proposals/0525-rawspan-safe-loading-api.md
- SE-0447, Span: Safe Access to Contiguous Storage. https://github.com/swiftlang/swift-evolution/blob/main/proposals/0447-span-access-shared-contiguous-storage.md
- SE-0527, UniqueArray와 RigidArray. https://github.com/swiftlang/swift-evolution/blob/main/proposals/0527-rigidarray-uniquearray.md
- 구현 PR. https://github.com/swiftlang/swift/pull/88640 , https://github.com/swiftlang/swift/pull/88702
- 리뷰 스레드. https://forums.swift.org/t/se-0525-safe-loading-api-for-rawspan/85811
- swift-binary-parsing. https://github.com/apple/swift-binary-parsing
- MemoryLayout.stride. https://developer.apple.com/documentation/swift/memorylayout/stride
'Apple > iOS, UIKit, Documentation' 카테고리의 다른 글
| kernel-object와 mach-port 정리 (0) | 2026.02.12 |
|---|---|
| swift rethrows (0) | 2025.12.03 |
| AssetInventory 정리 + AssetInstallationRequest (0) | 2025.11.22 |
| SpeechAnalyzer (0) | 2025.11.22 |
| SpeechTranscriber 정리 + DictationTranscriber (0) | 2025.11.22 |