Skip to content
Open
11 changes: 11 additions & 0 deletions Core/Node-API/Source/env_quickjs.cc
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ namespace Napi
}
env_ptr->handle_scope_stack.clear();

// Handles escaped from scopes that were never closed are held aside
// rather than on the stack, so free them here too.
for (auto& entry : env_ptr->escapable_scopes)
{
if (entry.second.escaped)
{
JS_FreeValue(env_ptr->context, *entry.second.escaped);
}
}
env_ptr->escapable_scopes.clear();

// Run the cycle collector so napi_wrap finalizers (which
// destroy C++ wrapper objects and release any embedded
// napi_refs) get a chance to execute while the env is still
Expand Down
12 changes: 9 additions & 3 deletions Core/Node-API/Source/js_native_api_chakra.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1929,7 +1929,8 @@ napi_status napi_open_escapable_handle_scope(
napi_escapable_handle_scope* result) {
CHECK_ENV(env);
CHECK_ARG(env, result);
*result = reinterpret_cast<napi_escapable_handle_scope>(1);
*result = reinterpret_cast<napi_escapable_handle_scope>(
++env->next_escapable_scope_token);
return napi_ok;
}

Expand All @@ -1939,11 +1940,13 @@ napi_status napi_close_escapable_handle_scope(
napi_escapable_handle_scope scope) {
CHECK_ENV(env);
CHECK_ARG(env, scope);
env->escaped_scopes.erase(reinterpret_cast<size_t>(scope));
return napi_ok;
}

