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 1 commit
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 the last 12 bits
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the code wrong or the comment wrong? This appears to clear bits 12 ... 15.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment was wrong. This clears bits 48-51 of the UInt64 aligned with the RFC. Example below

10010100 10010010 10111010 11000100 11110011 01010011 00101001 11100111
10010100 10010010 10111010 11000100 11110011 01010011 00001001 11100111

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
19 changes: 19 additions & 0 deletions Tests/FoundationEssentialsTests/UUIDTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,23 @@ 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)
}
}
}

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

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