This is an old revision of the document!
PHP RFC: Add Random class and RandomNumberGenerator intreface
- Version: 1.1
- Date: 2021-05-18
- Author: Go Kudo zeriyoshi@gmail.com
- Status: Under Discussion
- Implementation: https://github.com/php/php-src/pull/7079
- First Published at: http://wiki.php.net/rfc/object_scope_prng
Introduction
PHP is currently having problems with RNG reproducibility.
PHP's RNG has been unified into an implementation using the Mersenne twister, with the rand() and srand() functions becoming aliases for mt_rand() and mt_srand() respectively in PHP 7.1.
But, these functions still store the state in the global state of PHP and are not easily reproducible. Look at the following example.
echo foo(1234, function (): void {}) . PHP_EOL; // Result: 1480009472 echo foo(1234, function (): void { mt_rand(); }) . PHP_EOL; // Result: 1747253290 function foo(int $seed, callable $bar): int { mt_srand($seed); $result = mt_rand(); $bar(); $result += mt_rand(); return $result; }
As mentioned above, the reproducibility of random numbers can easily be lost if additional processing is added later.
In addition, the fiber extension was introduced in PHP 8.1. This makes it more difficult to keep track of the execution order. However, this problem has existed since the introduced of Generator.
There is also the problem of functions that implicitly use the state stored in PHP's global state. shuffle(), str_shuffle(), and array_rand() functions implicitly advance the state of a random number. This means that the following code is not reproducible, but it is difficult for the user to notice this.
mt_srand(1234); echo mt_rand() . PHP_EOL; // Result: 411284887 mt_srand(1234); str_shuffle('foobar'); echo mt_rand() . PHP_EOL; // Result: 1314500282
Proposal
Implements Random class and RandomNumberGenerator interface.
These class / interface implement in ext/standard, always bundled PHP core.
The PHP code that represents the implementation is as follows:
const RANDOM_XORSHIFT128PLUS = 'xorshift128plus'; // 64-bit, fast, default const RANDOM_MT19937 = 'mt19937'; // 32-bit, for backward compatibility const RANDOM_SECURE = 'secure'; // Cryptographically-Secure PRNG interface RandomNumberGenerator { /** * Generate a number. */ public function generate(): int; } final class Random { private ?RandomNumberGenerator $rng; /** * Get registered algorithm string in array. */ public static function getAlgos(): array; /** * Get algorithm information in array. * if not registered, returns null. */ public static function getAlgoInfo(string $algo): ?array; /** * Constructor. * if $seed is null, generating seed by php_random_bytes(), but whether to use depends to algo. */ public function __construct(string|RandomNumberGenerator $algo = RANDOM_XORSHIFT128PLUS, ?int $seed = null) {} /** * Returns raw generated number by RNG. * if on the 32-bit machine, least 32-bit will be truncated. */ public function nextInt(): int {} /** * Generates a number within a given range. * similar random_int() function. */ public function getInt(int $min, int $max): int {} /** * Generates a string within a given range. * similar random_bytes() function. */ public function getBytes(int $length): string {} /** * Shuffling the given array items. * similar shuffle(), but non-pass-by-reference. */ public function shuffleArray(array $array): array {} /** * Shuffling the given string characters. * similar str_shuffle(). */ public function shuffleString(string $string): string {} /** * Serializing state to string if algo's supported. */ public function __serialize(): array {} /** * Unseriialize state from string in algo's supported. */ public function __unserialize(array $data): void {} }
This class retrieves and uses the RNG algorithm registered in the core, based on the string passed in the constructor argument $algo.
The bundled RNGs are as follows:
- XorShift128+: 64-bit, reproducible, PRNG.
- MT19937: 32-bit, reproducible, PRNG, compatible mt_srand() / mt_rand().
- secure: 64-bit, non-reproducible, CSPRNG, uses php_random_bytes() internally.
By default, XorShift128+ is used. It can generate 64-bit values, is used by major browsers, and is fast and reliable. On the other hand, MT19937 is retained for compatibility.
“secure” is practically equivalent to random_int(), but can be used to shuffle arrays and strings. Also, since it is an object, it is easy to exchange implementations.
This class also supports RNGs defined in userland. It can be used by passing an instance of a class that implements the RandomNumberGenerator interface provided at the same time as the first argument.This is useful for unit testing or when you want to use a fixed number.
class UserDefinedRNG implements RandomNumberGenerator { protected int $current = 0; public function generate(): int { return ++$this->current; } } function foobar(Random $random): void { for ($i = 0; $i < 9; $i++) { echo $random->nextInt(); } } foobar(new Random(new UserDefinedRNG())); // Results: 123456789
Algorithms can be registered and unregistered using PHP's C API, and new implementations can be added via Extension. The following APIs are provided:
/* Register new RNG algo */ int php_random_class_algo_register(const php_random_class_algo *algo); /* Unregister RNG algo */ void php_random_class_algo_unregister(const char *ident); /* Find and get registered algo */ const php_random_class_algo* php_random_class_algo_find(const zend_string *ident);
Also, as with MT, various alternative APIs using object scope RNGs will be provided.
/* similar php_mt_rand() */ uint64_t php_random_class_next(php_random_class *random_class); /* similar php_mt_rand_range() */ zend_long php_random_class_range(php_random_class *random_class, zend_long min, zend_long max); /* similar php_array_data_shuffle() */ void php_random_class_array_data_shuffle(php_random_class *random_class, zval *array); /* similar php_string_shuffle() */ void php_random_class_string_shuffle(php_random_class *random_class, char *str, zend_long len);
Random class can be serialized or cloned if the algorithm supports it. This is useful for storing and reusing state.
// serialize $foo = new Random(); for ($i = 0; $i < 10; $i++) { $foo->nextInt(); } var_dump(unserialize(serialize($foo))->nextInt() === $foo->nextInt()); // true // clone $bar = new Random(); for ($i = 0; $i < 10; $i++) { $bar->nextInt(); } $baz = clone $bar; var_dump($baz->nextInt() === $bar->nextInt()); // true
Using this feature, the first example can be rewritten as follows:
echo foo(1234, function (): void {}) . PHP_EOL; // Result: 1480009472 echo foo(1234, function (): void { mt_rand(); }) . PHP_EOL; // Result: 1480009472 function foo(int $seed, callable $bar): int { $random = new Random(RANDOM_MT19937, $seed); $result = $random->nextInt(); $bar(); $result += $random->nextInt(); return $result; }
Backward Incompatible Changes
The following items will no longer be available:
- “Random” class name
- “RandomNumberGenerator” class (interface) name
- “RANDOM_XORSHIFT128PLUS” constant
- “RANDOM_MT19937” constant
- “RANDOM_SECURE” constant
Proposed PHP Version(s)
8.1
RFC Impact
To SAPIs
none
To Existing Extensions
none
To Opcache
none
New Constants
- RANDOM_XORSHIFT128PLUS
- RANDOM_MT19937
- RANDOM_SECURE
php.ini Defaults
none
Open Issues
none
Vote
Voting opens 2021-MM-DD and 2021-MM-DD at 00:00:00 EDT. 2/3 required to accept.