Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 29 additions & 75 deletions doc/contributing/adding-v8-fast-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ for example, they may not trigger garbage collection.
* Fast API functions may not trigger garbage collection. This means by proxy
that JavaScript execution and heap allocation are also forbidden, including
`v8::Array::Get()` or `v8::Number::New()`.
* Throwing errors is not available from within a fast API call, but can be done
through the fallback to the slow API.
* Throwing errors is not available from within a fast API call. If you need to
throw, perform validation in JavaScript or only expose a slow API.
Comment on lines +14 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't actually accurate either – it's possible to throw errors within fast API calls, as the API now provides access to the isolate.

I did try to include this under some wider proposed changes at #58999, although that never garnered much comment. If this PR does go through separately, then this wording will need updating.

Copy link
Member Author

@gurgunday gurgunday Sep 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah let's do your PR @Renegade334

* Not all parameter and return types are supported in fast API calls.
For a full list, please look into
[`v8-fast-api-calls.h`](../../deps/v8/include/v8-fast-api-calls.h).
Expand All @@ -28,9 +28,6 @@ for example, they may not trigger garbage collection.
information.
* Fast API functions must be tested following the example in
[Test with Fast API path](#test-with-fast-api-path).
* The fast callback must be idempotent up to the point where error and fallback
conditions are checked, because otherwise executing the slow callback might
produce visible side effects twice.
* If the receiver is used in the callback, it must be passed as a second argument,
leaving the first one unused, to prevent the JS land from accidentally omitting the receiver when
invoking the fast API method.
Expand All @@ -42,52 +39,20 @@ for example, they may not trigger garbage collection.
static int32_t FastInternalModuleStat(
Local<Object> unused,
Local<Object> recv,
const FastOneByteString& input,
FastApiCallbackOptions& options) {
const FastOneByteString& input) {
Environment* env = Environment::GetCurrent(recv->GetCreationContextChecked());
// More code
}
```

## Fallback to slow path

Fast API supports fallback to slow path for when it is desirable to do so,
for example, when throwing a custom error or executing JavaScript code is
needed. The fallback mechanism can be enabled and changed from the C++
implementation of the fast API function declaration.

Passing `true` to the `fallback` option will force V8 to run the slow path
with the same arguments.

In V8, the options fallback is defined as `FastApiCallbackOptions` inside
[`v8-fast-api-calls.h`](../../deps/v8/include/v8-fast-api-calls.h).

* C++ land

Example of a conditional fast path on C++

```cpp
// Anywhere in the execution flow, you can set fallback and stop the execution.
static double divide(const int32_t a,
const int32_t b,
v8::FastApiCallbackOptions& options) {
if (b == 0) {
options.fallback = true;
return 0;
} else {
return a / b;
}
}
```

## Example

A typical function that communicates between JavaScript and C++ is as follows.

* On the JavaScript side:

```js
const { divide } = internalBinding('custom_namespace');
const { add } = internalBinding('custom_namespace');
```

* On the C++ side:
Expand All @@ -99,48 +64,41 @@ A typical function that communicates between JavaScript and C++ is as follows.
namespace node {
namespace custom_namespace {

static void SlowDivide(const FunctionCallbackInfo<Value>& args) {
static inline int32_t AddImpl(const int32_t a, const int32_t b) {
return a + b;
}

static void SlowAdd(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 2);
CHECK(args[0]->IsInt32());
CHECK(args[1]->IsInt32());
auto a = args[0].As<v8::Int32>();
auto b = args[1].As<v8::Int32>();

if (b->Value() == 0) {
return node::THROW_ERR_INVALID_STATE(env, "Error");
}

double result = a->Value() / b->Value();
args.GetReturnValue().Set(v8::Number::New(env->isolate(), result));
int32_t result = AddImpl(a->Value(), b->Value());
args.GetReturnValue().Set(v8::Int32::New(env->isolate(), result));
}

static double FastDivide(const int32_t a,
const int32_t b,
v8::FastApiCallbackOptions& options) {
if (b == 0) {
TRACK_V8_FAST_API_CALL("custom_namespace.divide.error");
options.fallback = true;
return 0;
} else {
TRACK_V8_FAST_API_CALL("custom_namespace.divide.ok");
return a / b;
}
static int32_t FastAdd(const int32_t a,
const int32_t b) {
TRACK_V8_FAST_API_CALL("custom_namespace.add");
return AddImpl(a, b);
}

CFunction fast_divide_(CFunction::Make(FastDivide));
CFunction fast_add_(CFunction::Make(FastAdd));

static void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
SetFastMethod(context, target, "divide", SlowDivide, &fast_divide_);
SetFastMethod(context, target, "add", SlowAdd, &fast_add_);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(SlowDivide);
registry->Register(FastDivide);
registry->Register(fast_divide_.GetTypeInfo());
registry->Register(SlowAdd);
registry->Register(FastAdd);
registry->Register(fast_add_.GetTypeInfo());
}

} // namespace custom_namespace
Expand All @@ -156,16 +114,15 @@ A typical function that communicates between JavaScript and C++ is as follows.
* Update external references ([`node_external_reference.h`](../../src/node_external_reference.h))

Since our implementation used
`double(const int32_t a, const int32_t b, v8::FastApiCallbackOptions& options)`
`int32_t(const int32_t a, const int32_t b)`
signature, we need to add it to external references and in
`ALLOWED_EXTERNAL_REFERENCE_TYPES`.

Example declaration:

```cpp
using CFunctionCallbackReturningDouble = double (*)(const int32_t a,
const int32_t b,
v8::FastApiCallbackOptions& options);
using CFunctionCallbackReturningInt32 = int32_t (*)(const int32_t a,
const int32_t b);
```

### Test with Fast API path
Expand All @@ -188,27 +145,24 @@ const common = require('../common');

const { internalBinding } = require('internal/test/binding');
// We could also require a function that uses the internal binding internally.
const { divide } = internalBinding('custom_namespace');
const { add } = internalBinding('custom_namespace');

// The function that will be optimized. It has to be a function written in
// JavaScript. Since `divide` comes from the C++ side, we need to wrap it.
function testFastPath(a, b) {
return divide(a, b);
return add(a, b);
}

eval('%PrepareFunctionForOptimization(testFastPath)');
// This call will let V8 know about the argument types that the function expects.
assert.strictEqual(testFastPath(6, 3), 2);
assert.strictEqual(testFastPath(6, 3), 9);

eval('%OptimizeFunctionOnNextCall(testFastPath)');
assert.strictEqual(testFastPath(8, 2), 4);
assert.throws(() => testFastPath(1, 0), {
code: 'ERR_INVALID_STATE',
});
assert.strictEqual(testFastPath(8, 2), 10);
assert.strictEqual(testFastPath(1, 0), 1);

if (common.isDebug) {
const { getV8FastApiCallCount } = internalBinding('debug');
assert.strictEqual(getV8FastApiCallCount('custom_namespace.divide.ok'), 1);
assert.strictEqual(getV8FastApiCallCount('custom_namespace.divide.error'), 1);
assert.strictEqual(getV8FastApiCallCount('custom_namespace.add'), 2);
}
```
Loading