// Stub implementation of handle scope apis for JSRT.
// This one will return escapee value as this is called from leveldown db.
// JSRT roots values independently of any scope, so the escapee is returned as
// is. The scope is still tracked so a second escape is rejected as Node-API
// requires.
napi_status napi_escape_handle(napi_env env,
napi_escapable_handle_scope scope,
napi_value escapee,
Expand All @@ -1952,6 +1955,9 @@ napi_status napi_escape_handle(napi_env env,
CHECK_ARG(env, scope);
CHECK_ARG(env, escapee);
CHECK_ARG(env, result);
if (!env->escaped_scopes.insert(reinterpret_cast<size_t>(scope)).second) {
return napi_set_last_error(env, napi_escape_called_twice);
}
*result = escapee;
return napi_ok;
}
Expand Down
8 changes: 8 additions & 0 deletions Core/Node-API/Source/js_native_api_chakra.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <napi/js_native_api_types.h>
#include <thread>
#include <cassert>
#include <set>

struct napi_env__ {
JsSourceContext source_context = JS_SOURCE_CONTEXT_NONE;
Expand All @@ -15,6 +16,13 @@ struct napi_env__ {

JsPropertyIdRef wrap_property_id = JS_INVALID_REFERENCE;

// Escapable scope bookkeeping. Values are rooted by the engine rather than by
// a scope here, so this exists only to honour the one-escape-per-scope rule.
// The token is a monotonic counter, never an index into anything, so two
// scopes can never share one.
size_t next_escapable_scope_token = 0;
std::set<size_t> escaped_scopes;

const std::thread::id thread_id{std::this_thread::get_id()};
};

Expand Down
12 changes: 9 additions & 3 deletions Core/Node-API/Source/js_native_api_javascriptcore.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2143,7 +2143,8 @@ napi_status napi_open_escapable_handle_scope(napi_env env,
napi_escapable_handle_scope* result) {
CHECK_ENV(env);
CHECK_ARG(env, result);
*result = reinterpret_cast<napi_escapable_handle_scope>(1);
*result = reinterpret_cast<napi_escapable_handle_scope>(
++env->next_escapable_scope_token);
return napi_ok;
}

Expand All @@ -2152,11 +2153,13 @@ napi_status napi_close_escapable_handle_scope(napi_env env,
napi_escapable_handle_scope scope) {
CHECK_ENV(env);
CHECK_ARG(env, scope);
env->escaped_scopes.erase(reinterpret_cast<size_t>(scope));
return napi_ok;
}

// Stub implementation of handle scope apis for JSC.
// This one will return escapee value as this is called from leveldown db.
// JSC roots values independently of any scope, so the escapee is returned as
// is. The scope is still tracked so a second escape is rejected as Node-API
// requires.
napi_status napi_escape_handle(napi_env env,
napi_escapable_handle_scope scope,
napi_value escapee,
Expand All @@ -2165,6 +2168,9 @@ napi_status napi_escape_handle(napi_env env,
CHECK_ARG(env, scope);
CHECK_ARG(env, escapee);
CHECK_ARG(env, result);
if (!env->escaped_scopes.insert(reinterpret_cast<size_t>(scope)).second) {
return napi_set_last_error(env, napi_escape_called_twice);
}
*result = escapee;
return napi_ok;
}
Expand Down
8 changes: 8 additions & 0 deletions Core/Node-API/Source/js_native_api_javascriptcore.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <list>
#include <thread>
#include <cassert>
#include <set>

struct napi_env__ {
JSGlobalContextRef context{};
Expand All @@ -20,6 +21,13 @@ struct napi_env__ {
JSValueRef reference_info_symbol{};
JSValueRef wrapper_info_symbol{};

// Escapable scope bookkeeping. Values are rooted by the engine rather than by
// a scope here, so this exists only to honour the one-escape-per-scope rule.
// The token is a monotonic counter, never an index into anything, so two
// scopes can never share one.
size_t next_escapable_scope_token{0};
std::set<size_t> escaped_scopes{};

const std::thread::id thread_id{std::this_thread::get_id()};

napi_env__(JSGlobalContextRef context) : context{context} {
Expand Down
74 changes: 36 additions & 38 deletions Core/Node-API/Source/js_native_api_quickjs.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1917,9 +1917,13 @@ napi_status napi_open_escapable_handle_scope(napi_env env, napi_escapable_handle
CHECK_ENV(env);
CHECK_ARG(env, result);

// Same as regular handle scope for QuickJS
env->current_scope_start = env->handle_scope_stack.size();
*result = reinterpret_cast<napi_escapable_handle_scope>(env->current_scope_start + 1);

// The token is a counter, not a position: scopes opened with no handle allocated
// between them share a position and would otherwise be indistinguishable.
const size_t token = ++env->next_escapable_scope_token;
env->escapable_scopes.emplace(token, napi_env__::EscapableScope{env->current_scope_start, nullptr});
*result = reinterpret_cast<napi_escapable_handle_scope>(token);

napi_clear_last_error(env);
return napi_ok;
Expand All @@ -1929,14 +1933,28 @@ napi_status napi_close_escapable_handle_scope(napi_env env, napi_escapable_handl
CHECK_ENV(env);
CHECK_ARG(env, scope);

// Same cleanup as regular handle scope
size_t scope_start = reinterpret_cast<size_t>(scope) - 1;
const auto it = env->escapable_scopes.find(reinterpret_cast<size_t>(scope));
if (it == env->escapable_scopes.end()) {
return napi_set_last_error(env, napi_invalid_arg);
}

const size_t scope_start = it->second.scope_start;

for (size_t i = scope_start; i < env->handle_scope_stack.size(); i++) {
JS_FreeValue(env->context, *env->handle_scope_stack[i]);
}

env->handle_scope_stack.resize(scope_start);

// The escaped handle, if any, was held aside by napi_escape_handle rather than
// stored on the stack. Now that this scope's own handles are gone it can be
// pushed on: it lands at scope_start, which belongs to the parent scope, so it
// outlives this close and is freed when the parent closes.
if (it->second.escaped) {
env->handle_scope_stack.push_back(std::move(it->second.escaped));
}
env->escapable_scopes.erase(it);

env->current_scope_start = scope_start;

napi_clear_last_error(env);
Expand All @@ -1949,43 +1967,23 @@ napi_status napi_escape_handle(napi_env env, napi_escapable_handle_scope scope,
CHECK_ARG(env, escapee);
CHECK_ARG(env, result);

// Get the scope start index
size_t scope_start = reinterpret_cast<size_t>(scope) - 1;
const auto it = env->escapable_scopes.find(reinterpret_cast<size_t>(scope));
if (it == env->escapable_scopes.end()) {
return napi_set_last_error(env, napi_invalid_arg);
}

// Duplicate the JSValue to create a new handle that will outlive the current scope
JSValue jsValue = ToJSValue(escapee);
JSValue escapedValue = JS_DupValue(env->context, jsValue);

// Store the escaped value in the parent scope (before scope_start)
auto parentPtr = std::make_unique<JSValue>(escapedValue);
napi_value parentHandle = reinterpret_cast<napi_value>(parentPtr.get());

// Insert at parent scope position (before current scope)
if (scope_start > 0) {
env->handle_scope_stack.insert(
env->handle_scope_stack.begin() + scope_start,
std::move(parentPtr)
);

// Note: Inserting shifts indices, but since we're inserting at scope_start,
// the current scope's start index is now scope_start + 1
// We need to update current_scope_start if it was pointing to this scope
if (env->current_scope_start == scope_start) {
env->current_scope_start = scope_start + 1;
}
} else {
// No parent scope - just add to the beginning
env->handle_scope_stack.insert(
env->handle_scope_stack.begin(),
std::move(parentPtr)
);

if (env->current_scope_start == 0) {
env->current_scope_start = 1;
}
// Node-API allows napi_escape_handle to be called at most once per scope.
if (it->second.escaped) {
return napi_set_last_error(env, napi_escape_called_twice);
}

*result = parentHandle;
// Duplicate the JSValue to create a new handle that will outlive the current scope
JSValue escapedValue = JS_DupValue(env->context, ToJSValue(escapee));

auto holder = std::make_unique<JSValue>(escapedValue);
*result = reinterpret_cast<napi_value>(holder.get());
it->second.escaped = std::move(holder);

napi_clear_last_error(env);
return napi_ok;
}
Expand Down
17 changes: 17 additions & 0 deletions Core/Node-API/Source/js_native_api_quickjs.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <thread>
#include <cassert>
#include <memory>
#include <map>
#include <vector>

// Reference info for preventing GC. Defined in the header so that both
Expand All @@ -33,6 +34,22 @@ struct napi_env__ {
std::vector<std::unique_ptr<JSValue>> handle_scope_stack;
size_t current_scope_start = 0;

// One record per open escapable scope, keyed by the opaque token handed to the
// caller. The token is a monotonic counter rather than a position in
// handle_scope_stack: two scopes opened with no handle allocated between them
// occupy the same position, so a position-derived token cannot tell them apart.
// The escaped handle is held here rather than on handle_scope_stack because
// inserting into the middle of the stack would shift every entry above it and
// invalidate the recorded start of any nested scope that is still open.
// napi_close_escapable_handle_scope pushes it onto the stack once the scope's own
// handles are gone, at which point it lands in the parent scope and is freed with it.
struct EscapableScope {
size_t scope_start;
std::unique_ptr<JSValue> escaped;
};
std::map<size_t, EscapableScope> escapable_scopes;
size_t next_escapable_scope_token = 0;

// Tracks every RefInfo* created by napi_create_reference so that
// pending strong references can be released during Detach. Without
// this, any napi_ref held by a native object (e.g. a polyfill's
Expand Down
Loading
Loading