doc: add primordials guidelines
PR-URL: https://github.com/nodejs/node/pull/38635 Reviewed-By: Akhil Marsonya <akhil.marsonya27@gmail.com> Reviewed-By: Darshan Sen <raisinten@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com> Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
This commit is contained in:
parent
d8460de72e
commit
92f8c03ce6
595
doc/contributing/primordials.md
Normal file
595
doc/contributing/primordials.md
Normal file
@ -0,0 +1,595 @@
|
||||
# Usage of primordials in core
|
||||
|
||||
The file `lib/internal/per_context/primordials.js` subclasses and stores the JS
|
||||
built-ins that come from the VM so that Node.js built-in modules do not need to
|
||||
later look these up from the global proxy, which can be mutated by users.
|
||||
|
||||
Usage of primordials should be preferred for any new code, but replacing current
|
||||
code with primordials should be
|
||||
[done with care](#primordials-with-known-performance-issues). It is highly
|
||||
recommended to ping the relevant team when reviewing a pull request that touches
|
||||
one of the subsystems they "own".
|
||||
|
||||
## Accessing primordials
|
||||
|
||||
The primordials are meant for internal use only, and are only accessible for
|
||||
internal core modules. User code cannot use or rely on primordials. It is
|
||||
usually fine to rely on ECMAScript built-ins and assume that it will behave as
|
||||
specified.
|
||||
|
||||
If you would like to access the `primordials` object to help you with Node.js
|
||||
core development or for tinkering, you can expose it on the global scope using
|
||||
this combination of CLI flags:
|
||||
|
||||
```bash
|
||||
node --expose-internals -r internal/test/binding
|
||||
```
|
||||
|
||||
## Contents of primordials
|
||||
|
||||
### Properties of the global object
|
||||
|
||||
Objects and functions on the global object can be deleted or replaced. Using
|
||||
them from primordials makes the code more reliable:
|
||||
|
||||
```js
|
||||
globalThis.Array === primordials.Array; // true
|
||||
|
||||
globalThis.Array = function() {
|
||||
return [1, 2, 3];
|
||||
};
|
||||
globalThis.Array === primordials.Array; // false
|
||||
|
||||
primordials.Array(0); // []
|
||||
globalThis.Array(0); // [1,2,3]
|
||||
```
|
||||
|
||||
### Prototype methods
|
||||
|
||||
ECMAScript provides a group of methods available on built-in objects that are
|
||||
used to interact with JavaScript objects.
|
||||
|
||||
```js
|
||||
const array = [1, 2, 3];
|
||||
array.push(4); // Here `push` refers to %Array.prototype.push%.
|
||||
console.log(JSON.stringify(array)); // [1,2,3,4]
|
||||
|
||||
// %Array.prototype%.push is modified in userland.
|
||||
Array.prototype.push = function push(val) {
|
||||
return this.unshift(val);
|
||||
};
|
||||
|
||||
array.push(5); // Now `push` refers to the modified method.
|
||||
console.log(JSON.stringify(array)); // [5,1,2,3,4]
|
||||
```
|
||||
|
||||
Primordials wrap the original prototype functions with new functions that take
|
||||
the `this` value as the first argument:
|
||||
|
||||
```js
|
||||
const {
|
||||
ArrayPrototypePush,
|
||||
} = primordials;
|
||||
|
||||
const array = [1, 2, 3];
|
||||
ArrayPrototypePush(array, 4);
|
||||
console.log(JSON.stringify(array)); // [1,2,3,4]
|
||||
|
||||
Array.prototype.push = function push(val) {
|
||||
return this.unshift(val);
|
||||
};
|
||||
|
||||
ArrayPrototypePush(array, 5);
|
||||
console.log(JSON.stringify(array)); // [1,2,3,4,5]
|
||||
```
|
||||
|
||||
### Safe classes
|
||||
|
||||
Safe classes are classes that provide the same API as their equivalent class,
|
||||
but whose implementation aims to avoid any reliance on user-mutable code.
|
||||
Safe classes should not be exposed to user-land; use unsafe equivalent when
|
||||
dealing with objects that are accessible from user-land.
|
||||
|
||||
### Variadic functions
|
||||
|
||||
There are some built-in functions that accept a variable number of arguments
|
||||
(e.g.: `Math.max`, `%Array.prototype.push%`). It is sometimes useful to provide
|
||||
the list of arguments as an array. You can use primordial function with the
|
||||
suffix `Apply` (e.g.: `MathMaxApply`, `ArrayPrototypePushApply`) to do that.
|
||||
|
||||
## Primordials with known performance issues
|
||||
|
||||
One of the reasons why the current Node.js API is not completely tamper-proof is
|
||||
performance: sometimes the use of primordials can cause performance regressions
|
||||
with V8, which when in a hot code path, could significantly decrease the
|
||||
performance of code in Node.js.
|
||||
|
||||
* Methods that mutate the internal state of arrays:
|
||||
* `ArrayPrototypePush`
|
||||
* `ArrayPrototypePop`
|
||||
* `ArrayPrototypeShift`
|
||||
* `ArrayPrototypeUnshift`
|
||||
* Methods of the function prototype:
|
||||
* `FunctionPrototypeBind`
|
||||
* `FunctionPrototypeCall`: creates performance issues when used to invoke
|
||||
super constructors.
|
||||
* `FunctionPrototype`: use `() => {}` instead when referencing a no-op
|
||||
function.
|
||||
* `SafeArrayIterator`
|
||||
* `SafeStringIterator`
|
||||
* `SafePromiseAll`
|
||||
* `SafePromiseAllSettled`
|
||||
* `SafePromiseAny`
|
||||
* `SafePromiseRace`
|
||||
* `SafePromisePrototypeFinally`: use `try {} finally {}` block instead.
|
||||
|
||||
In general, when sending or reviewing a PR that makes changes in a hot code
|
||||
path, use extra caution and run extensive benchmarks.
|
||||
|
||||
## Implicit use of user-mutable methods
|
||||
|
||||
### Unsafe array iteration
|
||||
|
||||
There are many usual practices in JavaScript that rely on iteration. It's useful
|
||||
to be aware of them when dealing with arrays (or `TypedArray`s) in core as array
|
||||
iteration typically calls several user-mutable methods. This sections lists the
|
||||
most common patterns in which ECMAScript code relies non-explicitly on array
|
||||
iteration and how to avoid it.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Avoid for-of loops on arrays</summary>
|
||||
|
||||
```js
|
||||
for (const item of array) {
|
||||
console.log(item);
|
||||
}
|
||||
```
|
||||
|
||||
This code is internally expanded into something that looks like:
|
||||
|
||||
```js
|
||||
{
|
||||
// 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
|
||||
// 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
|
||||
// 3. Call that function.
|
||||
const iterator = array[Symbol.iterator]();
|
||||
// 1. Lookup `next` property on `iterator` (doesn't exist).
|
||||
// 2. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
|
||||
// 3. Call that function.
|
||||
let { done, value: item } = iterator.next();
|
||||
while (!done) {
|
||||
console.log(item);
|
||||
// Repeat.
|
||||
({ done, value: item } = iterator.next());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Instead of utilizing iterators, you can use the more traditional but still very
|
||||
performant `for` loop:
|
||||
|
||||
```js
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
console.log(array[i]);
|
||||
}
|
||||
```
|
||||
|
||||
The following code snippet illustrates how user-land code could impact the
|
||||
behavior of internal modules:
|
||||
|
||||
```js
|
||||
// User-land
|
||||
Array.prototype[Symbol.iterator] = () => ({
|
||||
next: () => ({ done: true }),
|
||||
});
|
||||
|
||||
// Core
|
||||
let forOfLoopBlockExecuted = false;
|
||||
let forLoopBlockExecuted = false;
|
||||
const array = [1, 2, 3];
|
||||
for (const item of array) {
|
||||
forOfLoopBlockExecuted = true;
|
||||
}
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
forLoopBlockExecuted = true;
|
||||
}
|
||||
console.log(forOfLoopBlockExecuted); // false
|
||||
console.log(forLoopBlockExecuted); // true
|
||||
```
|
||||
|
||||
This only applies if you are working with a genuine array (or array-like
|
||||
object). If you are instead expecting an iterator, a for-of loop may be a better
|
||||
choice.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Avoid array destructuring assignment on arrays</summary>
|
||||
|
||||
```js
|
||||
const [first, second] = array;
|
||||
```
|
||||
|
||||
This is roughly equivalent to:
|
||||
|
||||
```js
|
||||
// 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
|
||||
// 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
|
||||
// 3. Call that function.
|
||||
const iterator = array[Symbol.iterator]();
|
||||
// 1. Lookup `next` property on `iterator` (doesn't exist).
|
||||
// 2. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
|
||||
// 3. Call that function.
|
||||
const first = iterator.next().value;
|
||||
// Repeat.
|
||||
const second = iterator.next().value;
|
||||
```
|
||||
|
||||
Instead you can use object destructuring:
|
||||
|
||||
```js
|
||||
const { 0: first, 1: second } = array;
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```js
|
||||
const first = array[0];
|
||||
const second = array[1];
|
||||
```
|
||||
|
||||
This only applies if you are working with a genuine array (or array-like
|
||||
object). If you are instead expecting an iterator, array destructuring is the
|
||||
best choice.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Avoid spread operator on arrays</summary>
|
||||
|
||||
```js
|
||||
// 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
|
||||
// 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
|
||||
// 3. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
|
||||
const arrayCopy = [...array];
|
||||
func(...array);
|
||||
```
|
||||
|
||||
Instead you can use other ECMAScript features to achieve the same result:
|
||||
|
||||
```js
|
||||
const arrayCopy = ArrayPrototypeSlice(array);
|
||||
ReflectApply(func, null, array);
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary><code>%Object.fromEntries%</code> iterate over an array</summary>
|
||||
|
||||
```js
|
||||
{
|
||||
// Unsafe code example:
|
||||
// 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
|
||||
// 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
|
||||
// 3. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
|
||||
const obj = ObjectFromEntries(array);
|
||||
}
|
||||
|
||||
{
|
||||
// Safe example using `SafeArrayIterator`:
|
||||
const obj = ObjectFromEntries(new SafeArrayIterator(array));
|
||||
}
|
||||
|
||||
{
|
||||
// Safe example without using `SafeArrayIterator`:
|
||||
const obj = {};
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
obj[array[i][0]] = array[i][1];
|
||||
}
|
||||
// In a hot code path, this would be the preferred method.
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary><code>%Promise.all%</code>,
|
||||
<code>%Promise.allSettled%</code>,
|
||||
<code>%Promise.any%</code>, and
|
||||
<code>%Promise.race%</code> iterate over an array</summary>
|
||||
|
||||
```js
|
||||
// 1. Lookup @@iterator property on `array` (user-mutable if user-provided).
|
||||
// 2. Lookup @@iterator property on %Array.prototype% (user-mutable).
|
||||
// 3. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
|
||||
PromiseAll(array); // unsafe
|
||||
|
||||
PromiseAll(new SafeArrayIterator(array));
|
||||
SafePromiseAll(array); // safe
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary><code>%Map%</code>, <code>%Set%</code>, <code>%WeakMap%</code>, and
|
||||
<code>%WeakSet%</code> constructors iterate over an array</summary>
|
||||
|
||||
```js
|
||||
// User-land
|
||||
Array.prototype[Symbol.iterator] = () => ({
|
||||
next: () => ({ done: true }),
|
||||
});
|
||||
|
||||
// Core
|
||||
|
||||
// 1. Lookup @@iterator property on %Array.prototype% (user-mutable).
|
||||
// 2. Lookup `next` property on %ArrayIteratorPrototype% (user-mutable).
|
||||
const set = new SafeSet([1, 2, 3]);
|
||||
|
||||
console.log(set.size); // 0
|
||||
```
|
||||
|
||||
```js
|
||||
// User-land
|
||||
Array.prototype[Symbol.iterator] = () => ({
|
||||
next: () => ({ done: true }),
|
||||
});
|
||||
|
||||
// Core
|
||||
const set = new SafeSet();
|
||||
set.add(1).add(2).add(3);
|
||||
console.log(set.size); // 3
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Promise objects
|
||||
|
||||
<details>
|
||||
|
||||
<summary><code>%Promise.prototype.finally%</code> looks up <code>then</code>
|
||||
property of the Promise instance</summary>
|
||||
|
||||
```js
|
||||
// User-land
|
||||
Promise.prototype.then = function then(a, b) {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// Core
|
||||
let finallyBlockExecuted = false;
|
||||
PromisePrototypeFinally(somePromiseThatEventuallySettles,
|
||||
() => { finallyBlockExecuted = true; });
|
||||
process.on('exit', () => console.log(finallyBlockExecuted)); // false
|
||||
```
|
||||
|
||||
```js
|
||||
// User-land
|
||||
Promise.prototype.then = function then(a, b) {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// Core
|
||||
let finallyBlockExecuted = false;
|
||||
(async () => {
|
||||
try {
|
||||
return await somePromiseThatEventuallySettles;
|
||||
} finally {
|
||||
finallyBlockExecuted = true;
|
||||
}
|
||||
})();
|
||||
process.on('exit', () => console.log(finallyBlockExecuted)); // true
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary><code>%Promise.all%</code>,
|
||||
<code>%Promise.allSettled%</code>,
|
||||
<code>%Promise.any%</code>, and
|
||||
<code>%Promise.race%</code> look up <code>then</code>
|
||||
property of the Promise instances</summary>
|
||||
|
||||
You can use safe alternatives from primordials that differ slightly from the
|
||||
original methods:
|
||||
* It expects an array (or array-like object) instead of an iterable.
|
||||
* It wraps each promise in `SafePromise` objects and wraps the result in a new
|
||||
`Promise` instance – which may come with a performance penalty.
|
||||
* Because it doesn't look up `then` property, it may not be the right tool to
|
||||
handle user-provided promises (which may be instances of a subclass of
|
||||
`Promise`).
|
||||
|
||||
```js
|
||||
// User-land
|
||||
Promise.prototype.then = function then(a, b) {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// Core
|
||||
let thenBlockExecuted = false;
|
||||
PromisePrototypeThen(
|
||||
PromiseAll(new SafeArrayIterator([PromiseResolve()])),
|
||||
() => { thenBlockExecuted = true; }
|
||||
);
|
||||
process.on('exit', () => console.log(thenBlockExecuted)); // false
|
||||
```
|
||||
|
||||
```js
|
||||
// User-land
|
||||
Promise.prototype.then = function then(a, b) {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// Core
|
||||
let thenBlockExecuted = false;
|
||||
PromisePrototypeThen(
|
||||
SafePromiseAll([PromiseResolve()]),
|
||||
() => { thenBlockExecuted = true; }
|
||||
);
|
||||
process.on('exit', () => console.log(thenBlockExecuted)); // true
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### (Async) Generator functions
|
||||
|
||||
Generators and async generators returned by generator functions and async
|
||||
generator functions are relying on user-mutable methods; their use in core
|
||||
should be avoided.
|
||||
|
||||
<details>
|
||||
|
||||
<summary><code>%GeneratorFunction.prototype.prototype%.next</code> is
|
||||
user-mutable</summary>
|
||||
|
||||
```js
|
||||
// User-land
|
||||
Object.getPrototypeOf(function* () {}).prototype.next = function next() {
|
||||
return { done: true };
|
||||
};
|
||||
|
||||
// Core
|
||||
function* someGenerator() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
}
|
||||
let loopCodeExecuted = false;
|
||||
for (const nb of someGenerator()) {
|
||||
loopCodeExecuted = true;
|
||||
}
|
||||
console.log(loopCodeExecuted); // false
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary><code>%AsyncGeneratorFunction.prototype.prototype%.next</code> is
|
||||
user-mutable</summary>
|
||||
|
||||
```js
|
||||
// User-land
|
||||
Object.getPrototypeOf(async function* () {}).prototype.next = function next() {
|
||||
return new Promise(() => {});
|
||||
};
|
||||
|
||||
// Core
|
||||
async function* someGenerator() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
}
|
||||
let finallyBlockExecuted = false;
|
||||
async () => {
|
||||
try {
|
||||
for await (const nb of someGenerator()) {
|
||||
// some code;
|
||||
}
|
||||
} finally {
|
||||
finallyBlockExecuted = true;
|
||||
}
|
||||
};
|
||||
process.on('exit', () => console.log(finallyBlockExecuted)); // false
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Text processing
|
||||
|
||||
#### Unsafe string methods
|
||||
|
||||
| The string method | looks up the property |
|
||||
| ----------------------------- | --------------------- |
|
||||
| `String.prototype.match` | `Symbol.match` |
|
||||
| `String.prototype.matchAll` | `Symbol.matchAll` |
|
||||
| `String.prototype.replace` | `Symbol.replace` |
|
||||
| `String.prototype.replaceAll` | `Symbol.replace` |
|
||||
| `String.prototype.search` | `Symbol.search` |
|
||||
| `String.prototype.split` | `Symbol.split` |
|
||||
|
||||
```js
|
||||
// User-land
|
||||
RegExp.prototype[Symbol.replace] = () => 'foo';
|
||||
String.prototype[Symbol.replace] = () => 'baz';
|
||||
|
||||
// Core
|
||||
console.log(StringPrototypeReplace('ber', /e/, 'a')); // 'foo'
|
||||
console.log(StringPrototypeReplace('ber', 'e', 'a')); // 'baz'
|
||||
console.log(RegExpPrototypeSymbolReplace(/e/, 'ber', 'a')); // 'bar'
|
||||
```
|
||||
|
||||
#### Unsafe string iteration
|
||||
|
||||
As with arrays, iterating over strings calls several user-mutable methods. Avoid
|
||||
iterating over strings when possible, or use `SafeStringIterator`.
|
||||
|
||||
#### Unsafe `RegExp` methods
|
||||
|
||||
Functions that lookup the `exec` property on the prototype chain:
|
||||
|
||||
* `RegExp.prototype[Symbol.match]`
|
||||
* `RegExp.prototype[Symbol.matchAll]`
|
||||
* `RegExp.prototype[Symbol.replace]`
|
||||
* `RegExp.prototype[Symbol.search]`
|
||||
* `RegExp.prototype[Symbol.split]`
|
||||
* `RegExp.prototype.test`
|
||||
|
||||
```js
|
||||
// User-land
|
||||
RegExp.prototype.exec = () => null;
|
||||
|
||||
// Core
|
||||
console.log(RegExpPrototypeTest(/o/, 'foo')); // false
|
||||
console.log(RegExpPrototypeExec(/o/, 'foo') !== null); // true
|
||||
```
|
||||
|
||||
#### Don't trust `RegExp` flags
|
||||
|
||||
RegExp flags are not own properties of the regex instances, which means flags
|
||||
can be reset from user-land.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>List of <code>RegExp</code> methods that look up properties from
|
||||
mutable getters</summary>
|
||||
|
||||
| `RegExp` method | looks up the following flag-related properties |
|
||||
| ------------------------------ | ------------------------------------------------------------------ |
|
||||
| `get RegExp.prototype.flags` | `global`, `ignoreCase`, `multiline`, `dotAll`, `unicode`, `sticky` |
|
||||
| `RegExp.prototype[@@match]` | `global`, `unicode` |
|
||||
| `RegExp.prototype[@@matchAll]` | `flags` |
|
||||
| `RegExp.prototype[@@replace]` | `global`, `unicode` |
|
||||
| `RegExp.prototype[@@split]` | `flags` |
|
||||
| `RegExp.prototype.toString` | `flags` |
|
||||
|
||||
</details>
|
||||
|
||||
```js
|
||||
// User-land
|
||||
Object.defineProperty(RegExp.prototype, 'global', { value: false });
|
||||
|
||||
// Core
|
||||
console.log(RegExpPrototypeSymbolReplace(/o/g, 'foo', 'a')); // 'fao'
|
||||
|
||||
const regex = /o/g;
|
||||
ObjectDefineProperties(regex, {
|
||||
dotAll: { value: false },
|
||||
exec: { value: undefined },
|
||||
flags: { value: 'g' },
|
||||
global: { value: true },
|
||||
ignoreCase: { value: false },
|
||||
multiline: { value: false },
|
||||
unicode: { value: false },
|
||||
sticky: { value: false },
|
||||
});
|
||||
console.log(RegExpPrototypeSymbolReplace(regex, 'foo', 'a')); // 'faa'
|
||||
```
|
Loading…
x
Reference in New Issue
Block a user