Skip to content
Open
Show file tree
Hide file tree
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
9 changes: 9 additions & 0 deletions Core/Node-API/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,15 @@ add_library(napi ${SOURCES})
target_include_directories(napi ${INCLUDE_DIRECTORIES})
target_link_libraries(napi ${LINK_LIBRARIES})

# The Chakra and JavaScriptCore shims implement napi_escape_handle as a pass
# through that returns the escapee without tracking scopes, so they always report
# napi_ok and cannot report napi_escape_called_twice. Published as an INTERFACE
# definition so every consumer sees it without each test target repeating the
# engine check.
if(NAPI_JAVASCRIPT_ENGINE STREQUAL "Chakra" OR NAPI_JAVASCRIPT_ENGINE STREQUAL "JavaScriptCore")
target_compile_definitions(napi INTERFACE JSRUNTIMEHOST_NAPI_ESCAPE_HANDLE_IS_PASSTHROUGH)
endif()

if(NAPI_JAVASCRIPT_ENGINE STREQUAL "Hermes")
# Apply Hermes-specific warning suppressions ONLY to env_hermes.cc so
# they don't relax the rules for the rest of the napi sources.
Expand Down
8 changes: 8 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,14 @@ 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->escaped_handles)
{
JS_FreeValue(env_ptr->context, *entry.second);
}
env_ptr->escaped_handles.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
58 changes: 25 additions & 33 deletions Core/Node-API/Source/js_native_api_quickjs.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1929,14 +1929,24 @@ 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;

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.
const auto escaped = env->escaped_handles.find(scope_start);
if (escaped != env->escaped_handles.end()) {
env->handle_scope_stack.push_back(std::move(escaped->second));
env->escaped_handles.erase(escaped);
}

env->current_scope_start = scope_start;

napi_clear_last_error(env);
Expand All @@ -1952,40 +1962,22 @@ napi_status napi_escape_handle(napi_env env, napi_escapable_handle_scope scope,
// Get the scope start index
size_t scope_start = reinterpret_cast<size_t>(scope) - 1;

// 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 (env->escaped_handles.find(scope_start) != env->escaped_handles.end()) {
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));

// Hold the handle aside until the scope closes, rather than inserting it into
// handle_scope_stack here. An insert would shift every entry above scope_start,
// which silently invalidates the recorded start of any nested scope that is still
// open -- closing that scope would then keep the wrong slot and free this handle.
auto holder = std::make_unique<JSValue>(escapedValue);
*result = reinterpret_cast<napi_value>(holder.get());
env->escaped_handles.emplace(scope_start, std::move(holder));

napi_clear_last_error(env);
return napi_ok;
}
Expand Down
10 changes: 10 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,15 @@ struct napi_env__ {
std::vector<std::unique_ptr<JSValue>> handle_scope_stack;
size_t current_scope_start = 0;

// Handles escaped by napi_escape_handle, keyed by the scope start recorded by
// napi_open_escapable_handle_scope. They are deliberately held aside rather than
// put on handle_scope_stack: inserting into the middle of the stack would shift
// every entry above it, invalidating the indices that already-open nested scopes
// and their opaque tokens are built from. napi_close_escapable_handle_scope
// pushes the handle 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.
std::map<size_t, std::unique_ptr<JSValue>> escaped_handles;

// 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