Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2c32a8f
Fix napi_get_property_names conformance on JSC, ChakraCore and QuickJS
bkaradzic Jul 30, 2026
8c5077f
Fix napi_get_prototype on JSC and skip the for...in oracle where it i…
bkaradzic Jul 30, 2026
c8d1418
Implement Object::GetPropertyNames in the JSI Node-API adapter
bkaradzic Jul 30, 2026
d02a1f1
Test the ToObject coercion, and make null/undefined consistent
bkaradzic Jul 30, 2026
1d3855b
Skip the primitive-wrapping cases on Hermes
bkaradzic Jul 30, 2026
fab5e24
Address property-name review feedback
Copilot Jul 31, 2026
2c45610
Terminate the prototype walk on a cycle and report last_error consist…
bkaradzic Jul 31, 2026
747bd1d
Scope the new property-name tests to the backends they describe
bkaradzic Jul 31, 2026
9ac6431
Coerce the argument in napi_get_prototype on JavaScriptCore
bkaradzic Jul 31, 2026
af17208
Correct the JSObjectCopyPropertyNames comment
bkaradzic Jul 31, 2026
6ee4f42
Reuse upstream engine test plumbing
Copilot Sep 14, 2026
2bb1fdd
Match for-in proxy enumeration semantics
Sep 22, 2026
eaf8cfe
Capture property enumeration intrinsics at runtime attachment
Sep 24, 2026
3daeba9
Release Chakra intrinsic references before runtime disposal
Sep 24, 2026
0445f7d
Trace first property enumeration call for Chakra CI diagnosis
Sep 24, 2026
b83e3c8
Record Chakra property-name length before copying
Sep 24, 2026
b8d98d0
Bound Chakra UTF-16 copies in bytes and report copied length
Sep 24, 2026
4de2e56
Use the global object in Chakra property-name regressions
Sep 24, 2026
5e37159
Merge current main and move property-name tests into feature files
bghgary Sep 25, 2026
e32c642
Own cached references across runtime teardown
bghgary Sep 25, 2026
77fe3b8
Name property-name tests by API rather than issue
bghgary Sep 25, 2026
f0ac600
Guard JavaScriptCore and QuickJS attachment with unique ownership
bghgary Sep 25, 2026
1af8328
Keep Chakra embedding teardown to Attach and Detach
bghgary Sep 28, 2026
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
2 changes: 1 addition & 1 deletion Core/AppRuntime/Source/AppRuntime_Chakra.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ namespace Babylon
ThrowIfFailed(JsSetCurrentContext(JS_INVALID_REFERENCE));
ThrowIfFailed(JsDisposeRuntime(jsRuntime));

// Detach must come after JsDisposeRuntime since it triggers finalizers which require env.
// JsDisposeRuntime runs finalizers which require env, so detach afterward.
Napi::Detach(env);
}

Expand Down
5 changes: 4 additions & 1 deletion Core/Node-API-JSI/Include/napi/napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,10 @@ inline bool Object::Delete(uint32_t index) {
}

inline Array Object::GetPropertyNames() const {
throw std::runtime_error{"TODO"};
// `jsi::Object::getPropertyNames` returns the enumerable string-keyed
// properties of this object and of its prototype chain, which is exactly what
// `napi_get_property_names` is specified to produce.
return {_env, _object->getPropertyNames(_env->rt)};
}

// TODO: not implemented
Expand Down
12 changes: 9 additions & 3 deletions Core/Node-API/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,27 @@ if(NAPI_BUILD_ABI)
set(SOURCES ${SOURCES}
"Source/env_quickjs.cc"
"Source/js_native_api_quickjs.cc"
"Source/js_native_api_quickjs.h")
"Source/js_native_api_quickjs.h"
"Source/js_native_api_shared.cc"
"Source/js_native_api_shared.h")
set(LINK_LIBRARIES ${LINK_LIBRARIES} PUBLIC qjs)
elseif(NAPI_JAVASCRIPT_ENGINE STREQUAL "Chakra")
set(SOURCES ${SOURCES}
"Source/env_chakra.cc"
"Source/js_native_api_chakra.cc"
"Source/js_native_api_chakra.h")
"Source/js_native_api_chakra.h"
"Source/js_native_api_shared.cc"
"Source/js_native_api_shared.h")

