Skip to content

Commit 02c03c3

Browse files
authored
swift: Variadic withAllBorrowed, to break down the pyramids of doom
1 parent 2879220 commit 02c03c3

26 files changed

+486
-345
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
//
2+
// Copyright 2025 Signal Messenger, LLC.
3+
// SPDX-License-Identifier: AGPL-3.0-only
4+
//
5+
6+
import Foundation
7+
import SignalFfi
8+
9+
/// Represents a type that can be "borrowed" into an FFI-compatible form using a scope-based callback.
10+
internal protocol BorrowForFfi {
11+
associatedtype Borrowed
12+
func withBorrowed<Result>(_ callback: (Borrowed) throws -> Result) throws -> Result
13+
}
14+
15+
/// Invokes `callback` with the borrowed form of each `input`
16+
internal func withAllBorrowed<Result, each Input: BorrowForFfi>(
17+
_ input: repeat each Input,
18+
in callback: (repeat (each Input).Borrowed) throws -> Result
19+
) throws -> Result {
20+
return try withoutActuallyEscaping(callback) { callback in
21+
// Optimization: allocate the correct-sized array up front.
22+
var count = 0
23+
for _ in repeat (each Input).self {
24+
count += 1
25+
}
26+
27+
// Make space for each of the "borrowed" values.
28+
// If we had first-class references like Rust we could do:
29+
// var borrows: (repeat (each Input)?) = (repeat nil)
30+
// var places = (repeat &mut (each borrows))
31+
// and then iterate over `inputs` and `places` together.
32+
// But we don't, so we instead build up an array of pointers
33+
// to stack locations and read them all out at the end.
34+
// And we use a stack array instead of a full Array as a microoptimization.
35+
// (We could put the values directly in the array instead using Any,
36+
// but then we have to downcast them to get them out.)
37+
return try withUnsafeTemporaryAllocation(of: UnsafeRawPointer.self, capacity: count) { borrows in
38+
var nextIndex = count - 1
39+
40+
// Because the "borrow" operations have to be nested to work correctly,
41+
// we build up a compound operation whose final (innermost) step is
42+
// "actually invoke the callback". The wrapping layers are described in the loop below.
43+
var operation = {
44+
// The "innermost" operation reads out all the values from the pointers in `borrows`
45+
// and invokes the user's callback.
46+
var pointerIter = borrows.makeIterator()
47+
let borrows = (repeat pointerIter.next()!.load(as: (each Input).Borrowed.self))
48+
return try callback(repeat each borrows)
49+
}
50+
51+
for next in repeat each input {
52+
// For each input, we wrap `operation` ("the rest of the work") in "borrow this input,
53+
// store it for later, and invoke the rest of the work".
54+
// Note that this only works because we're promising not to use the array after the
55+
// operation is complete.
56+
operation = { [operation] in
57+
return try next.withBorrowed { borrowed in
58+
try withUnsafePointer(to: borrowed) { pointer in
59+
borrows[nextIndex] = UnsafeRawPointer(pointer)
60+
nextIndex -= 1
61+
return try operation()
62+
}
63+
}
64+
}
65+
}
66+
67+
// Finally, we can invoke our stacked operation, which looks something like this:
68+
// - Borrow input3, then...
69+
// - Borrow input2, then...
70+
// - Borrow input1, then...
71+
// - Read out (input1, input2, input3) and invoke the original callback.
72+
return try operation()
73+
}
74+
}
75+
}
76+
77+
// Overloads for the short cases, to help with optimization.
78+
79+
internal func withAllBorrowed<Result, Input1: BorrowForFfi>(
80+
_ input1: Input1,
81+
in callback: (Input1.Borrowed) throws -> Result
82+
) throws -> Result {
83+
return try input1.withBorrowed(callback)
84+
}
85+
86+
internal func withAllBorrowed<Result, Input1: BorrowForFfi, Input2: BorrowForFfi>(
87+
_ input1: Input1,
88+
_ input2: Input2,
89+
in callback: (Input1.Borrowed, Input2.Borrowed) throws -> Result
90+
) throws -> Result {
91+
// Borrow in reverse order, like the variadic one does.
92+
return try input2.withBorrowed { input2 in
93+
try input1.withBorrowed { input1 in
94+
try callback(input1, input2)
95+
}
96+
}
97+
}
98+
99+
internal func withAllBorrowed<Result, Input1: BorrowForFfi, Input2: BorrowForFfi, Input3: BorrowForFfi>(
100+
_ input1: Input1,
101+
_ input2: Input2,
102+
_ input3: Input3,
103+
in callback: (Input1.Borrowed, Input2.Borrowed, Input3.Borrowed) throws -> Result
104+
) throws -> Result {
105+
// Borrow in reverse order, like the variadic one does.
106+
return try input3.withBorrowed { input3 in
107+
try input2.withBorrowed { input2 in
108+
try input1.withBorrowed { input1 in
109+
try callback(input1, input2, input3)
110+
}
111+
}
112+
}
113+
}
114+
115+
extension NativeHandleOwner: BorrowForFfi {
116+
typealias Borrowed = PointerType
117+
func withBorrowed<Result>(_ callback: (Borrowed) throws -> Result) rethrows -> Result {
118+
return try self.withNativeHandle(callback)
119+
}
120+
}
121+
122+
extension ByteArray: BorrowForFfi {
123+
typealias Borrowed = SignalBorrowedBuffer
124+
func withBorrowed<Result>(_ callback: (Borrowed) throws -> Result) rethrows -> Result {
125+
return try self.withUnsafeBorrowedBuffer(callback)
126+
}
127+
}
128+
129+
extension ServiceId: BorrowForFfi {
130+
typealias Borrowed = UnsafePointer<ServiceIdStorage>
131+
func withBorrowed<Result>(_ callback: (Borrowed) throws -> Result) rethrows -> Result {
132+
return try self.withPointerToFixedWidthBinary(callback)
133+
}
134+
}
135+
136+
extension Randomness: BorrowForFfi {
137+
typealias Borrowed = UnsafePointer<SignalRandomnessBytes>
138+
func withBorrowed<Result>(_ callback: (Borrowed) throws -> Result) rethrows -> Result {
139+
return try self.withUnsafePointerToBytes(callback)
140+
}
141+
}
142+
143+
extension Data: BorrowForFfi {
144+
typealias Borrowed = SignalBorrowedBuffer
145+
func withBorrowed<Result>(_ callback: (SignalBorrowedBuffer) throws -> Result) rethrows -> Result {
146+
return try self.withUnsafeBorrowedBuffer(callback)
147+
}
148+
}
149+
150+
extension Optional: BorrowForFfi where Wrapped: BorrowForFfi, Wrapped.Borrowed == SignalBorrowedBuffer {
151+
typealias Borrowed = SignalOptionalBorrowedSliceOfc_uchar
152+
func withBorrowed<Result>(_ callback: (SignalOptionalBorrowedSliceOfc_uchar) throws -> Result) throws -> Result {
153+
guard let self else {
154+
return try callback(.init())
155+
}
156+
return try self.withBorrowed { buffer in
157+
try callback(.init(present: true, value: buffer))
158+
}
159+
}
160+
}
161+
162+
internal struct ContiguousBytesWrapper<Inner: ContiguousBytes>: BorrowForFfi {
163+
var inner: Inner
164+
165+
typealias Borrowed = SignalBorrowedBuffer
166+
func withBorrowed<Result>(_ callback: (SignalBorrowedBuffer) throws -> Result) rethrows -> Result {
167+
return try self.inner.withUnsafeBorrowedBuffer(callback)
168+
}
169+
}
170+
171+
extension BorrowForFfi {
172+
// A trick to expose a contextual shortcut where a BorrowForFfi instance is expected.
173+
// See <https://github.com/swiftlang/swift-evolution/blob/main/proposals/0299-extend-generic-static-member-lookup.md>.
174+
static func bytes<Bytes: ContiguousBytes>(_ bytes: Bytes) -> Self where Self == ContiguousBytesWrapper<Bytes> {
175+
.init(inner: bytes)
176+
}
177+
}
178+
179+
extension UUID: BorrowForFfi {
180+
typealias Borrowed = UnsafePointer<uuid_t>
181+
func withBorrowed<Result>(_ callback: (Borrowed) throws -> Result) rethrows -> Result {
182+
return try withUnsafePointer(to: self.uuid, callback)
183+
}
184+
}
185+
186+
internal struct FixedLengthWrapper<FixedLengthRepr>: BorrowForFfi {
187+
var inner: ByteArray
188+
189+
typealias Borrowed = UnsafePointer<FixedLengthRepr>
190+
func withBorrowed<Result>(_ callback: (Borrowed) throws -> Result) throws -> Result {
191+
return try self.inner.withUnsafePointerToSerialized(callback)
192+
}
193+
}
194+
195+
extension BorrowForFfi {
196+
static func fixed<FixedLengthRepr>(_ serialized: ByteArray) -> Self where Self == FixedLengthWrapper<FixedLengthRepr> {
197+
.init(inner: serialized)
198+
}
199+
}
200+
201+
protocol FfiBorrowedSlice {
202+
associatedtype Element
203+
// We ought to be able to make the requirement init(base:length:), like the one that gets synthesized,
204+
// but that doesn't seem to work.
205+
init(_ buffer: UnsafeBufferPointer<Element>)
206+
}
207+
208+
extension SignalBorrowedSliceOfConstPointerSessionRecord: FfiBorrowedSlice {
209+
init(_ buffer: UnsafeBufferPointer<SignalConstPointerSessionRecord>) {
210+
self.init(base: buffer.baseAddress, length: buffer.count)
211+
}
212+
}
213+
214+
extension SignalBorrowedSliceOfConstPointerProtocolAddress: FfiBorrowedSlice {
215+
init(_ buffer: UnsafeBufferPointer<SignalConstPointerProtocolAddress>) {
216+
self.init(base: buffer.baseAddress, length: buffer.count)
217+
}
218+
}
219+
220+
internal struct ElementsWrapper<FfiType: FfiBorrowedSlice>: BorrowForFfi {
221+
var inner: [FfiType.Element]
222+
223+
typealias Borrowed = FfiType
224+
func withBorrowed<Result>(_ callback: (Borrowed) throws -> Result) throws -> Result {
225+
return try self.inner.withUnsafeBufferPointer {
226+
try callback(.init($0))
227+
}
228+
}
229+
}
230+
231+
extension BorrowForFfi {
232+
static func slice<FfiType: FfiBorrowedSlice>(_ input: [FfiType.Element]) -> Self where Self == ElementsWrapper<FfiType> {
233+
.init(inner: input)
234+
}
235+
}

