rfc:rng_extension

This is an old revision of the document!


PHP RFC: Random Extension 3.0

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()
  • random_int()
  • random_bytes()

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()

This class can be used in the following way.

// functions
mt_srand(1234);
mt_rand(); // generate random number
mt_rand(1, 10); // generate random number in range
str_shuffle("foobar"); // shuffle string
$arr = range(1, 10);
shuffle($arr); // shuffle array items (pass by reference)
 
// object
$mt = new Random\NumberGenerator\MT19937(1234);
$mt->generate(); // generate random number 
$random = new Random($mt);
$random->getInt(1, 10); // generate random number in range
$random->shuffleString("foobar"); // shuffle string
$random->shuffleArray(range(1, 10)); // shuffle array items (pass by value)

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.
  • Changes random source to php_random_int() a 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.

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

Patches and Tests

rfc/rng_extension.1630679300.txt.gz · Last modified: 2021/09/03 14:28 by zeriyoshi