set(LINK_LIBRARIES ${LINK_LIBRARIES}
INTERFACE "chakrart.lib")
elseif(NAPI_JAVASCRIPT_ENGINE STREQUAL "JavaScriptCore")
set(SOURCES ${SOURCES}
"Source/env_javascriptcore.cc"
"Source/js_native_api_javascriptcore.cc"
"Source/js_native_api_javascriptcore.h")
"Source/js_native_api_javascriptcore.h"
"Source/js_native_api_shared.cc"
"Source/js_native_api_shared.h")

if(ANDROID)
set(V8_PACKAGE_NAME "jsc-android")
Expand Down
100 changes: 81 additions & 19 deletions Core/Node-API/Source/env_chakra.cc
Original file line number Diff line number Diff line change
@@ -1,49 +1,111 @@
#include <napi/env.h>
#include "js_native_api_chakra.h"
#include <jsrt.h>
#include <array>
#include <exception>
#include <memory>
#include <stdexcept>
#include <strsafe.h>

namespace
{
std::array<napi_ref*, 6> CachedReferences(napi_env env)
{
auto& intrinsics{env->property_name_intrinsics};
return {&intrinsics.object_constructor, &intrinsics.own_names,
&intrinsics.own_descriptor, &intrinsics.prototype,
&env->has_own_property_reference, &env->wrap_symbol_reference};
}

void ThrowIfFailed(JsErrorCode errorCode)
{
if (errorCode != JsErrorCode::JsNoError)
{
throw std::exception();
}
}

napi_status ReleaseCachedReferences(napi_env env)
{
napi_status firstError{napi_ok};
for (napi_ref* ref : CachedReferences(env))
{
if (*ref != nullptr)
{
const napi_status status{napi_delete_reference(env, *ref)};
if (status == napi_ok)
{
*ref = nullptr;
}
else if (firstError == napi_ok)
{
firstError = status;
}
}
}
return firstError;
}
}

namespace Napi
{
Env Attach()
{
napi_env env_ptr{new napi_env__};

JsValueRef global;
ThrowIfFailed(JsGetGlobalObject(&global));
JsPropertyIdRef propertyId;
ThrowIfFailed(JsGetPropertyIdFromName(L"Object", &propertyId));
JsValueRef object;
ThrowIfFailed(JsGetProperty(global, propertyId, &object));
JsValueRef prototype;
ThrowIfFailed(JsGetPrototype(object, &prototype));
ThrowIfFailed(JsGetPropertyIdFromName(L"hasOwnProperty", &propertyId));
ThrowIfFailed(JsGetProperty(prototype, propertyId, &env_ptr->has_own_property_function));
auto env_ptr{std::make_unique<napi_env__>()};
try
{
JsValueRef global;
ThrowIfFailed(JsGetGlobalObject(&global));
JsPropertyIdRef propertyId;
ThrowIfFailed(JsGetPropertyIdFromName(L"Object", &propertyId));
JsValueRef object;
ThrowIfFailed(JsGetProperty(global, propertyId, &object));
JsValueRef prototype;
ThrowIfFailed(JsGetPrototype(object, &prototype));
ThrowIfFailed(JsGetPropertyIdFromName(L"hasOwnProperty", &propertyId));
ThrowIfFailed(JsGetProperty(prototype, propertyId, &env_ptr->has_own_property_function));
if (napi_create_reference(env_ptr.get(), reinterpret_cast<napi_value>(env_ptr->has_own_property_function), 1, &env_ptr->has_own_property_reference) != napi_ok)
{
throw std::runtime_error{"Napi::Attach: failed to retain hasOwnProperty"};
}
if (napi_shared::CapturePropertyNameIntrinsics(env_ptr.get(), env_ptr->property_name_intrinsics) != napi_ok)
{
throw std::runtime_error{"Napi::Attach: failed to capture property-name intrinsics"};
}

JsValueRef wrapSymbolDescription;
ThrowIfFailed(JsPointerToString(L"BabylonNative_External", 22, &wrapSymbolDescription));
JsValueRef wrapSymbol;
ThrowIfFailed(JsCreateSymbol(wrapSymbolDescription, &wrapSymbol));
ThrowIfFailed(JsAddRef(wrapSymbol, nullptr));
ThrowIfFailed(JsGetPropertyIdFromSymbol(wrapSymbol, &env_ptr->wrap_property_id));
JsValueRef wrapSymbolDescription;
ThrowIfFailed(JsPointerToString(L"BabylonNative_External", 22, &wrapSymbolDescription));
JsValueRef wrapSymbol;
ThrowIfFailed(JsCreateSymbol(wrapSymbolDescription, &wrapSymbol));
if (napi_create_reference(env_ptr.get(), reinterpret_cast<napi_value>(wrapSymbol), 1, &env_ptr->wrap_symbol_reference) != napi_ok)
{
throw std::runtime_error{"Napi::Attach: failed to retain wrap symbol"};
}
ThrowIfFailed(JsGetPropertyIdFromSymbol(wrapSymbol, &env_ptr->wrap_property_id));

return {env_ptr};
return {env_ptr.release()};
}
catch (...)
{
if (ReleaseCachedReferences(env_ptr.get()) != napi_ok)
{
std::throw_with_nested(std::runtime_error{"Napi::Attach: failed to release cached references"});
}
throw;
}
}

void Detach(Env env)
{
napi_env env_ptr{env};
for (napi_ref* ref : CachedReferences(env_ptr))
{
if (*ref != nullptr)
{
napi_chakra_internal::DiscardReferenceAfterRuntimeDisposal(*ref);
*ref = nullptr;
}
}
delete env_ptr;
}
}
14 changes: 12 additions & 2 deletions Core/Node-API/Source/env_javascriptcore.cc
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
#include <napi/env.h>
#include <napi/js_native_api_types.h>
#include "js_native_api_javascriptcore.h"
#include <memory>
#include <stdexcept>

