Ориентация главы

Случайные возможности XRT делятся на четыре слоя, у каждого своё место: слой Secure идёт к источнику энтропии операционной системы (xrtSecureRandom — только для ключей и токенов); слой глобального Rand — потоколокальное умолчание; слой Fast ставит скорость вперёд, не безопасность; слой экземпляров RNG сеется явно и полностью воспроизводим (xrng). Глава проясняет выбор слоя и пришпиливает одну красную линию: PCG и семейство Fast не криптографически стойки — токены, ключи и nonce обязаны идти через слой Secure. Воспроизводимость на фиксированном сеянии — другая тема: снапшоты тестов и системы повторов стоят на «одно семя — одна последовательность».

11.1引入

Два внешне противоречивых требования сосуществуют. В играх и симуляции вам нужна случайность воспроизводимая — тот же матч, тот же сейв при загрузке, последовательность случайных событий обязана совпадать, иначе запись-повтор и сетевая синхронизация отказывают целиком; в безопасности вам нужна случайность непредсказуемая — угаданный сессионный токен равен угнанному аккаунту. Эти два дела инженерно несут разные механизмы: первое — явно сеянный псевдослучайный генератор (одно семя — одна последовательность), второе — источник энтропии ОС (xrtSecureRandom).

Частая ошибка — смешивать их: посеять быстрый генератор временем и генерировать токены — время угадываемо, генератор предсказуем, два слоя защиты дают ноль. XRT режет этот класс ошибок явными именами: Rand/FastRand/Rng с первого взгляда статистические, SecureRandom/SecureText с первого взгляда охранные, а документация модуля прямо гласит «граница безопасности обязана использовать Secure». Имя есть документация — на ревью кода не нужно лезть в реализацию; одно имя функции завершает половину охранного аудита.

11.2概念

Четырёхслойная система и выбор

1SecurexrtSecureRandom / SecureText, энтропия ОС — единственный легитимный источник ключей, токенов, nonce
2Глобальный RandxrtRand64 и др., потоколокальное умолчание — бизнес-выборки, демо, имитация нагрузки
3FastxrtFastRandSeed и др., скорость вперёд не безопасно — впрыск дрожания в горячих путях, графический шум
4Экземпляр RNGxrng + посев, явное воспроизводимое состояние — снапшоты тестов, запись-повтор, параллельные независимые потоки

Выбор задаёт только два вопроса: нужна непредсказуемость? Да → 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 — фиксированное семя, без глобального состояния, вывод полностью воспроизводим:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/math/random/main.c -lws2_32 -liphlpapiexplicit: 4207372542dice : 6real : 0.219369470187bytes : 8641shuffle : 2 1 6

Что только что произошло. (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 — по слою на проход с утверждениями воспроизводимости и ограниченности:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/math/random_tour/main.c -lws2_32 -liphlpapirandom: global rand64/below/range/bytes/shuffle okrandom: fast (seeded) 32/64/below/range/real/shuffle okrandom: rng instance ready=1 below32/64/range/text okrandom: secure text from alphabet len=8

Что только что произошло. (1) Пятёрка глобального слоя: прямые вызовы внутри потока, без объявления состояния. (2) Слой Fast сеет xrtFastRandSeed и берёт значения — быстро, но не безопасно; комментарий заголовка пришпиливает границу. (3) Слой экземпляров RNG проверяет RngReady и ограниченные версии; (4) слой Secure генерирует 8-символьный токен из пользовательского алфавита — прохождение ассерта длины завершает (содержимое, разумеется, каждый раз новое). Вся программа проверяет воспроизводимость через «одно семя — две генерации — идентичные результаты» и ограниченность ассертами диапазона — те самые два класса утверждений, которые стоит копировать в собственные случайные тесты.

11.4契约

  • Красная линия безопасности: ключи, токены, пароли, nonce, временные имена ресурсов и вся граница безопасности обязаны использовать xrtSecureRandom / SecureText; Rng/Rand/FastRand — всё статистическое, заявлено вдвойне документацией и именованием.
  • Контракт воспроизводимости: одно семя (с номером потока) и одна последовательность вызовов → один вывод, стабильно кроссплатформенно и межверсионно (согласованность xrtMath поддерживает снизу).
  • Независимые потоки: много экземпляров на разных номерах потока взаимно независимы; одно семя и один поток = полностью коррелированная копия — перед параллельной симуляцией сверьте строки посева.
  • Потоки: глобальный Rand потоколокален, конкурентность без блокировок; экземпляр RNG принадлежит области видимости, объявившей его, межпоточное разделение требует своей синхронизации.
  • Семантика интервалов: RngRangeClosed замкнуто-замкнут (включая оба конца); самописное x % n несёт смещение остатка — не надо.
  • Порядок есть состояние: воспроизводя последовательность, виды и порядок генерационных вызовов обязаны точно совпадать с записью.

Типичные ошибки

Ловушка 1: генерация охранных токенов статистической случайностью:Ловушка 1: генерация охранных токенов статистической случайностью

Симптомы: аудит безопасности помечает токены предсказуемыми; в принципе атакующему достаточно наблюдать немного выходов, чтобы восстановить внутреннее состояние и вычислить все последующие токены.

Причина: 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) */
Ловушка 2: два RNG на одном семени:Ловушка 2: два RNG на одном семени

Симптомы: в параллельной симуляции множество «независимых» случайных потоков сильно коррелированы — порядки колод одинаковы, паттерны нагрузки синхронны, статистические выводы искажены.

Причина: одно семя и один номер потока дают в точности одну последовательность; «много экземпляров» само по себе не даёт «много последовательностей».

错误写法
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 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки