転職したらスマレジだった件

スマレジのエンジニアやまてのテックブログです。マジレス大歓迎です。

Flutter|uuid パッケージでの UUID 生成手順とコードリーディング

スマレジのテックファーム(SES 部門)で Web 系エンジニアとして働いている やまて(@r_yamate) と申します。

2023 年 4 月からは、スマレジの関連アプリの開発業務を担当しています。触ったことのなかった Flutter での開発を担当することになり、日々奮闘中です。開発しているアプリはモニター利用も始まり、開発の醍醐味を味わえていて楽しいです。

はじめに

今回は、 Flutter を用いて UUID(Universally Unique Identifier)を生成する手順についてまとめます。 併せて UUID がどういったものか、uuid パッケージのコードを読んで UUID をどのように生成しているのかを確認します。

目次

環境

私が試した Flutter の環境は以下のとおりです。

step1. 依存パッケージの追加

pubspec.yaml への依存パッケージ追加

プロジェクトの pubspec.yaml ファイルに以下のように uuid パッケージを追加します。

dependencies:
  uuid: ^4.1.0

uuid パッケージは、その名の通り UUID を生成してくれるパッケージです。

pub.dev

その後、ターミナルで flutter pub get コマンドを実行して依存パッケージを取得します。

step2. コードの実装

パッケージのインポート

まず、 uuid パッケージをインポートします。

import 'package:uuid/uuid.dart';

UUID 生成の関数を作成

次に、以下のような UUID 生成の関数を作成します。

/// UUIDを生成する。
///
/// 生成されたUUIDとして文字列を返す。
String generateUuid() {
  var uuid = Uuid();
  return uuid.v4();
}

実行結果

生成される UUID は、以下のような形式になります。

'550e8400-e29b-41d4-a716-446655440000'

ところで… UUID とは

UUID(Universally Unique Identifier)の定義

UUID は、 Universally Unique Identifier の略で、直訳すると「全世界で一意な識別子」です。

UUID は、RFC 4122 に基づいて定義されています。

https://www.ietf.org/rfc/rfc4122.txt

UUID は「128 ビットの長さで、場所と時間を超えて一意性を保証することができる」という記載が RFC 4122 の概要にあります。

UUID の形式

UUID は 128 ビットの数値ですが、次の形式を持つ 16 進数の文字列として表現されます。

550e8400-e29b-41d4-a716-446655440000

なお、 RFC 4122 で定義されている UUID には現在 1 〜 5 のバージョンがあり、バージョンによって、生成方法が異なります。

例えば バージョン 1 については、以下の部分にハイフンで分けられています。

  • 550e8400 : 8文字 - タイムロー (time-low) の値。 UUID の時間の低 32 ビットを示しています。
  • e29b : 4文字 - タイムミッド (time-mid) の値。 UUID の時間の中 16 ビットを示しています。
  • 41d4 : 4文字 - タイムハイとバージョン (time-high-and-version) の値。 UUID の時間の高12ビットとバージョンを示しています。
  • a716 : 4文字 - クロックシーケンスハイとリザーブド (clock-seq-high-and-reserved) と クロックシーケンスロー (clock-seq-low) の値。この2つの情報が組み合わされています。
  • 446655440000 : 12文字 - ノード (node) の値。ノードは、つまり MAC アドレスを示しています。

UUID のバージョン

バージョンは、 UUID がどのように生成されるかを示しており、現在は以下の 5 つです。

  • バージョン 1 :現在のタイムスタンプと MAC アドレスに基づいて生成
    • TTTTTTTT-TTTT-1TTT-sSSS-AAAAAAAAAAAA
  • バージョン 2:DCE(分散計算環境)のセキュリティに関連
  • バージョン 3:名前ベースの UUID 。 MD5 ハッシュを使用
  • バージョン 4:ランダムまたは擬似ランダム数に基づいて生成
    • RRRRRRRR-RRRR-4RRR-rRRR-RRRRRRRRRRRR
  • バージョン 5:名前ベースの UUID 。 SHA-1 ハッシュを使用

システム開発でよく使われる UUID はバージョン 4

システム開発で一般的によく使われる UUID は、 バージョン 4 です。バージョン 4 の UUID がよく使われる理由は、指定されたビットを除いて完全にランダムな値で構成されているためです。

私は「現在のタイムスタンプや端末などから生成するから UUID は一意なんだろうな」とバージョン 1 のイメージで想像していたのですが、バージョン 4 は現在のタイムスタンプなど関係なく、完全にランダムな値で生成されるとのことでした。

参考リンク

www.cockroachlabs.com

パッケージのコードリーディング

uuid パッケージで、 UUID をどのように生成しているのかを確認します。

github.com

Uuidクラスの v4 メソッドを実行したときにどのように動作しているか、パッケージのコードを読んで確認します。

全体的なざっくりとしたプロセスとしては、初めにランダムな 16 バイトのデータが生成され、次にそのデータが UUID の仕様に従って修正されて、最終的にハイフンを含む文字列形式に変換されるというプロセスが行われます。

確認:lib/uuid.dart

class Uuid {

  // 〜省略〜 

