-
[SE-0525] Safe loading API for RawSpanSwift 2026. 7. 26. 09:25
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 SE-0525 — RawSpan을 위한 안전한 로딩 API에 대해 정리해보겠습니다 🙋🏻
Intro
Proposal: SE-0525
Author: Guillaume Lessard
Review Manager: Xiaodi Wu
Status: Implemented (Swift 6.4)
Related Proposals: SE-0447
Motivation
SE-0447에서
RawSpan을 도입하면서 임의 타입의 값을 로드할 수 있는 unsafe 함수들도 함께 들어왔어요 🙌네이티브 정수 타입처럼 그중 일부는 로드해도 실제로 안전한데도,
unsafe라는 표시 때문에 사용하는 입장에서는 늘 의심이 생기기 마련입니다 🥲이번 제안은 byte-loading 연산 중 안전한 것들을 명확히 구분해주는 게 목표예요.
그리고 안전한 byte-loading을 정의하려면 안전한 byte-storing도 함께 정리해야 해서, 이 제안은 그 부분도 함께 다룹니다.
Proposed Solution
두 개의 새 프로토콜을 제안합니다.
초기화된 타입 값과 초기화된 raw 바이트 사이의 변환을 지원하는 프로토콜이에요.
하나는 항상 안전하게 raw 바이트로 읽을 수 있는 타입이 채택하는
ConvertibleToBytes, 다른 하나는 raw 바이트에서 항상 안전하게 해석될 수 있는 타입이 채택하는ConvertibleFromBytes입니다.ConvertibleToBytes
어떤 타입의 값으로 메모리를 초기화할 때, 그 타입의 stride를 이루는 모든 바이트가 초기화되어야 한다면
ConvertibleToBytes를 채택할 수 있어요.@_marker protocol ConvertibleToBytes: Copyable {}메모리 표현에 패딩이 전혀 없는 타입, 즉 저장 프로퍼티 크기의 합이 stride와 같은 타입이 채택할 수 있습니다.
예를 들어
Optional<Int16>은 stride 4바이트 중 3바이트만 쓰기 때문에 채택할 수 없지만,struct Pair { var a, b: Int16 }는 크기와 stride가 같아서 채택 가능해요.ConvertibleToBytes를 채택하려면 다음 조건을 만족해야 합니다.
- 하나 이상의 저장 프로퍼티를 가질 것
- 모든 저장 프로퍼티가
ConvertibleToBytes를 채택한 타입일 것 - 저장 프로퍼티들이 패딩 없이 메모리에 연속적으로 저장될 것
- 어떤 값도 자신의 바이트 일부를 무시하지 않을 것 (대부분의 enum이 여기서 제외됩니다)
표준 라이브러리의 많은 기본 타입은 이 프로토콜을 채택하지만, 표준 라이브러리 바깥의 타입은 처음에는 채택할 수 없어요.
ConvertibleToBytes채택 선언은 그 타입을 담고 있는 모듈에서만 할 수 있습니다.ConvertibleFromBytes
@_marker protocol ConvertibleFromBytes: BitwiseCopyable {}저장 프로퍼티를 이루는 모든 바이트의 모든 비트 패턴이 유효한 타입이라면
ConvertibleFromBytes를 채택할 수 있어요.내부나 trailing 패딩이 있는 타입도 허용되지만, 저장 프로퍼티 값에 의미론적인 제약이 있으면 안 됩니다.
예를 들어
struct Point { var x, y: Int }는 x, y 사이에 아무 제약이 없어서 채택 가능하지만,Range<Int>는lowerBound <= upperBound라는 의미론적 제약이 있어서 채택할 수 없어요.UnicodeScalar(일부 비트 패턴이 무효), 가상의 UTF8SmallString(바이트 순서가 유효성에 영향),UnsafeRawPointer(런타임 전까지 값의 유효성을 알 수 없음) 등도 채택할 수 없는 예시예요.컴파일러가
ConvertibleFromBytes의 의미론적 요구사항을 강제할 수 없기 때문에, 표준 라이브러리 바깥의 타입은 unchecked conformance로만 채택할 수 있습니다.extension MyType: @unchecked ConvertibleFromBytes {}FullyInhabited
typealias FullyInhabited = ConvertibleToBytes & ConvertibleFromBytesConvertibleToBytes와ConvertibleFromBytes의 교집합이에요.RawSpan과 MutableRawSpan
RawSpan과MutableRawSpan에 제네릭한load(as:)함수가 새로 생겨요.포인터 정렬 제약 없이
ConvertibleFromBytes값을 읽어올 수 있고, bounds-checked라서 이 함수는 안전합니다.extension RawSpan { func load( fromByteOffset: Int, as: T.Type = T.self ) -> T }추가로,
ConvertibleFromBytes와FixedWidthInteger를 동시에 채택한 타입을 위해 byte order를 지정할 수 있는 버전도 있어요.extension RawSpan { func load( fromByteOffset: Int, as: T.Type = T.self, _ byteOrder: ByteOrder ) -> T } @frozen enum ByteOrder: Equatable, Hashable, Sendable { case bigEndian, littleEndian static var native: Self { get } }ConvertibleFromBytes & FixedWidthInteger를 채택하는 표준 라이브러리 타입 목록은UInt8,Int8,UInt16,Int16,UInt32,Int32,UInt64,Int64,UInt,Int,UInt128,Int128이에요.이
load(as:)함수들은 atomic 연산이 아니고, unchecked byte offset 버전은 제공되지 않아요 (필요하면 기존unsafeLoad(fromUncheckedByteOffset:as:)를 쓰면 됩니다).RawSpan 계열의 subscript
UInt8을 위한 편의 기능으로, 기존Span,MutableSpan,OutputSpan과 비슷한 subscript를RawSpan,MutableRawSpan,OutputRawSpan에도 정의합니다.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 } }MutableRawSpan과 OutputRawSpan
MutableRawSpan에는storeBytes()의 새 오버로드가 추가돼요.extension MutableRawSpan { mutating func storeBytes( of value: T, toByteOffset offset: Int, as type: T.Type, _ byteOrder: ByteOrder ) where T: ConvertibleToBytes & BitwiseCopyable & FixedWidthInteger @unsafe mutating func storeBytes( repeating repeatedValue: T, count: Int, as type: T.Type ) where T: BitwiseCopyable mutating func storeBytes( repeating repeatedValue: T, count: Int, as type: T.Type, _ byteOrder: ByteOrder ) where T: ConvertibleToBytes & BitwiseCopyable & FixedWidthInteger }기존
T: BitwiseCopyable로 제약된storeBytes함수는@unsafe로 표시됩니다.컴파일러 최적화로 인해 초기화되지 않은 바이트가 생길 수 있기 때문이에요.
반복 저장 variant도 이번에 추가되는데, 이것도
@unsafe로 표시됩니다.OutputRawSpan에는 대응하는 append() 함수가 생겨요.
extension OutputRawSpan { mutating func append( _ value: T, as type: T.Type ) where T: ConvertibleToBytes & BitwiseCopyable mutating func append( _ value: T, as type: T.Type, _ byteOrder: ByteOrder ) where T: ConvertibleToBytes & BitwiseCopyable & FixedWidthInteger mutating func append( repeating repeatedValue: T, count: Int, as type: T.Type ) where T: ConvertibleToBytes & BitwiseCopyable mutating func append( repeating repeatedValue: T, count: Int, as type: T.Type, _ byteOrder: ByteOrder ) where T: ConvertibleToBytes & BitwiseCopyable & FixedWidthInteger }기존
T: BitwiseCopyable만으로 제약된append함수도@unsafe로 표시됩니다.여기서 제안하는 타입들의 메모리 레이아웃은 컴파일러/라이브러리 버전에 걸쳐 안정적이라고 보장되진 않아요.
실행 중인 프로세스끼리 데이터를 주고받거나 같은 프로세스에서 나중에 쓰려고 저장하는 용도라면 문제없지만, 네트워크 통신이나 파일 시스템 저장을 위한 직렬화처럼 더 정교한 용도에는 이 API가 building block 정도로만 쓰일 수 있어요.
Span과 MutableSpan
Span에는init(viewing: RawSpan)이니셜라이저가 새로 생겨서,Span.Element가ConvertibleFromBytes를 채택했다면 정렬되지 않은 raw 메모리 범위를 typedSpan으로 볼 수 있어요. 정렬과 bounds를 검사하는데,RawSpan의 포인터 정렬이Element에 맞지 않거나 bounds가 stride의 배수가 아니면 트랩이 발생합니다.extension Span { @_lifetime(copy bytes) init(viewing bytes: RawSpan) where Element: ConvertibleFromBytes }MutableSpan에도MutableRawSpan의 메모리를 typedMutableSpan처럼 mutate할 수 있는 이니셜라이저가 추가돼요.마찬가지로 정렬과 bounds를 검사해서 맞지 않으면 트랩합니다.
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으로의 변환은 정렬이 잘 맞고 네이티브 byte order인 경우만 지원해요.더 복잡한 파싱이 필요하다면 swift-binary-parsing 패키지의
ParserSpan타입을 참고하면 됩니다.RawSpan의 메모리 정렬을 확인하는 기능은 추후 별도 제안으로 다뤄질 예정이에요.기존
bytes,mutableBytesaccessor도Element가 각각ConvertibleToBytes,ConvertibleToBytes & ConvertibleFromBytes를 채택했을 때 안전한 오버로드를 갖게 됩니다.Detailed Design
표준 라이브러리 채택 목록
여러 정수/부동소수점 타입,
Duration,InlineArray,CollectionOfOne, 여러SIMD타입, 범위 타입 일부,Bool,ObjectIdentifier, 각종 포인터 타입 등에 다음과 같이 채택이 추가됩니다.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와 저장 공간이 같지만 한 원소만큼 tail padding이 있어서ConvertibleToBytes를 채택하지 못해요.안전한 최상위 bitCast 함수
두 프로토콜 덕분에 타입을 안전하게 재해석하는 함수도 정의할 수 있게 됐어요.
func bitCast<T, U>(_ original: T, to type: U.Type) -> U where T: ConvertibleToBytes, U: ConvertibleFromBytes
Source Compatibility
이번 제안은 오직 추가로만 이루어져 있어서 소스 호환성을 유지합니다.
다만 오버로드를 추가하는 건 언제나 리스크가 있어서, 기존의 유효한 코드에 영향을 줄 가능성도 있어요.
중대한 호환성 이슈가 없는지 확인하려면 테스트가 필요합니다.
ABI Compatibility
이번에 추가되는 함수들은 추가 ABI를 만들지 않는 방식으로 구현될 예정이에요.
이 함수들은
Span의 존재를 전제로 하기 때문에, Darwin 플랫폼에서는 Swift 표준 라이브러리가 OS와 함께 배포되는 만큼 최소 배포 타깃이 있습니다.ByteOrder는 새 타입이라 자체 availability를 가지며,ByteOrder인자를 쓰는 함수들도 같은 availability를 공유합니다.
Implications on Adoption
이번 제안의 추가 사항들은 새 버전의 Swift 표준 라이브러리를 필요로 합니다.
Future Directions
ConvertibleToBytes 프로토콜에 대한 검증
ConvertibleToBytes채택은 나중에 컴파일러가 추가로 검증하게 될 거예요.이 프로토콜은 addressable 메모리 상의 레이아웃에만 의존하기 때문에 컴파일 타임에 완전히 검증할 수 있고,
BitwiseCopyable처럼 자동화도 가능할 것으로 보입니다.검증과 함께, 패딩 대신 저장된 null 바이트를 자동으로 넣어주는 방식도 고려할 수 있어요.
ConvertibleFromBytes 프로토콜에 대한 부분적 검증
ConvertibleFromBytes채택도 나중에 어느 정도 검증될 수 있어요. 컴파일러는 모든 저장 프로퍼티가
ConvertibleFromBytes를 채택했는지는 강제할 수 있지만, 의미론적 제약의 부재까지 직접 강제할 순 없습니다.다만 모든 저장 프로퍼티가
public이고var인 경우처럼 우회적인 방법을 받아들일 수도 있어요.C에서 import된 타입 지원
Clang importer가 어떤 기본 C 타입이 이 프로토콜들을 지원하는지 알도록 만들 수도 있어요.
aggregate C 타입에 대해서도 이 프로토콜 채택을 선언할 방법이 있으면 좋겠는데, 이를 위해 "채택 선언은 타입을 담은 모듈에서만 가능하다"는 제약을 import된 C 타입에 한해 완화하는 것도 유용할 것 같습니다.
튜플 지원
ConvertibleToBytes타입으로 구성된 튜플은 그 자체로ConvertibleToBytes여야 하고,ConvertibleFromBytes도 마찬가지예요.RawSpan의 정렬 상태를 확인하는 유틸리티
Span이니셜라이저들은 올바르게 정렬된RawSpan을 요구하는데, 특정 타입에 대해 잘 정렬된 오프셋을 확인하는 유틸리티가 있으면 좋을 것 같다고 제안하고 있어요.@unsafe 함수/프로퍼티 이름 재정비
이전 제안에서 도입된 일부 함수/프로퍼티는 나중에 unsafe로 표시됐지만, 이름 자체에는 그 사실이 드러나지 않아요.
strict memory safety 모드가 꺼져 있어도 unsafe함을 드러내는 이름들을 식별해서, 별도 제안을 통해 최소한의 혼란으로 이름을 바꾸는 계획이 필요합니다.
OutputSpan과 OutputRawSpan 사이의 append 지원
원래 이 제안에는
Element가ConvertibleFromBytes일 때OutputRawSpan으로OutputSpan의 일부에 append하는 함수도 포함되어 있었어요.span.append(upTo: capacity) { for _ in 0..<$0.capacity { $0.append(UInt8.zero) } }capacity를 위한 인자 레이블(여기선 "upTo")을 정하는 게 까다로운 주제라,
UniqueArray의insert,replace,append에도 같은 형태의 API가 필요하다는 점을 짚었어요.이 네이밍은 SE-0527(UniqueArray와 RigidArray) 쪽에서 충분히 논의된 뒤,
OutputSpan과OutputRawSpan에 적절한 variant와 함께 추가하자는 방향으로 정리됐습니다.Alternatives Considered
함수 이름에 로드할 타입 이름을 인코딩하기
loadInt32(fromByteOffset:_:),storeBytes(int32:toByteOffset:_:)처럼 구체적인 함수들을 따로 만들면 오버로드 문제를 피할 수 있어서 타입 체커 입장에서는 더 편할 수 있어요.컴파일러가 검증하는 ConvertibleToBytes 레이아웃 제약을 기다리기
이 제안이 다루는 기능은 시급하고, 표준 라이브러리 추가만으로도 달성할 수 있어요.
ConvertibleToBytes레이아웃 제약의 검증에는 상당한 컴파일러 작업이 필요한데, 이 문서가 제안하는 API만으로도 충분히 가치가 있다고 판단했습니다.FixedWidthInteger & BitwiseCopyable, BinaryFloatingPoint & BitwiseCopyable로 제네릭화하기
이 프로토콜들은 채택자가 완전히 inhabited되어야 한다는 걸 보장하지 않아서 충분한 제약이 아니에요.
FullyInhabited 프로토콜 하나만 추가하기
이 제안의 두 번째 pitch에서는
FullyInhabited하나만 제안했었어요.논의 결과 결국 두 프로토콜 쌍이 필요하다는 게 드러났고, 구현 부담도 비슷했기 때문에 두 프로토콜을 각각 구현하는 쪽이 더 나은 선택으로 정리됐습니다.
ByteOrder 파라미터 생략하기
표준 라이브러리의
FixedWidthInteger에는.bigEndian,.littleEndian계산 프로퍼티가 있어서 이를 활용할 수도 있었어요.하지만 이 프로퍼티들은
Self를 반환해서 byte ordering과 유효한 값 자체를 혼동시키기 때문에 명확하지 않아요.이 제안은 byte ordering을 직렬화라는, 원래 있어야 할 자리에 적용합니다.
안전한 load() 함수를 정렬된 연산으로 기본 설정하기
UnsafeRawPointer의 원래load()는 올바른 정렬을 요구했고, 나중에야 덜 제한적인loadUnaligned()가 추가됐어요.이건 오랫동안 아쉬운 부분으로 여겨져 왔고, 이번 제안은 새 안전한
load()함수들이 정렬되지 않은 연산을 수행하도록 만들어서 이를 개선하려 하고 있습니다.toByteOffset, fromByteOffset 파라미터를 0으로 기본값 설정하기
원래는 이 파라미터들에 기본값을 두려고 했는데, 그러면
RawSpan이나MutableRawSpan의 전체 길이를 반드시 써야 한다는 인상을 줄 수 있다는 지적이 있었어요.비슷한 기존 unsafe API에는 이미 기본값이 있다는 점을 감안해서, 이 결정은 나중에 조율이 필요할 수 있습니다.
Conclusion
ConvertibleToBytes와ConvertibleFromBytes프로토콜 덕분에 그동안unsafe딱지가 붙어 있던RawSpanbyte-loading이 명확하게 안전한 영역과 그렇지 않은 영역으로 나뉘게 됐어요.정수, 부동소수점, SIMD, 포인터 타입 등 표준 라이브러리의 많은 타입이 바로 이 프로토콜들을 채택하고, 안전한
bitCast까지 함께 따라오면서 프로세스 간 데이터 전달이나 파싱 유틸리티를 만들 때 훨씬 믿음직한 building block이 생긴 셈입니다 🙌References
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' 카테고리의 다른 글
[SE-0532] Optional noncopyable improvements and generalizations (1) 2026.07.18 What's new in Swift (feat. WWDC 2026) (1) 2026.06.22 [SE-0531] Literal Expressions (1) 2026.06.21 [SE-0530] Async Result Support (0) 2026.06.07 [SE-0528] Continuation — Safe and Performant Async Continuations (0) 2026.05.30