Skip to content

Proposal to generate UUIDs using RandomNumberGenerators #1271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Proposals/NNNN-random-uuid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Generating UUIDs using RandomNumberGenerators

* Proposal: [SF-NNNN](NNNN-random-uuid.md)
* Authors: [FranzBusch](https://github.com/FranzBusch)
* Review Manager: TBD
* Status: **Awaiting review**
* Implementation: [swiftlang/swift-foundation#1271](https://github.com/swiftlang/swift-foundation/pull/1271)
* Review: ([pitch](https://forums.swift.org/...))

## Introduction

UUIDs (Universally Unique IDentifiers) are 128 bits long and is intended to
guarantee uniqueness across space and time. This proposal adds APIs to generate
UUIDs from Swift's random number generators.

## Motivation

UUIDs often need to be randomly generated. This is currently possible by calling
the `UUID` initializer. However, this initializer doesn't allow providing a
custom source from which the `UUID` is generated. Swift's standard library
provides a common abstraction for random number generators through the
`RandomNumberGenerator` protocol. Providing methods to generate `UUID`s using a
`RandomNumberGenerator` allows developers to customize their source of randomness.

An example where this is useful is where a system needs to generate UUIDs using a
deterministically seeded random number generator.

## Proposed solution

This proposal adds a new static method to the `UUID` type to generate new random `UUIDs` using a `RandomNumberGenerator`.

```swift
/// Generates a new random UUID.
///
/// - Parameter generator: The random number generator to use when creating the new random value.
/// - Returns: A random UUID.
@available(FoundationPreview 6.2, *)
public static func random(
using generator: inout some RandomNumberGenerator
) -> UUID
```

## Source compatibility

The new API is purely additive and ha no impact on the existing API.

## Implications on adoption

This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source compatibility.

## Alternatives considered

### Initializer based random UUID generation

The existing `UUID.init()` is already generating new random `UUID`s and a new
`UUID(using: &rng)` method would be a good alternative to the proposed static method.
However, the static `random` method has precedence on various types such as [Int.random](https://developer.apple.com/documentation/swift/int/random(in:)-9mjpw).
44 changes: 44 additions & 0 deletions Sources/FoundationEssentials/UUID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,50 @@ public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable {
hasher.combine(bytes: buffer)
}
}

/// Generates a new random UUID.
///
/// - Parameter generator: The random number generator to use when creating the new random value.
/// - Returns: A random UUID.
@available(FoundationPreview 6.2, *)
public static func random(
using generator: inout some RandomNumberGenerator
) -> UUID {
let first = UInt64.random(in: .min ... .max, using: &generator)
let second = UInt64.random(in: .min ... .max, using: &generator)

var firstBits = first
var secondBits = second

// Set the version to 4 (0100 in binary)
firstBits &= 0xFFFFFFFFFFFF0FFF // Clear bits 48 through 51
firstBits |= 0x0000000000004000 // Set the version bits to '0100' at the correct position

// Set the variant to '10' (RFC9562 variant)
secondBits &= 0x3FFFFFFFFFFFFFFF // Clear the 2 most significant bits
secondBits |= 0x8000000000000000 // Set the two MSB to '10'

let uuidBytes = (
UInt8(truncatingIfNeeded: firstBits >> 56),
UInt8(truncatingIfNeeded: firstBits >> 48),
UInt8(truncatingIfNeeded: firstBits >> 40),
UInt8(truncatingIfNeeded: firstBits >> 32),
UInt8(truncatingIfNeeded: firstBits >> 24),
UInt8(truncatingIfNeeded: firstBits >> 16),
UInt8(truncatingIfNeeded: firstBits >> 8),
UInt8(truncatingIfNeeded: firstBits),
UInt8(truncatingIfNeeded: secondBits >> 56),
UInt8(truncatingIfNeeded: secondBits >> 48),
UInt8(truncatingIfNeeded: secondBits >> 40),
UInt8(truncatingIfNeeded: secondBits >> 32),
UInt8(truncatingIfNeeded: secondBits >> 24),
UInt8(truncatingIfNeeded: secondBits >> 16),
UInt8(truncatingIfNeeded: secondBits >> 8),
UInt8(truncatingIfNeeded: secondBits)
)

return UUID(uuid: uuidBytes)
}

public var description: String {
return uuidString
Expand Down
60 changes: 60 additions & 0 deletions Tests/FoundationEssentialsTests/UUIDTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,64 @@ final class UUIDTests : XCTestCase {
XCTAssertFalse(uuid2 > uuid1)
XCTAssertTrue(uuid2 == uuid1)
}

func testRandomVersionAndVariant() {
var generator = SystemRandomNumberGenerator()
for _ in 0..<10000 {
let uuid = UUID.random(using: &generator)
XCTAssertEqual(uuid.versionNumber, 0b0100)
XCTAssertEqual(uuid.varint, 0b10)
}
}

func testDeterministicRandomGeneration() {
var generator = PCGRandomNumberGenerator(seed: 123456789)

let firstUUID = UUID.random(using: &generator)
XCTAssertEqual(firstUUID, UUID(uuidString: "9492BAC4-F353-49E7-ACBB-A40941CA65DE"))

let secondUUID = UUID.random(using: &generator)
XCTAssertEqual(secondUUID, UUID(uuidString: "392C44E5-EB3E-4455-85A7-AF9556722B9A"))

let thirdUUID = UUID.random(using: &generator)
XCTAssertEqual(thirdUUID, UUID(uuidString: "9ABFCCE9-AA85-485C-9CBF-C62F0C8D1D1A"))

let fourthUUID = UUID.random(using: &generator)
XCTAssertEqual(fourthUUID, UUID(uuidString: "2B29542E-F719-4D58-87B9-C6291ADD4541"))
}
}

extension UUID {
fileprivate var versionNumber: Int {
Int(self.uuid.6 >> 4)
}

fileprivate var varint: Int {
Int(self.uuid.8 >> 6 & 0b11)
}
}

fileprivate struct PCGRandomNumberGenerator: RandomNumberGenerator {
private static let multiplier: UInt128 = 47_026_247_687_942_121_848_144_207_491_837_523_525
private static let increment: UInt128 = 117_397_592_171_526_113_268_558_934_119_004_209_487

private var state: UInt128

fileprivate init(seed: UInt64) {
self.state = UInt128(seed)
}

fileprivate mutating func next() -> UInt64 {
self.state = self.state &* Self.multiplier &+ Self.increment

return rotr64(
value: UInt64(truncatingIfNeeded: self.state &>> 64) ^ UInt64(truncatingIfNeeded: self.state),
rotation: UInt64(truncatingIfNeeded: self.state &>> 122)
)
}

private func rotr64(value: UInt64, rotation: UInt64) -> UInt64 {
(value &>> rotation) | value &<< ((~rotation &+ 1) & 63)
}
}