swift/Sources/LibSignalClient/Fingerprint.swift

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,21 @@ public struct NumericFingerprintGenerator: Sendable {
5353
remoteKey: PublicKey
5454
) throws -> Fingerprint {
5555
var obj = SignalMutPointerFingerprint()
56-
try withNativeHandles(localKey, remoteKey) { localKeyHandle, remoteKeyHandle in
57-
try localIdentifier.withUnsafeBorrowedBuffer { localBuffer in
58-
try remoteIdentifier.withUnsafeBorrowedBuffer { remoteBuffer in
59-
try checkError(signal_fingerprint_new(
60-
&obj,
61-
UInt32(self.iterations),
62-
UInt32(version),
63-
localBuffer,
64-
localKeyHandle.const(),
65-
remoteBuffer,
66-
remoteKeyHandle.const()
67-
))
68-
}
69-
}
56+
try withAllBorrowed(
57+
localKey,
58+
remoteKey,
59+
.bytes(localIdentifier),
60+
.bytes(remoteIdentifier)
61+
) { localKeyHandle, remoteKeyHandle, localBuffer, remoteBuffer in
62+
try checkError(signal_fingerprint_new(
63+
&obj,
64+
UInt32(self.iterations),
65+
UInt32(version),
66+
localBuffer,
67+
localKeyHandle.const(),
68+
remoteBuffer,
69+
remoteKeyHandle.const()
70+
))
7071
}
7172

7273
let fprintStr = try invokeFnReturningString {

swift/Sources/LibSignalClient/IdentityKey.swift

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,8 @@ public struct IdentityKey: Equatable, Sendable {
2323

2424
public func verifyAlternateIdentity<Bytes: ContiguousBytes>(_ other: IdentityKey, signature: Bytes) throws -> Bool {
2525
var result = false
26-
try withNativeHandles(publicKey, other.publicKey) { selfHandle, otherHandle in
27-
try signature.withUnsafeBorrowedBuffer { signatureBuffer in
28-
try checkError(signal_identitykey_verify_alternate_identity(&result, selfHandle.const(), otherHandle.const(), signatureBuffer))
29-
}
26+
try withAllBorrowed(publicKey, other.publicKey, .bytes(signature)) { selfHandle, otherHandle, signatureBuffer in
27+
try checkError(signal_identitykey_verify_alternate_identity(&result, selfHandle.const(), otherHandle.const(), signatureBuffer))
3028
}
3129
return result
3230
}
@@ -59,8 +57,8 @@ public struct IdentityKeyPair: Sendable {
5957
}
6058

6159
public func serialize() -> Data {
62-
return withNativeHandles(self.publicKey, self.privateKey) { publicKey, privateKey in
63-
failOnError {
60+
return failOnError {
61+
try withAllBorrowed(self.publicKey, self.privateKey) { publicKey, privateKey in
6462
try invokeFnReturningData {
6563
signal_identitykeypair_serialize($0, publicKey.const(), privateKey.const())
6664
}
@@ -73,8 +71,8 @@ public struct IdentityKeyPair: Sendable {
7371
}
7472

7573
public func signAlternateIdentity(_ other: IdentityKey) -> Data {
76-
return withNativeHandles(self.publicKey, self.privateKey, other.publicKey) { publicKey, privateKey, other in
77-
failOnError {
74+
return failOnError {
75+
try withAllBorrowed(self.publicKey, self.privateKey, other.publicKey) { publicKey, privateKey, other in
7876
try invokeFnReturningData {
7977
signal_identitykeypair_sign_alternate_identity($0, publicKey.const(), privateKey.const(), other.const())
8078
}

swift/Sources/LibSignalClient/Kem.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ extension SignalConstPointerKyberPublicKey: SignalConstPointer {
119119

120120
extension KEMPublicKey: Equatable {
121121
public static func == (lhs: KEMPublicKey, rhs: KEMPublicKey) -> Bool {
122-
return withNativeHandles(lhs, rhs) { lHandle, rHandle in
123-
failOnError {
122+
return failOnError {
123+
try withAllBorrowed(lhs, rhs) { lHandle, rHandle in
124124
try invokeFnReturningBool {
125125
signal_kyber_public_key_equals($0, lHandle.const(), rHandle.const())
126126
}

0 commit comments

Comments
 (0)