Ориентация главы
Случайные возможности XRT делятся на четыре слоя, у каждого своё место: слой Secure идёт к источнику энтропии операционной системы (xrtSecureRandom — только для ключей и токенов); слой глобального Rand — потоколокальное умолчание; слой Fast ставит скорость вперёд, не безопасность; слой экземпляров RNG сеется явно и полностью воспроизводим (xrng). Глава проясняет выбор слоя и пришпиливает одну красную линию: PCG и семейство Fast не криптографически стойки — токены, ключи и nonce обязаны идти через слой Secure. Воспроизводимость на фиксированном сеянии — другая тема: снапшоты тестов и системы повторов стоят на «одно семя — одна последовательность».
11.1引入
Два внешне противоречивых требования сосуществуют. В играх и симуляции вам нужна случайность воспроизводимая — тот же матч, тот же сейв при загрузке, последовательность случайных событий обязана совпадать, иначе запись-повтор и сетевая синхронизация отказывают целиком; в безопасности вам нужна случайность непредсказуемая — угаданный сессионный токен равен угнанному аккаунту. Эти два дела инженерно несут разные механизмы: первое — явно сеянный псевдослучайный генератор (одно семя — одна последовательность), второе — источник энтропии ОС (xrtSecureRandom).
Частая ошибка — смешивать их: посеять быстрый генератор временем и генерировать токены — время угадываемо, генератор предсказуем, два слоя защиты дают ноль. XRT режет этот класс ошибок явными именами: Rand/FastRand/Rng с первого взгляда статистические, SecureRandom/SecureText с первого взгляда охранные, а документация модуля прямо гласит «граница безопасности обязана использовать Secure». Имя есть документация — на ревью кода не нужно лезть в реализацию; одно имя функции завершает половину охранного аудита.
11.2概念
Четырёхслойная система и выбор
Выбор задаёт только два вопроса: нужна непредсказуемость? Да → Secure, конец. Нужна воспроизводимость? Да → экземпляр RNG (явное состояние, явное семя). Ни то ни другое → глобальный Rand ради удобства, Fast — для подтверждённых чувствительных к производительности горячих путей. Префиксы имён четырёх слоёв взаимооднозначны этим ответам — на ревью видно с первого взгляда.
Добавьте список-ощущение «какой слой для какого сценария»: генерация сессионных токенов → SecureText; броски выпадения в игре → экземпляр RNG (требование повтора); возмущение трафика в нагрузочном тестировании → глобальный Rand; шум графических частиц → Fast; временные имена файлов → SecureText или XID (глава 12); точки внедрения сбоев в тестах → экземпляр RNG (семя есть номер тест-кейса). Список растёт с проектом, но каждый новый пункт обязан отвечать на один из тех двух вопросов.
Экземпляр RNG: явное состояние и номер потока
xrng — структура состояния, которую можно положить куда угодно: стековую переменную, поле структуры, кусок байтов в файле сохранения. xrtRngSeed(&Rng, 种子, 流序号) сеет (семя, номер потока — дословные токены ниже): одно семя, разные номера потока дают взаимно независимые последовательности. Этот второй параметр решает классическую ловушку: два генератора на одном семени дают полностью коррелированный вывод — когда сервер выдаёт каждой сессии по RNG, номер сессии как номер потока гарантирует нескоррелированность. Семейство генерации покрывает целые (Rng32/Rng64), плавающие (RngReal, [0,1)), ограниченные целые (RngRangeClosed, замкнуто-замкнутый интервал — бросок кости 1..6 включая 6), заполнение байт (RngBytes) и тасование на месте (RngShuffle, Фишер-Йетс).
Глобальный слой и потоки
Глобальные функции вроде xrtRand64 используют потоколокальное состояние — конкурентные вызовы из многих потоков без блокировок и без взаимных помех последовательностям. Слой подходит под «быстро дайте случайное число»; когда последовательность должна быть согласована между вызовами (одна и та же логика при перезапуске — тот же результат), возвращайтесь к экземпляру RNG. Семейство Fast (xrtFastRandSeed для посева, xrtFastRand32 для выборки и др.) — сперва-скоростная реализация без охранных гарантий, для подтверждённых экстремально-производительных неохранных сценариев.
Случайный текст и алфавит
RandText / RandStringFrom / SecureText генерируют случайные строки: можно задать набор символов (например, только однозначные 23456789abcdefghjkmnpqrstuvwxyz) для приглашительных кодов, временных имён каталогов и сессионных токенов. Охранным сценариям соответствуют SecureText и SecureStringFrom — они стоят на xrtSecureRandom, и непредсказуемость случайного текста тождественна байтовой.
Пользовательский алфавит — не только красота: исключая похожие 0O1lI, приглашительные коды перестают ошибочно переписываться пользователями; ограничиваясь URL-безопасным набором, токен входит прямо в ссылки и QR-коды. Длина генерации задаётся вызывающим, а неудача (источник энтропии недоступен) так же объявляется возвращаемым значением — неудачи на охранном пути никогда не глотаются молча.
Инженерные применения воспроизводимости
У воспроизводимой случайности три частых места приземления. Снапшоты тестов: семя записывается в фикстуру теста, ассертится хеш сгенерированной последовательности (глава 12) против записанного отпечатка — регрессионное тестирование случайной логики из «на удачу» становится точным утверждением. Запись-повтор: хранится только семя и последовательность операций; при повторе генерационные вызовы исполняются заново, случайные события покадрово совпадают. Внедрение сбоев: RNG решает точки внедрения (в паре с FailAfter главы 6 для смешанных режимов отказов), семя — номер тест-кейса: повтор неудачного прогона не меняет результат. Общая дисциплина трёх мест: порядок генерационных вызовов — тоже состояние — пропустите один Rng32 при воспроизведении, и всё после сдвинется.
Чтение трёх классов утверждений случайных тестов
Перед прогоном примеров зафиксируем, как тестируется случайный код — оба примера ассертят из этих трёх классов. Утверждения воспроизводимости: одна и та же генерация дважды с одним семенем — результаты поэлементно равны (или равны хеш-отпечатки). Утверждения ограниченности: значения попадают в заявленный интервал, включая оба конца замкнутого интервала, каждый минимум по разу. Утверждения распределения: на больших выборках частоты близки к ожиданию (Near главы 10 по относительному отклонению). Три класса каждый на своём посту: воспроизводимость гарантирует определённость, ограниченность — семантику, распределение — качество; писав только один класс, вы упускаете дефекты соответствующего измерения.
11.3示例
Полная программа: всё семейство явного RNG
Из примера репозитория examples/math/random/main.c — фиксированное семя, без глобального состояния, вывод полностью воспроизводим:
*/ #include <stdio.h> #include <xrt.h> int main(void) { xrng Rng; /* 状态在栈上:零全局依赖 */ int arrOrder[] = { 1, 2, 3, 4, 5, 6 }; uint8 arrBytes[8]; xrtRngSeed(&Rng, 2026, 7); /* 种子 2026,流 7 */ /* 整数与浮点:同一状态顺序推进(消耗即前进)。 */ printf("explicit: %u\n", (unsigned int)xrtRng32(&Rng)); printf("dice : %lld\n", (long long)xrtRngRangeClosed(&Rng, 1, 6)); printf("real : %.12f\n", xrtRngReal(&Rng)); /* 字节填充与洗牌:RngBytes 无失败路径(内部检查冗余但保留统一风格)。 */ if ( !xrtRngBytes(&Rng, arrBytes, sizeof(arrBytes)) || !xrtRngShuffle(&Rng, arrOrder, sizeof(arrOrder) / sizeof(arrOrder[0]), sizeof(arrOrder[0])) ) { return 1; } printf("bytes : %02x%02x\n", (unsigned int)arrBytes[0], (unsigned int)arrBytes[1]); printf("shuffle : %d %d %d\n", arrOrder[0], arrOrder[1], arrOrder[2]); return 0; }
Что только что произошло. (1) xrng Rng — локальное состояние на стеке: кто объявил, тот и держит; положить в структуру, по одному на сессию, сохранить в файле повтора — одна и та же форма. (2) xrtRngSeed(&Rng, 2026, 7): семя 2026, номер потока 7; эти полсотни байт последовательности отныне полностью определены — любая машина в любое время даст эти пять строк вывода — краеугольный камень снапшотов тестов и записи-повтора. (3) Пять генерационных функций последовательно продвигают одно состояние: взятое раньше целое влияет на брошенную позже кость — воспроизводя последовательность, порядок вызовов тоже обязан совпадать. (4) RngRangeClosed(&Rng, 1, 6) — замкнуто-замкнутый интервал; броску кости больше не нужно 1 + x % 6 (двойная ловушка полузамкнутого интервала плюс смещение остатка). (5) RngShuffle — тасование на месте: массив всё тот же, порядок равномерно перемешан Фишером-Йетсом; «равномерно» не риторика — каждая перестановка равновероятна, и это объект проверки распределения в упражнениях главы.
Полная программа: обзор интерфейсов всех четырёх слоёв
Из examples/math/random_tour/main.c — по слою на проход с утверждениями воспроизводимости и ограниченности:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> #define SV(x) XRT_STR_LITERAL(x) /* 字母表断言:str 全部字符都在表内且长度正确。 */ static bool exampleInAlphabet(cstr sText, size_t iLength, xstrview Alphabet) { size_t i; size_t j; if ( strlen(sText) != iLength ) { return false; } for ( i = 0; i < iLength; ++i ) { bool bFound = false; for ( j = 0; j < Alphabet.Size; ++j ) { if ( sText[i] == Alphabet.Data[j] ) { bFound = true; break; } } if ( !bFound ) { return false; } } return true; } int main(void) { static const char sHex[] = "0123456789abcdef"; xrng RngA; xrng RngB; char Buffer[16]; char TextA[9]; char TextB[9]; uint8 BytesA[8]; uint8 BytesB[8]; str sGenerated; int i; int iResult = 1; /* ---- 全局线程层:有界性 + 洗牌保持多重集 ---- */ (void)xrtRand64(); if ( (xrtRandBelow(10u) >= 10u) || (xrtRandBelow(1u) != 0u) ) { goto Cleanup; } { int64 iMin = 100; int64 iMax; for ( i = 0; i < 64; ++i ) { iMax = xrtRandRange(-5, 5); if ( (iMax < -5) || (iMax > 5) || (iMax == iMin) ) { iMin = iMax; } } } if ( !xrtRandBytes(Buffer, sizeof(Buffer)) ) { goto Cleanup; } { /* 洗牌 0..5:结果必须是原多重集的排列。 */ int Numbers[6] = { 0, 1, 2, 3, 4, 5 }; bool bSeen[6] = { false }; bool bPermutation = true; if ( !xrtRandShuffle(Numbers, 6u, sizeof(int)) ) { goto Cleanup; } for ( i = 0; i < 6; ++i ) { if ( (Numbers[i] < 0) || (Numbers[i] > 5) || bSeen[Numbers[i]] ) { bPermutation = false; break; } bSeen[Numbers[i]] = true; } if ( !bPermutation ) { goto Cleanup; } } /* RandText:按字母表填充固定缓冲;RandStringFrom 分配。 */ if ( !xrtRandText(SV(sHex), TextA, sizeof(TextA), 8u) || !exampleInAlphabet(TextA, 8u, SV(sHex)) ) { goto Cleanup; } sGenerated = xrtRandStringFrom(SV(sHex), 16u); if ( (sGenerated == NULL) || !exampleInAlphabet(sGenerated, 16u, SV(sHex)) ) { goto Cleanup; } xrtFree(sGenerated); /* RandString:URL-safe 64 字符表(自定义表见 StringFrom)。 */ sGenerated = xrtRandString(12u); if ( (sGenerated == NULL) || (strlen(sGenerated) != 12u) ) { goto Cleanup; } xrtFree(sGenerated); printf("random: global rand64/below/range/bytes/shuffle ok\n"); /* ---- Fast 层:同种子可复现 ---- */ xrtFastRandSeed(12345u, 67890u); (void)xrtFastRand32(); (void)xrtFastRand64(); { /* 每个生成器只消费一次:回放序列必须完全对齐。 */ int64 iRange = xrtFastRandRange(-3, 3); if ( (xrtFastRandBelow(7u) >= 7u) || (iRange < -3) || (iRange > 3) || (xrtFastRandRangeClosed(1, 1) != 1) ) { goto Cleanup; } } { double dReal = xrtFastRandReal(); if ( (dReal < 0.0) || (dReal >= 1.0) ) { goto Cleanup; } } if ( !xrtFastRandBytes(BytesA, sizeof(BytesA)) ) { goto Cleanup; } { int Numbers[4] = { 0, 1, 2, 3 }; if ( !xrtFastRandShuffle(Numbers, 4u, sizeof(int)) ) { goto Cleanup; } } /* 重置同一种子:32/64/字节序列必须完全一致。 */ xrtFastRandSeed(12345u, 67890u); (void)xrtFastRand32(); (void)xrtFastRand64(); (void)xrtFastRandBelow(7u); (void)xrtFastRandRange(-3, 3); (void)xrtFastRandRangeClosed(1, 1); (void)xrtFastRandReal(); if ( !xrtFastRandBytes(BytesB, sizeof(BytesB)) || (memcmp(BytesA, BytesB, sizeof(BytesA)) != 0) ) { goto Cleanup; } printf("random: fast (seeded) 32/64/below/range/real/shuffle ok\n"); /* ---- RNG 实例层:Ready 门 + Below/Range/Text ---- */ xrtRngSeed(&RngA, 42u, 7u); xrtRngSeed(&RngB, 42u, 7u); if ( !xrtRngReady(&RngA) || (xrtRngReady(NULL)) ) { goto Cleanup; } /* 双实例同种子:Rng64/RngBelow/RngRange 逐个一致。 */ for ( i = 0; i < 8; ++i ) { if ( xrtRng64(&RngA) != xrtRng64(&RngB) ) { goto Cleanup; } } { /* 双实例逐调用同步消费,任何一次额外抽取都会失步。 */ uint32 uA = xrtRngBelow32(&RngA, 100u); uint32 uB = xrtRngBelow32(&RngB, 100u); if ( (uA != uB) || (uA >= 100u) ) { goto Cleanup; } uA = xrtRngBelow32(&RngA, 100u); uB = xrtRngBelow32(&RngB, 100u); if ( (uA != uB) || (uA >= 100u) ) { goto Cleanup; } { uint64 uA = xrtRngBelow64(&RngA, UINT64_C(1000000)); uint64 uB = xrtRngBelow64(&RngB, UINT64_C(1000000)); if ( (uA != uB) || (uA >= UINT64_C(1000000)) ) { goto Cleanup; } } } { int64 iA = xrtRngRange(&RngA, -100, 100); int64 iB = xrtRngRange(&RngB, -100, 100); if ( (iA != iB) || (iA < -100) || (iA > 100) ) { goto Cleanup; } } /* RngText 固定缓冲 + RngStringFrom 分配(同种子双实例一致)。 */ if ( !xrtRngText(&RngA, SV(sHex), TextA, sizeof(TextA), 8u) || !xrtRngText(&RngB, SV(sHex), TextB, sizeof(TextB), 8u) || (memcmp(TextA, TextB, 8u) != 0) || !exampleInAlphabet(TextA, 8u, SV(sHex)) ) { goto Cleanup; } sGenerated = xrtRngStringFrom(&RngA, SV(sHex), 12u); if ( (sGenerated == NULL) || !exampleInAlphabet(sGenerated, 12u, SV(sHex)) ) { goto Cleanup; } xrtFree(sGenerated); printf("random: rng instance ready=1 below32/64/range/text ok\n"); /* ---- Secure:字母表定制的文本生成 ---- */ sGenerated = xrtSecureStringFrom(SV(sHex), 8u); if ( (sGenerated == NULL) || !exampleInAlphabet(sGenerated, 8u, SV(sHex)) ) { goto Cleanup; } xrtFree(sGenerated); if ( !xrtSecureText(SV(sHex), TextA, sizeof(TextA), 8u) || !exampleInAlphabet(TextA, 8u, SV(sHex)) ) { goto Cleanup; } printf("random: secure text from alphabet len=8\n"); iResult = 0; Cleanup: return iResult; }
Что только что произошло. (1) Пятёрка глобального слоя: прямые вызовы внутри потока, без объявления состояния. (2) Слой Fast сеет xrtFastRandSeed и берёт значения — быстро, но не безопасно; комментарий заголовка пришпиливает границу. (3) Слой экземпляров RNG проверяет RngReady и ограниченные версии; (4) слой Secure генерирует 8-символьный токен из пользовательского алфавита — прохождение ассерта длины завершает (содержимое, разумеется, каждый раз новое). Вся программа проверяет воспроизводимость через «одно семя — две генерации — идентичные результаты» и ограниченность ассертами диапазона — те самые два класса утверждений, которые стоит копировать в собственные случайные тесты.
11.4契约
- Красная линия безопасности: ключи, токены, пароли, nonce, временные имена ресурсов и вся граница безопасности обязаны использовать
xrtSecureRandom/SecureText;Rng/Rand/FastRand— всё статистическое, заявлено вдвойне документацией и именованием. - Контракт воспроизводимости: одно семя (с номером потока) и одна последовательность вызовов → один вывод, стабильно кроссплатформенно и межверсионно (согласованность
xrtMathподдерживает снизу). - Независимые потоки: много экземпляров на разных номерах потока взаимно независимы; одно семя и один поток = полностью коррелированная копия — перед параллельной симуляцией сверьте строки посева.
- Потоки: глобальный Rand потоколокален, конкурентность без блокировок; экземпляр RNG принадлежит области видимости, объявившей его, межпоточное разделение требует своей синхронизации.
- Семантика интервалов:
RngRangeClosedзамкнуто-замкнут (включая оба конца); самописноеx % nнесёт смещение остатка — не надо. - Порядок есть состояние: воспроизводя последовательность, виды и порядок генерационных вызовов обязаны точно совпадать с записью.
Типичные ошибки
Симптомы: аудит безопасности помечает токены предсказуемыми; в принципе атакующему достаточно наблюдать немного выходов, чтобы восстановить внутреннее состояние и вычислить все последующие токены.
Причина: PCG и семейство Fast — статистические; воспроизводимость и непредсказуемость проектно противоположны — первая обрекает вторую.
xrng Rng; xrtRngSeed(&Rng, (uint64)time(NULL), 1); /* время угадываемо */ xrtRngBytes(&Rng, Token, sizeof(Token)); /* PCG предсказуем — токен для вида */
uint8 Token[16]; if ( !xrtSecureRandom(Token, sizeof(Token)) ) { return false; /* отказ энтропии обрабатывается как отказ */ } /* по завершении xrtSecureZero(Token, sizeof(Token)) (глава 5) */
Симптомы: в параллельной симуляции множество «независимых» случайных потоков сильно коррелированы — порядки колод одинаковы, паттерны нагрузки синхронны, статистические выводы искажены.
Причина: одно семя и один номер потока дают в точности одну последовательность; «много экземпляров» само по себе не даёт «много последовательностей».
for ( i = 0; i < 4; ++i ) { xrtRngSeed(&Workers[i].Rng, 2026, 1); /* четыре воркера — одна последовательность */ }
for ( i = 0; i < 4; ++i ) { xrtRngSeed(&Workers[i].Rng, 2026, (uint64)i + 1u); /* номера потока разделяют течения */ }
Практические задания
Возьмите десять целых подряд с фиксированным семенем и напечатайте; перегоните с другим номером потока, сравните — последовательности различны; верните исходное семя и перегоните, ассертя поэлементное совпадение с первым разом. Напоследок снимите отпечаток десятки через Hash64 главы 12 — будущая регрессия этого теста сверяет одно 64-битное значение.
Бросьте RngRangeClosed(&Rng, 1, 6) сто тысяч раз, просуммируйте частоты 1–6 и проверьте Near главы 10, что относительное отклонение каждой грани от ожидания (сто тысяч на шесть) меньше одного процента. Затем контроль: нарочно генерируйте через 1 + (int)(xrtRng32(&Rng) % 6) и посмотрите, приемлемо ли отклонение всё ещё — прочувствуйте, является ли «смещение остатка» реальной проблемой в малых диапазонах, и запишите вывод.
Спроектируйте минимальный повтор матча: семя открытия (с номером потока) и последовательность действий игрока сохраняются; «повтор» исполняет семя и действия заново, ассертя каждое случайное событие (каждое выпадение, каждый бросок) в поэлементном согласии с живой записью. Критерий приёмки: сейвы двух разных семён повторяются каждый самосогласованно и различно между собой; один и тот же сейв дважды даёт поэлементно одинаковый результат.
Итоги
| Тема | Краткая справка |
|---|---|
| Четыре слоя | Secure (энтропия) / глобальный Rand (потоколокальный) / Fast (скорость) / экземпляр RNG (воспроизводимость) |
| Красная линия | токены, ключи, nonce — только SecureRandom / SecureText |
| Посев | RngSeed(&Rng, 种子, 流序号); разделение течений номером потока |
| Интервал | RngRangeClosed замкнуто-замкнут с концами; x % n отвергнуто |
| Воспроизведение | одно семя и порядок вызовов → один вывод; порядок вызовов — тоже состояние |
| Текст | SecureStringFrom генерирует токены из пользовательского алфавита; исключение похожих символов бережёт от ошибочного переписывания |
📖 Справочники API в этой главе:random (41 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки