Skip to content

ULID Generator — C source

Generate Universally Unique Lexicographically Sortable Identifiers (ULID) - 26-character Crockford-base32 strings that sort by millisecond timestamp. Paste any ULID to decode its timestamp and randomness. Runs entirely in your browser.

This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.

/* ulid-generator - 26-char Crockford-base32 sortable ID: 10 chars of 48-bit ms timestamp + 16 chars of 80-bit randomness. Language: C (C99, stdlib only). Port of src/lib/ulid.ts (Go twin: cli/ulid-generator) - encode/generate/decode-time core; inspectUlid (date/random-hex view) lives in this dir's javascript.js (80-line budget). */
#include <stdint.h>
#include <stdio.h>
#include <string.h>

static const char CROCKFORD[33] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; /* no I, L, O, U */

/* Divide the big-endian value in b (mutated in place) by 32; return the
   remainder 0-31. Long division in base 256, exactly as in the Go twin. */
static int div_by_32(uint8_t *b, int len) {
    int rem = 0;
    for (int i = 0; i < len; i++) {
        int cur = (rem << 8) | b[i];
        b[i] = (uint8_t)(cur >> 5);
        rem = cur & 0x1F;
    }
    return rem;
}

/* Base32-encode the 128-bit big-endian [6 time bytes | 10 random bytes] into
   the canonical 26 chars. Because 32^16 == 2^80, the first 10 chars equal the
   TS/Go twins' separate time encoding - the cross-language lock-step anchor. */
static void encode_ulid(const uint8_t time_b[6], const uint8_t rnd_b[10], char out[27]) {
    uint8_t b[16];
    memcpy(b, time_b, 6);
    memcpy(b + 6, rnd_b, 10);
    for (int i = 25; i >= 0; i--) out[i] = CROCKFORD[div_by_32(b, 16)];
    out[26] = '\0';
}

/* 48-bit ms timestamp -> 6 big-endian bytes (MSB first), like TS timeBytes(). */
static void time_bytes(uint64_t ms, uint8_t out[6]) {
    for (int i = 5; i >= 0; i--, ms >>= 8) out[i] = (uint8_t)(ms & 0xFF);
}

/* Demo RNG: xorshift64*. C's stdlib has no CSPRNG - production code wants
   getrandom(2)/arc4random_buf, the analogue of Go's crypto/rand. */
static uint64_t rng_state = 88172645463325252ULL;
static uint8_t rng_byte(void) {
    rng_state ^= rng_state >> 12; rng_state ^= rng_state << 25; rng_state ^= rng_state >> 27;
    return (uint8_t)((rng_state * 2685821657736338717ULL) >> 56);
}

/* Extract the 48-bit ms timestamp from the first 10 chars; -1 on malformed
   input (wrong length, or a char outside the alphabet such as I/L/O/U). */
static int decode_ulid_time(const char *id, uint64_t *out) {
    if (strlen(id) != 26) return -1;
    uint64_t ts = 0;
    for (int i = 0; i < 10; i++) {
        const char *p = strchr(CROCKFORD, id[i]);
        if (p == NULL) return -1;
        ts = ts * 32 + (uint64_t)(p - CROCKFORD);
    }
    *out = ts;
    return 0;
}

int main(void) {
    uint8_t tb[6], rb[10];
    time_bytes(1722000000000ULL, tb); /* fixed epoch-ms so the time chars are deterministic */
    for (int i = 0; i < 10; i++) rb[i] = rng_byte();
    char ulid[27];
    encode_ulid(tb, rb, ulid);
    printf("ulid:     %s\n", ulid);
    uint64_t ms;
    if (decode_ulid_time(ulid, &ms) == 0)
        printf("time(ms): %llu  (round-trip ok)\n", (unsigned long long)ms);
    return 0;
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →