Skip to content

Commit 8ce9756

Browse files
FranzBuschitingliu
andauthored
Proposal to generate UUIDs using RandomNumberGenerators (#1271)
* Proposal to generate `UUID`s using `RandomNumberGenerator`s This PR adds a proposal to generate `UUID's` using `RandomNumberGenerator`s * Seeded deterministic UUID generation test * Fix code comment * Migrate to swift-testing * Update availability Co-authored-by: Tina L <[email protected]> * Use binary literals * Add separators --------- Co-authored-by: Tina L <[email protected]>
1 parent c37addb commit 8ce9756

File tree

3 files changed

+161
-0
lines changed

3 files changed

+161
-0
lines changed

Proposals/NNNN-random-uuid.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Generating UUIDs using RandomNumberGenerators
2+
3+
* Proposal: [SF-NNNN](NNNN-random-uuid.md)
4+
* Authors: [FranzBusch](https://github.com/FranzBusch)
5+
* Review Manager: TBD
6+
* Status: **Awaiting review**
7+
* Implementation: [swiftlang/swift-foundation#1271](https://github.com/swiftlang/swift-foundation/pull/1271)
8+
* Review: ([pitch](https://forums.swift.org/...))
9+
10+
## Introduction
11+
12+
UUIDs (Universally Unique IDentifiers) are 128 bits long and is intended to
13+
guarantee uniqueness across space and time. This proposal adds APIs to generate
14+
UUIDs from Swift's random number generators.
15+
16+
## Motivation
17+
18+
UUIDs often need to be randomly generated. This is currently possible by calling
19+
the `UUID` initializer. However, this initializer doesn't allow providing a
20+
custom source from which the `UUID` is generated. Swift's standard library
21+
provides a common abstraction for random number generators through the
22+
`RandomNumberGenerator` protocol. Providing methods to generate `UUID`s using a
23+
`RandomNumberGenerator` allows developers to customize their source of randomness.
24+
25+
An example where this is useful is where a system needs to generate UUIDs using a
26+
deterministically seeded random number generator.
27+
28+
## Proposed solution
29+
30+
This proposal adds a new static method to the `UUID` type to generate new random `UUIDs` using a `RandomNumberGenerator`.
31+
32+
```swift
33+
/// Generates a new random UUID.
34+
///
35+
/// - Parameter generator: The random number generator to use when creating the new random value.
36+
/// - Returns: A random UUID.
37+
@available(FoundationPreview 6.3, *)
38+
public static func random(
39+
using generator: inout some RandomNumberGenerator
40+
) -> UUID
41+
```
42+
43+
## Source compatibility
44+
45+
The new API is purely additive and ha no impact on the existing API.
46+
47+
## Implications on adoption
48+
49+
This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source compatibility.
50+
51+
## Alternatives considered
52+
53+
### Initializer based random UUID generation
54+
55+
The existing `UUID.init()` is already generating new random `UUID`s and a new
56+
`UUID(using: &rng)` method would be a good alternative to the proposed static method.
57+
However, the static `random` method has precedence on various types such as [Int.random](https://developer.apple.com/documentation/swift/int/random(in:)-9mjpw).

Sources/FoundationEssentials/UUID.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,50 @@ public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable {
7676
hasher.combine(bytes: buffer)
7777
}
7878
}
79+
80+
/// Generates a new random UUID.
81+
///
82+
/// - Parameter generator: The random number generator to use when creating the new random value.
83+
/// - Returns: A random UUID.
84+
@available(FoundationPreview 6.3, *)
85+
public static func random(
86+
using generator: inout some RandomNumberGenerator
87+
) -> UUID {
88+
let first = UInt64.random(in: .min ... .max, using: &generator)
89+
let second = UInt64.random(in: .min ... .max, using: &generator)
90+
91+
var firstBits = first
92+
var secondBits = second
93+
94+
// Set the version to 4 (0100 in binary)
95+
firstBits &= 0b11111111_11111111_11111111_11111111_11111111_11111111_00001111_11111111 // Clear bits 48 through 51
96+
firstBits |= 0b00000000_00000000_00000000_00000000_00000000_00000000_01000000_00000000 // Set the version bits to '0100' at the correct position
97+
98+
// Set the variant to '10' (RFC9562 variant)
99+
secondBits &= 0b00111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111 // Clear the 2 most significant bits
100+
secondBits |= 0b10000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000 // Set the two MSB to '10'
101+
102+
let uuidBytes = (
103+
UInt8(truncatingIfNeeded: firstBits >> 56),
104+
UInt8(truncatingIfNeeded: firstBits >> 48),
105+
UInt8(truncatingIfNeeded: firstBits >> 40),
106+
UInt8(truncatingIfNeeded: firstBits >> 32),
107+
UInt8(truncatingIfNeeded: firstBits >> 24),
108+
UInt8(truncatingIfNeeded: firstBits >> 16),
109+
UInt8(truncatingIfNeeded: firstBits >> 8),
110+
UInt8(truncatingIfNeeded: firstBits),
111+
UInt8(truncatingIfNeeded: secondBits >> 56),
112+
UInt8(truncatingIfNeeded: secondBits >> 48),
113+
UInt8(truncatingIfNeeded: secondBits >> 40),
114+
UInt8(truncatingIfNeeded: secondBits >> 32),
115+
UInt8(truncatingIfNeeded: secondBits >> 24),
116+
UInt8(truncatingIfNeeded: secondBits >> 16),
117+
UInt8(truncatingIfNeeded: secondBits >> 8),
118+
UInt8(truncatingIfNeeded: secondBits)
119+
)
120+
121+
return UUID(uuid: uuidBytes)
122+
}
79123

80124
public var description: String {
81125
return uuidString

Tests/FoundationEssentialsTests/UUIDTests.swift

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,64 @@ private struct UUIDTests {
124124
#expect(uuid2 <= uuid1)
125125
#expect(uuid2 == uuid1)
126126
}
127+
128+
@Test func randomVersionAndVariant() {
129+
var generator = SystemRandomNumberGenerator()
130+
for _ in 0..<10000 {
131+
let uuid = UUID.random(using: &generator)
132+
#expect(uuid.versionNumber == 0b0100)
133+
#expect(uuid.varint == 0b10)
134+
}
135+
}
136+
137+
@Test func deterministicRandomGeneration() {
138+
var generator = PCGRandomNumberGenerator(seed: 123456789)
139+
140+
let firstUUID = UUID.random(using: &generator)
141+
#expect(firstUUID == UUID(uuidString: "9492BAC4-F353-49E7-ACBB-A40941CA65DE"))
142+
143+
let secondUUID = UUID.random(using: &generator)
144+
#expect(secondUUID == UUID(uuidString: "392C44E5-EB3E-4455-85A7-AF9556722B9A"))
145+
146+
let thirdUUID = UUID.random(using: &generator)
147+
#expect(thirdUUID == UUID(uuidString: "9ABFCCE9-AA85-485C-9CBF-C62F0C8D1D1A"))
148+
149+
let fourthUUID = UUID.random(using: &generator)
150+
#expect(fourthUUID == UUID(uuidString: "2B29542E-F719-4D58-87B9-C6291ADD4541"))
151+
}
127152
}
153+
154+
extension UUID {
155+
fileprivate var versionNumber: Int {
156+
Int(self.uuid.6 >> 4)
157+
}
158+
159+
fileprivate var varint: Int {
160+
Int(self.uuid.8 >> 6 & 0b11)
161+
}
162+
}
163+
164+
fileprivate struct PCGRandomNumberGenerator: RandomNumberGenerator {
165+
private static let multiplier: UInt128 = 47_026_247_687_942_121_848_144_207_491_837_523_525
166+
private static let increment: UInt128 = 117_397_592_171_526_113_268_558_934_119_004_209_487
167+
168+
private var state: UInt128
169+
170+
fileprivate init(seed: UInt64) {
171+
self.state = UInt128(seed)
172+
}
173+
174+
fileprivate mutating func next() -> UInt64 {
175+
self.state = self.state &* Self.multiplier &+ Self.increment
176+
177+
return rotr64(
178+
value: UInt64(truncatingIfNeeded: self.state &>> 64) ^ UInt64(truncatingIfNeeded: self.state),
179+
rotation: UInt64(truncatingIfNeeded: self.state &>> 122)
180+
)
181+
}
182+
183+
private func rotr64(value: UInt64, rotation: UInt64) -> UInt64 {
184+
(value &>> rotation) | value &<< ((~rotation &+ 1) & 63)
185+
}
186+
}
187+

0 commit comments

Comments
 (0)