Skip to content

Commit 86b697a

Browse files
Add basic RawSpan support for initializing and merging messages (#1857)
On platforms targeting Swift 6.2 and later this allows a `RawSpan` in lieu of `SwiftProtobufContiguousBytes`-conforming type when creating or merging a protobuf message. This will improve memory usage by reducing allocations, for example when parsing a large document that includes protobuf messages as part of its binary. Unlike the internal `_merge` function, this does not expose unsafe API while retaining its benefits. - I decided to make a twin declaration of the initializer and `merge` function instead of conforming `RawSpan` to `SwiftProtobufContiguousTypes`, because - a) `RawSpan` is not mutable, and - b) `Span` types are non-owning and cannot conform to initializers. - I used the conditional compilation block and `@available` directive per ~~analogy to [this implementation in SwiftNIO](https://github.com/apple/swift-nio/blob/main/Sources/NIOCore/ByteBuffer-core.swift#L1066)~~ [docs](https://developer.apple.com/documentation/swift/rawspan). --------- Co-authored-by: Franz Busch <f.busch@apple.com>
1 parent e2ba53a commit 86b697a

File tree

2 files changed

+145
-0
lines changed

2 files changed

+145
-0
lines changed

Sources/SwiftProtobuf/Message+BinaryAdditions.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,34 @@ extension Message {
100100
try merge(serializedBytes: bytes, extensions: extensions, partial: partial, options: options)
101101
}
102102

103+
#if compiler(>=6.2)
104+
/// Creates a new message by decoding the bytes provided by a `RawSpan`
105+
/// containing a serialized message in Protocol Buffer binary format.
106+
///
107+
/// - Parameters:
108+
/// - serializedBytes: The `RawSpan` of binary-encoded message data to decode.
109+
/// - extensions: An ``ExtensionMap`` used to look up and decode any
110+
/// extensions in this message or messages nested within this message's
111+
/// fields.
112+
/// - partial: If `false` (the default), this method will check
113+
/// ``Message/isInitialized-6abgi`` after decoding to verify that all required
114+
/// fields are present. If any are missing, this method throws
115+
/// ``BinaryDecodingError/missingRequiredFields``.
116+
/// - options: The ``BinaryDecodingOptions`` to use.
117+
/// - Throws: ``BinaryDecodingError`` if decoding fails.
118+
@inlinable
119+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *)
120+
public init(
121+
serializedBytes bytes: RawSpan,
122+
extensions: (any ExtensionMap)? = nil,
123+
partial: Bool = false,
124+
options: BinaryDecodingOptions = BinaryDecodingOptions()
125+
) throws {
126+
self.init()
127+
try merge(serializedBytes: bytes, extensions: extensions, partial: partial, options: options)
128+
}
129+
#endif
130+
103131
/// Updates the message by decoding the given `SwiftProtobufContiguousBytes` value
104132
/// containing a serialized message in Protocol Buffer binary format into the
105133
/// receiver.
@@ -131,6 +159,39 @@ extension Message {
131159
}
132160
}
133161

