ULIDの実装 - ulid-rsを読む
ビット演算なんもわからんから始めて解読していきます。
こちらのクレート。ULIDはTimeを表す48bitとランダムな80bitから作られる128bitのsortable identifier。
128-bit compatibility with UUID
1.21e+24 unique ULIDs per millisecond
Lexicographically sortable!
Canonically encoded as a 26 character string, as opposed to the 36 character UUID
Uses Crockford's base32 for better efficiency and readability (5 bits per character)
Case insensitive
No special characters (URL safe)
Monotonic sort order (correctly detects and handles the same millisecond)
https://github.com/ulid/spec
くわしくは上記リポジトリ。
たくさんの言語で実装されているが、今回はRustのulid-rsを読んでいきます。理由は、自分が読める言語であることと、なんとなくわかりやすそうだったので。目標は、別言語で自力で実装すること。
impl Ulid {
pub fn new() -> Ulid {
Ulid::from_datetime(SystemTime::now())
}
pub fn with_source<R: rand::Rng>(source: &mut R) -> Ulid {
Ulid::from_datetime_with_source(SystemTime::now(), source)
}
pub fn from_datetime_with_source<R>(datetime: SystemTime, source: &mut R) -> Ulid
where
R: rand::Rng,
{
let timestamp = datetime
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_millis();
let timebits = (timestamp & bitmask!(Self::TIME_BITS)) as u64;
let msb = timebits << 16 | u64::from(source.gen::<u16>());
let lsb = source.gen::<u64>();
Ulid::from((msb, lsb))
}
なので、実質Ulid::new()
= Ulid::from_datetime_with_source(SystemTime::now(), &mut rng)
。
use rand::prelude::*;
use std::time::SystemTime;
use ulid::Ulid;
fn main() {
let mut rng = StdRng::from_entropy();
let ulid = Ulid::from_datetime_with_source(SystemTime::now(), &mut rng);
println!("{}", ulid.to_string());
}
サンプルのmain関数を上記のようにして、git clone
したライブラリの中にprintln!
を埋め込みまくることで愚直に表示していく。
let timestamp = datetime
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_millis();
まずここでは、UNIX TIMEをmillisで取得している。取得できなければ0としている。
1662408777120 timestamp
let timebits = (timestamp & bitmask!(Self::TIME_BITS)) as u64;
このbitmask!
は何回も出てくるので、まずここから。
macro_rules! bitmask {
($len:expr) => {
((1 << $len) - 1)
};
}
integerをとり、1
をそのinteger分シフトして1を引く…というマクロ。デバッグしてみる。
let bitmask: u64 = ulid::bitmask!(40);
println!("{:b}", bitmask);
1111111111111111111111111111111111111111
つまり全てのbitが1の、任意の長さのビットマスクを作るマクロですね。
気をつけないといけないのは、u64
などと型をきちんと指定してあげないと、overflowでパニックを起こす点。
thread 'main' panicked at 'attempt to shift left with overflow', src/main.rs:9:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
型を指定しない場合、コンパイラがi32と推測してくるのでこうなってしまう。何桁ほしいのかをちゃんと指定すれば問題なし。
で、そのビットマスクとtimestampを&演算子でつないでいるので、timebitsというのは64bitで表されたtimestampということになる。
11000001100001111010100110101000100001001 timebits
ただしleading 0sは:b
だけでは表示されないので注意。
let msb = timebits << 16 | u64::from(source.gen::<u16>());
// println!("{:b} msb", msb);
let lsb = source.gen::<u64>();
// println!("{:b} lsb", lsb);
残りのここは最初何をやっているのかよくわからなかったのだけど、要するに、残りの80bit = randomnessを担保している部分を、16bitと64bitに分けて作って入れている、ということらしい。
impl From<(u64, u64)> for Ulid {
fn from((msb, lsb): (u64, u64)) -> Self {
let base = u128::from(msb) << 64 | u128::from(lsb);
Ulid(base)
}
}
そしてここに飛ぶ。
msbとlsbに分けているのは、それぞれをu64として扱うため(多分)。
まずmsbを左シフトしてから、u128として読み込んだlsbとOR演算子でつないで、完成形の128bitのULIDを生成している。
1100000110000111101010011010100010000100100100001010011000010110110100000111101100001101110011100100111110000001010001110 base
※leading 0sは非表示。
ULIDは通常01GC7N6M894562V87P3EE9Y0ME
といった形でよく見かけるが、これはbase32にエンコーディングされた見かけ。
このエンコード処理はbase32.rs
に定義されている。
pub fn encode_to(mut value: u128, buffer: &mut [u8]) -> Result<usize, EncodeError> {
// NOTE: This function can't be made const because mut refs aren't allowed for some reason
if buffer.len() < ULID_LEN {
return Err(EncodeError::BufferTooSmall);
}
for i in 0..ULID_LEN {
buffer[ULID_LEN - 1 - i] = ALPHABET[(value & 0x1f) as usize];
value >>= 5;
}
Ok(ULID_LEN)
}
引数のbufferは通常[u8, ULID_LEN]
(ULID_LEN = 26)として呼ばれるので、このエラーはほぼ発生しないタイプのエラーじゃないかなと思われる。
メインのforループについて。bufferの後ろから文字を詰めていくスタイル。
これをデバッグプリントしてみると…
for i in 0..ULID_LEN {
let v = (value & 0x1f) as usize;
println!("{:05b} v", v);
buffer[ULID_LEN - 1 - i] = ALPHABET[v];
value >>= 5;
}
1100000110000111101100011110110011101111111010110101011010111011011101001010100100001110010001101010101000010001010101000 base
01000 v
10101 v
01000 v
...
となる。つまり11111
と&でつなぐことで、右のほうから5桁ずつ切り出して5bitのusizeを取り出しているというわけですね。そのusizeにより、
const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
と定義されている使用文字から一つをとってくることで、エンコードしている。
たとえば01000
はusizeでいうと8で、ALPHABET[8]
は…8なので、このULIDの一番右の文字は
01GC7P7PEZTTPQDTAJ3J6N88N8
ちゃんと8になっている。なるほどー。
128bitなので最後のほうは足りてないということになるはずだが、それはあんまり気にしなくていいやつなんですかね。
Deno向けのシンプルなULID生成ライブラリを作ってみた。
Rustのようにu128
なんて便利な型が用意されているわけでもないので、最初はstring
型でtimestamp部分とrandom部分の疑似バイナリを扱っていたけれど、やっぱりなんか気持ちが良くないので、bigint
でなるべく処理するようにしたらまあまあコードがすっきりしたような気がします。
Rustコミュニティでたびたび触れられていたZigでも実装してみた。
Zig自体の感想としては"Better Go"。