Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
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
22 changes: 19 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,9 @@ 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);
const size_t token = ++env->next_escapable_scope_token;
env->open_escapable_scopes.emplace(token, false);
*result = reinterpret_cast<napi_escapable_handle_scope>(token);
return napi_ok;
}

Expand All @@ -1939,11 +1941,17 @@ napi_status napi_close_escapable_handle_scope(
napi_escapable_handle_scope scope) {
CHECK_ENV(env);
CHECK_ARG(env, scope);
const auto it = env->open_escapable_scopes.find(reinterpret_cast<size_t>(scope));
if (it == env->open_escapable_scopes.end()) {
return napi_set_last_error(env, napi_invalid_arg);
}
env->open_escapable_scopes.erase(it);
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 +1960,14 @@ napi_status napi_escape_handle(napi_env env,
CHECK_ARG(env, scope);
CHECK_ARG(env, escapee);
CHECK_ARG(env, result);
const auto it = env->open_escapable_scopes.find(reinterpret_cast<size_t>(scope));
if (it == env->open_escapable_scopes.end()) {
return napi_set_last_error(env, napi_invalid_arg);
}
if (it->second) {
return napi_set_last_error(env, napi_escape_called_twice);
}
it->second = true;
*result = escapee;
return napi_ok;
}
Expand Down
9 changes: 9 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 <map>

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

JsPropertyIdRef wrap_property_id = JS_INVALID_REFERENCE;

// Escapable scope bookkeeping: token -> whether that scope has escaped. Values
// are rooted by the engine rather than by a scope here, so this exists only to
// honour the one-escape-per-scope rule and to reject tokens that are not open.
// 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::map<size_t, bool> open_escapable_scopes;

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

Expand Down
22 changes: 19 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,9 @@ 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);
const size_t token = ++env->next_escapable_scope_token;
env->open_escapable_scopes.emplace(token, false);
*result = reinterpret_cast<napi_escapable_handle_scope>(token);
return napi_ok;
}

Expand All @@ -2152,11 +2154,17 @@ napi_status napi_close_escapable_handle_scope(napi_env env,
napi_escapable_handle_scope scope) {
CHECK_ENV(env);
CHECK_ARG(env, scope);
const auto it = env->open_escapable_scopes.find(reinterpret_cast<size_t>(scope));
if (it == env->open_escapable_scopes.end()) {
return napi_set_last_error(env, napi_invalid_arg);
}
env->open_escapable_scopes.erase(it);
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 +2173,14 @@ napi_status napi_escape_handle(napi_env env,
CHECK_ARG(env, scope);
CHECK_ARG(env, escapee);
CHECK_ARG(env, result);
const auto it = env->open_escapable_scopes.find(reinterpret_cast<size_t>(scope));
if (it == env->open_escapable_scopes.end()) {
return napi_set_last_error(env, napi_invalid_arg);
}
if (it->second) {
return napi_set_last_error(env, napi_escape_called_twice);
}
it->second = true;
*result = escapee;
return napi_ok;
}
Expand Down
9 changes: 9 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 <map>

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

// Escapable scope bookkeeping: token -> whether that scope has escaped. Values
// are rooted by the engine rather than by a scope here, so this exists only to
// honour the one-escape-per-scope rule and to reject tokens that are not open.
// 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::map<size_t, bool> open_escapable_scopes{};

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

napi_env__(JSGlobalContextRef context) : context{context} {
Expand Down
82 changes: 44 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,36 @@ 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;

// A scope closed out of LIFO order would leave scope_start past the end of the
// stack, and resize would then grow it with null entries that the next close
// dereferences. Node-API forbids that ordering, so report it rather than
// corrupting the stack.
if (scope_start > env->handle_scope_stack.size()) {
return napi_set_last_error(env, napi_handle_scope_mismatch);
}

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 +1975,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