namespace Napi
{
Napi::Env Attach(JSGlobalContextRef context)
{
napi_env env_ptr{new napi_env__{context}};
return {env_ptr};
auto env_ptr{std::make_unique<napi_env__>(context)};
if (napi_shared::CapturePropertyNameIntrinsics(env_ptr.get(), env_ptr->property_name_intrinsics) != napi_ok)
{
throw std::runtime_error{"Napi::Attach: failed to capture property-name intrinsics"};
}
return {env_ptr.release()};
}

void Detach(Napi::Env env)
{
napi_env env_ptr{env};
if (napi_shared::ReleasePropertyNameIntrinsics(env_ptr, env_ptr->property_name_intrinsics) != napi_ok)
{
throw std::runtime_error{"Napi::Detach: failed to release property-name intrinsics"};
}
delete env_ptr;
}

Expand Down
17 changes: 12 additions & 5 deletions Core/Node-API/Source/env_quickjs.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <napi/env.h>
#include "js_native_api_quickjs.h"
#include <memory>
#include <stdexcept>
#if defined(__clang__)
#pragma clang diagnostic push
Expand All @@ -14,7 +15,7 @@ namespace Napi
{
Env Attach(JSContext* context)
{
napi_env env_ptr{new napi_env__};
auto env_ptr{std::make_unique<napi_env__>()};
env_ptr->context = context;
env_ptr->current_context = env_ptr->context;

Expand All @@ -29,7 +30,6 @@ namespace Napi
if (JS_IsException(object) || !JS_IsObject(object))
{
JS_FreeValue(context, object);
delete env_ptr;
throw std::runtime_error{"Napi::Attach: failed to resolve the global 'Object' constructor"};
}

Expand All @@ -42,7 +42,6 @@ namespace Napi
if (JS_IsException(prototype) || !JS_IsObject(prototype))
{
JS_FreeValue(context, prototype);
delete env_ptr;
throw std::runtime_error{"Napi::Attach: failed to resolve Object.prototype"};
}

Expand All @@ -51,20 +50,28 @@ namespace Napi
if (JS_IsException(hasOwnProperty) || !JS_IsFunction(context, hasOwnProperty))
{
JS_FreeValue(context, hasOwnProperty);
delete env_ptr;
throw std::runtime_error{"Napi::Attach: failed to resolve Object.prototype.hasOwnProperty"};
}

env_ptr->has_own_property_function = hasOwnProperty;
if (napi_shared::CapturePropertyNameIntrinsics(env_ptr.get(), env_ptr->property_name_intrinsics) != napi_ok)
{
JS_FreeValue(context, hasOwnProperty);
throw std::runtime_error{"Napi::Attach: failed to capture property-name intrinsics"};
}

return {env_ptr};
return {env_ptr.release()};
}

void Detach(Env env)
{
napi_env env_ptr{env};
if (env_ptr)
{
if (napi_shared::ReleasePropertyNameIntrinsics(env_ptr, env_ptr->property_name_intrinsics) != napi_ok)
{
throw std::runtime_error{"Napi::Detach: failed to release property-name intrinsics"};
}
// Release every strong napi_ref still outstanding. This mirrors
// the V8 impl (napi_env__::DeleteMe) and is essential on QuickJS:
// any surviving strong ref pins a JS value from outside the GC
Expand Down
44 changes: 31 additions & 13 deletions Core/Node-API/Source/js_native_api_chakra.cc
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
#include "js_native_api_chakra.h"
#include "js_native_api_shared.h"
#include <napi/js_native_api.h>
#include <algorithm>
#include <array>
#include <cassert>
#include <cmath>
#include <cstring>
#include <memory>
#include <optional>
#include <vector>
#include <string>
Expand Down Expand Up @@ -48,13 +52,14 @@ JsErrorCode JsCopyStringUtf16(_In_ JsValueRef value, _Out_opt_ char16_t* buffer,
size_t stringLength;
CHECK_JSRT_ERROR_CODE(JsStringToPointer(value, &stringValue, &stringLength));

const size_t copied = buffer == nullptr ? stringLength : std::min(bufferSize, stringLength);
if (length != nullptr) {
*length = stringLength;
*length = copied;
}

if (buffer != nullptr) {
if (buffer != nullptr && copied != 0) {
static_assert(sizeof(char16_t) == sizeof(wchar_t));
memcpy_s(buffer, bufferSize, stringValue, stringLength * sizeof(wchar_t));
std::memcpy(buffer, stringValue, copied * sizeof(char16_t));
}

return JsErrorCode::JsNoError;
Expand Down Expand Up @@ -509,6 +514,12 @@ napi_status DefineProperty(napi_env env,

} // end anonymous namespace

void napi_chakra_internal::DiscardReferenceAfterRuntimeDisposal(napi_ref ref)
{
// The runtime has freed its JS values; JsRelease would access invalid handles.
delete reinterpret_cast<RefInfo*>(ref);
}

// Warning: Keep in-sync with napi_status enum
static const char* error_messages[] = {
nullptr,
Expand Down Expand Up @@ -678,11 +689,22 @@ napi_status napi_get_property_names(napi_env env,
napi_value object,
napi_value* result) {
CHECK_ENV(env);
CHECK_ARG(env, object);
CHECK_ARG(env, result);
JsValueRef obj = reinterpret_cast<JsValueRef>(object);
JsValueRef propertyNames;
CHECK_JSRT(env, JsGetOwnPropertyNames(obj, &propertyNames));
*result = reinterpret_cast<napi_value>(propertyNames);

// `JsGetOwnPropertyNames` is own-only and includes non-enumerable properties,
// so use the shared prototype-chain walk instead. It is written against the
// public `napi_*` surface and so cannot reach `napi_set_last_error`; do it
// here, since `CHECK_NAPI` only propagates the status and the preceding call
// inside the walk will have cleared the last error. The success path likewise
// has to clear it, so that a rejection recorded by an earlier call does not
// survive as the last error of a call that succeeded.
const napi_status status{napi_shared::GetEnumerablePropertyNames(env, object, result, env->property_name_intrinsics)};
if (status != napi_ok) {
return napi_set_last_error(env, status);
}

napi_clear_last_error(env);
return napi_ok;
}

Expand Down Expand Up @@ -1825,17 +1847,13 @@ napi_status napi_create_reference(napi_env env,
CHECK_ARG(env, result);

auto jsValue = reinterpret_cast<JsValueRef>(value);
auto info = new RefInfo{ reinterpret_cast<JsValueRef>(value), initial_refcount };
if (info == nullptr) {
return napi_set_last_error(env, napi_generic_failure);
}

std::unique_ptr<RefInfo> info{new RefInfo{jsValue, initial_refcount}};
if (info->count != 0)
{
CHECK_JSRT(env, JsAddRef(jsValue, nullptr));
}

*result = reinterpret_cast<napi_ref>(info);
*result = reinterpret_cast<napi_ref>(info.release());
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 @@ -5,16 +5,24 @@

#include <jsrt.h>
#include <napi/js_native_api_types.h>
#include "js_native_api_shared.h"
#include <thread>
#include <cassert>
#include <map>

namespace napi_chakra_internal {
void DiscardReferenceAfterRuntimeDisposal(napi_ref ref);
}

struct napi_env__ {
JsSourceContext source_context = JS_SOURCE_CONTEXT_NONE;
napi_extended_error_info last_error{ nullptr, nullptr, 0, napi_ok };
JsValueRef has_own_property_function = JS_INVALID_REFERENCE;
napi_ref has_own_property_reference{};
napi_shared::PropertyNameIntrinsics property_name_intrinsics{};

JsPropertyIdRef wrap_property_id = JS_INVALID_REFERENCE;
napi_ref wrap_symbol_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
Expand Down
Loading
Loading