162+
#if compiler(>=6.2)
163+
/// Updates the message by decoding the bytes provided by a `RawSpan` containing
164+
/// a serialized message in Protocol Buffer binary format into the receiver.
165+
///
166+
/// - Note: If this method throws an error, the message may still have been
167+
/// partially mutated by the binary data that was decoded before the error
168+
/// occurred.
169+
///
170+
/// - Parameters:
171+
/// - serializedBytes: The `RawSpan` of binary-encoded message data to decode.
172+
/// - extensions: An ``ExtensionMap`` used to look up and decode any
173+
/// extensions in this message or messages nested within this message's
174+
/// fields.
175+
/// - partial: If `false` (the default), this method will check
176+
/// ``Message/isInitialized-6abgi`` after decoding to verify that all required
177+
/// fields are present. If any are missing, this method throws
178+
/// ``BinaryDecodingError/missingRequiredFields``.
179+
/// - options: The ``BinaryDecodingOptions`` to use.
180+
/// - Throws: ``BinaryDecodingError`` if decoding fails.
181+
@inlinable
182+
@available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *)
183+
public mutating func merge(
184+
serializedBytes bytes: RawSpan,
185+
extensions: (any ExtensionMap)? = nil,
186+
partial: Bool = false,
187+
options: BinaryDecodingOptions = BinaryDecodingOptions()
188+
) throws {
189+
try bytes.withUnsafeBytes { (body: UnsafeRawBufferPointer) in
190+
try _merge(rawBuffer: body, extensions: extensions, partial: partial, options: options)
191+
}
192+
}
193+
#endif
194+
134195
// Helper for `merge()`s to keep the Decoder internal to SwiftProtobuf while
135196
// allowing the generic over `SwiftProtobufContiguousBytes` to get better codegen from the
136197
// compiler by being `@inlinable`. For some discussion on this see
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import Foundation
2+
import SwiftProtobuf
3+
import XCTest
4+
5+
#if compiler(>=6.2)
6+
7+
final class Test_RawSpan: XCTestCase {
8+
func testEmptyRawSpan() throws {
9+
guard #available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) else {
10+
throw XCTSkip("Span structs not available on selected platform")
11+
}
12+
13+
let emptyRawSpan = RawSpan()
14+
15+
let decoded = try SwiftProtoTesting_TestAllTypes(serializedBytes: emptyRawSpan)
16+
let expected = SwiftProtoTesting_TestAllTypes()
17+
18+
XCTAssertEqual(decoded, expected, "Empty span should decode to equal empty message")
19+
}
20+
21+
func testRawSpanReencodedEmptyByteArray() throws {
22+
guard #available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *) else {
23+
throw XCTSkip("span.bytes not available on selected platform")
24+
}
25+
26+
let expected: [UInt8] = []
27+
let expectedRawSpan = expected.span.bytes
28+
29+
let decoded = try SwiftProtoTesting_TestAllTypes(serializedBytes: expectedRawSpan)
30+
let reencoded: [UInt8] = try decoded.serializedBytes()
31+
32+
XCTAssertEqual(
33+
reencoded,
34+
expected,
35+
"Raw span of empty array of bytes should decode and encode as empty message"
36+
)
37+
}
38+
39+
func testRawSpanDataEncodeDecode() throws {
40+
guard #available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) else {
41+
throw XCTSkip("Span structs not available on selected platform")
42+
}
43+
44+
let expected = SwiftProtoTesting_TestAllTypes.with {
45+
$0.optionalInt32 = 1
46+
$0.optionalInt64 = Int64.max
47+
$0.optionalString = "RawSpan test"
48+
$0.repeatedBool = [true, false]
49+
}
50+
51+
let encoded: Data = try expected.serializedBytes()
52+
let encodedRawSpan: RawSpan = encoded.bytes
53+
54+
let decoded = try SwiftProtoTesting_TestAllTypes(serializedBytes: encodedRawSpan)
55+
56+
XCTAssertEqual(decoded, expected, "")
57+
}
58+
59+
func testRawSpanTruncated() throws {
60+
guard #available(macOS 10.14.4, iOS 12.2, watchOS 5.2, tvOS 12.2, visionOS 1.0, *) else {
61+
throw XCTSkip("Span structs not available on selected platform")
62+
}
63+
64+
let expected = SwiftProtoTesting_TestAllTypes.with {
65+
$0.optionalInt32 = 1
66+
$0.optionalInt64 = Int64.max
67+
$0.optionalString = "RawSpan test"
68+
$0.repeatedBool = [true, false]
69+
}
70+
71+
let encoded: Data = try expected.serializedBytes()
72+
let truncatedRawSpan: RawSpan = encoded.bytes.extracting(droppingLast: 1)
73+
74+
var decoded = SwiftProtoTesting_TestAllTypes()
75+
76+
XCTAssertThrowsError(
77+
try decoded.merge(serializedBytes: truncatedRawSpan)
78+
) { error in
79+
XCTAssertEqual(error as? BinaryDecodingError, BinaryDecodingError.truncated)
80+
}
81+
}
82+
}
83+
84+
#endif

0 commit comments

Comments
 (0)