rfc:rng_extension

This is an old revision of the document!


PHP RFC: Add Random class

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:

  1. “Random” class name
  2. “RandomNumberGenerator” class (interface) name
  3. “RANDOM_XORSHIFT128PLUS” constant
  4. “RANDOM_MT19937” constant
  5. “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.

Add Random class
Real name Yes No
Final result: 0 0
This poll has been closed.

Patches and Tests

rfc/rng_extension.1622555940.txt.gz · Last modified: 2021/06/01 13:59 by zeriyoshi