    String v4(
        {@Deprecated('use config instead. Removal in 5.0.0')
        Map<String, dynamic>? options,
        V4Options? config}) {
      if (options != null && options.isNotEmpty) {
        var rng = options["rng"];
        if (options["rng"] != null && options["rng"] is! RNG) {
          rng = LegacyRNG(
              options["rng"], options["namedArgs"], options["positionalArgs"]);
        }
        config = V4Options(options["random"], rng);
      }
      return UuidV4(goptions: goptions).generate(options: config);
    }
  • Uuidクラスの v4 メソッドは、引数として optionsconfig を受け取ることができます。
  • options が指定されている場合、古い RNG 方式をサポートするための変換処理を行っています。 RNG は Random Number Generator の略で、ランダムな数値を生成する方式ということです。
  • 実際の UUID 生成処理を行う UuidV4 クラスの generate メソッドを呼び出して、結果の UUID 文字列を返しています。

※ 余談ですが、 Uuid クラスのメソッドに v6, v7, v8 もあり、v5 までじゃないの?となりましたが、RFC 4122 では v5 まで定義されていて、新しいバージョンが提案されているようです。

datatracker.ietf.org

確認:lib/v4.dart

import 'data.dart';
import 'parsing.dart';
import 'rng.dart';

class UuidV4 {
  final GlobalOptions? goptions;

  const UuidV4({this.goptions});

  String generate({V4Options? options}) {
    // Use the built-in RNG or a custom provided RNG
    List<int> rng = options?.rng?.generate() ??
        goptions?.rng?.generate() ??
        MathRNG().generate();

    // Use provided values over RNG
    List<int> rnds = options?.random ?? rng;

    // per 4.4, set bits for version and clockSeq high and reserved
    rnds[6] = (rnds[6] & 0x0f) | 0x40;
    rnds[8] = (rnds[8] & 0x3f) | 0x80;

    return UuidParsing.unparse(rnds);
  }
}
  • UuidV4 クラスの generate メソッドは、バージョン 4 の UUID を生成する役割を持っています。
  • このメソッドは、既定の MathRNG を使用するか、または提供されたカスタム RNG を使用して、 16 バイトのランダムな数値のリストを生成します。
  • 次に、このリストにいくつかのビット操作を適用して、 RFC 4122 の要件に従って UUID を生成します。

確認:lib/rng.dart

abstract class RNG {
  const RNG();

  Uint8List generate() {
    final uint8list = generateInternal();
    if (uint8list.length != 16) {
      throw Exception(
          'The length of the Uint8list returned by the custom RNG must be 16.');
    } else {
      return uint8list;
    }
  }

  Uint8List generateInternal();
}
  • RNG は Random Number Generator のインターフェースを定義する抽象クラスです。
  • このクラスの目的は、 UUID クラスがランダムな数値を生成する際に使用する RNG のインターフェースを提供することです。
  • generate メソッドは、具体的な RNG の実装による generateInternal メソッドを呼び出して、 Uint8List 型のランダムな数値のリストを取得します。さらに、この generate メソッドは取得したリストの長さが 16 であることを確認します。もし 16 でない場合、例外をスローします。
  • generateInternal メソッドは抽象メソッドとして定義されており、具体的な RNG の実装でオーバーライドされます。
class MathRNG extends RNG {
  static final _random = Random();
  final int seed;

  const MathRNG({this.seed = -1});

  @override
  Uint8List generateInternal() {
    final b = Uint8List(16);
    final rand = (seed == -1) ? _random : Random(seed);

    for (var i = 0; i < 16; i++) {
      b[i] = rand.nextInt(256);
    }

    return b;
  }
}
  • MathRNG クラスは、RNG クラスを継承しています。
  • _random は静的な Random インスタンスを保持しており、このクラス全体で共有される乱数生成器として利用されます。
  • seed は、乱数生成の初期値として利用することができる整数です。デフォルトは -1 です。
  • MathRNG のコンストラクタでは、オプションで seed を受け取ることができます。指定しない場合、デフォルトの -1 が使用されます。
  • generateInternal メソッドは、 16 バイトの Uint8List を生成して返します。
  • seed の値が -1 である場合、静的な _random インスタンスを利用して乱数を生成します。それ以外の場合、新しい Random インスタンスseed で初期化して利用します。
  • 16 バイトのリスト内の各バイトに対して、 0 から 255 の範囲のランダムな整数を割り当てます。
  • 最後に、生成された Uint8List を返します。

確認:lib/parsing.dart

class UuidParsing {
  /// Easy number -> hex conversion
  static final List<String> _byteToHex = List<String>.generate(256, (i) {
    return i.toRadixString(16).padLeft(2, '0');
  });

  // 〜省略〜 

  static String unparse(List<int> buffer, {int offset = 0}) {
    if (buffer.length - offset < 16) {
      throw RangeError('buffer too small: need 16: length=${buffer.length}'
          '${offset != 0 ? ', offset=$offset' : ''}');
    }
    var i = offset;
    return '${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}'
        '${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}-'
        '${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}-'
        '${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}-'
        '${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}-'
        '${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}'
        '${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}'
        '${_byteToHex[buffer[i++]]}${_byteToHex[buffer[i++]]}';
  }
}
  • unparse メソッドは、バイト配列を UUID の文字列形式に変換するメソッドです。このメソッドは、入力として与えられたバイト配列を 16 進数の文字列に変換して、 UUID の形式に従った文字列を返します。
  • _byteToHexi.toRadixString(16).padLeft(2, '0')という式は、整数 i を 16 進数の文字列に変換し、結果の文字列の長さが 2 未満の場合に左側を '0' で埋める処理を行っています。

api.flutter.dev

api.flutter.dev

おわりに

今回は、 Flutter を用いて UUID(Universally Unique Identifier)を生成する手順と、 併せて UUID がどういったものか、uuid パッケージで UUID をどのように生成しているのかについてまとめました。

ありがとうございました。



便利そうなので画像生成も使えるようになりたい。