This is an old revision of the document!
PHP RFC: Random Extension 3.0
- Version: 3.0
- Date: 2021-09-02
- Author: Go Kudo zeriyoshi@gmail.com
- Status: Under Discussion
- Implementation: https://github.com/php/php-src/pull/7453
- First Published at: http://wiki.php.net/rfc/object_scope_prng
Introduction
Currently, PHP's random number implementation suffers from several problems.
The first is that there are many different implementations. Historically, the random number implementations have been separated into lcg.c, rand.c, mt_rand.c random.c respectively, and the header file dependencies are complex.
Second, the pseudo-random number generator makes use of global state. If a random number is consumed at an unexpected time, the reproducibility of the result may be lost. 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; }
Reproducibility of random numbers can easily be lost if additional code 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
Clean up the implementation, separate out the random number related functions as Random extension, and add an object scoped API.
All of the following functions will be moved to the newly created Random extension.
- lcg_value()
- srand()
- rand()
- mt_srand()
- mt_rand()
At the same time, the following internal APIs will also be relocated. If you want to use them, you can simply include ext/random/random.h.
- php_random_int_throw()
- php_random_int_silent()
- php_combined_lcg()
- php_mt_srand()
- php_mt_rand()
- php_mt_rand_range()
- php_mt_rand_common()
- php_srand()
- php_rand()
- php_random_bytes()
- php_random_int()
The following PHP constants will now be provided by the Random extension
- MT_RAND_MT19937
- MT_RAND_PHP
To solve the scope problem, the following classes will be added
- Random class
- Random\NumberGenrator abstract class
- Random\NumberGenerator\XorShift128Plus class
- Random\NumberGenerator\MT19937 class
- Random\NumberGenerator\Secure class
The Random class is a utility class that provides functionality using random numbers. It provides the following methods, but does not provide an alternative to array_rand because it is too complex.
- getInt()
- getBytes()
- shuffleArray()
- shuffleString()
The Random class accepts an instance that inherits from Random\NumberGenerator as a constructor argument.
This class is final and cannot be cloned, but it can be serialized. This is to prevent $rng from being copied by reference to a property and causing unintended behavior.
The serializability depends on the serializability of the contained $rng.
final class Random { private Random\NumberGenerator $randomNumberGenerator; public function __construct(?Random\NumberGenerator $randomNumberGenerator = null) {} public function getNumberGenerator(): Random\NumberGenerator {} public function getInt(int $min, int $max): int {} public function getBytes(int $length): string {} public function shuffleArray(array $array): array {} public function shuffleString(string $string): string {} public function __serialize(): array {} public function __unserialize(array $data): void {} }
The Random\NumberGenerator abstract class has a single abstract method called generate().
namespace Random; abstract class NumberGenerator { abstract public function generate(): int {} }
By defining a class that extends Random\NumberGenerator, the user can use their own random number generator. With the introduction of JIT in PHP 8.0, this can generate random numbers at a realistic speed.
class UserDefinedRNG extends Random\NumberGenerator { protected int $current = 0; public function generate(): int { return ++$this->current; } } function foobar(Random\NumberGenerator $numberGenerator): void { for ($i = 0; $i < 9; $i++) { echo $numberGenerator->generate(); } } foobar(new UserDefinedRNG()); // Results: 123456789
It is also useful when you want to use a random number sequence with a fixed result, such as in testing.
The Random class creates and uses an instance of the default random number generator, Random\NumberGenerator\XorShift128Plus, if the constructor argument is omitted.
XorShift128Plus is an efficient, high-quality algorithm used in modern browsers and other applications. This algorithm is capable of generating a wider range of random numbers in a 64-bit environment. In a 32-bit environment, the range beyond zend_long will simply be truncated. This indicates incompatibility between environments, but is acceptable for real-world use.
The Random\NumberGenerator\MT19937 class, which implements the MT19937 Mersenne twister, is also provided for backward compatibility or when a higher period is required. However, a 1-bit right shift is required to obtain exactly the same result as mt_rand(), as shown below. This is due to historical reasons.
$seed = 1234; $mt = new Random\NumberGenerator\MT19937($seed); mt_srand($seed); var_dump(mt_rand() === ($mt->generate() >> 1)); // true
The following NumberGenerator class supports serialization. Secure is not serializable because it uses random_bytes internally and has no state.
- Random\NumberGenerator\XorShift128Plus
- Random\NumberGenerator\MT19937
- Random\NumberGenerator extends user-defined classes.
Also, a new internal API will be implemented.
- php_random_ng_next()
- php_random_ng_range()
- php_random_ng_array_data_shuffle()
- php_random_ng_string_shuffle()
A Stub showing these implementations can be found on the Pull-Request. It's probably easier to understand if you look at it.
Future Scope
This proposal is just a first step to improve the situation of PHP's random number implementation.
If this proposal is approved, I will then propose the following changes
- Replace the state of the existing implementation with php_random_ng.
- Replace random_bytes() with random_bytes() for random numbers used in shuffle(), str_shuffle(), and array_rand().
- Deprecate srand() and mt_srand() (step by step)
Backward Incompatible Changes
The code that includes the following header file needs to be changed to ext/random/random.h
- ext/standard/lcg.h
- ext/standard/rand.h
- ext/standard/mt_rand.h
- ext/standard/random.h
The following class names have been reserved and will no longer be available
- “Random”
- “Random\NumberGenerator”
- “Random\NumberGenerator\XorShift128Plus”
- “Random\NumberGenerator\MT19937”
- “Random\NumberGenerator\Secure”
Proposed PHP Version(s)
8.2
RFC Impact
To SAPIs
none
To Existing Extensions
none
To Opcache
none
New Constants
none
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.