Skip to content

napi_get_property_names: throws on JavaScriptCore; inconsistent enumerability/prototype semantics on Chakra and QuickJS #216

Description

@bkaradzic-microsoft

napi_get_property_names behaves differently on all three non-V8 backends, and throws unconditionally on JavaScriptCore. Since Napi::Object::GetPropertyNames() is a direct wrapper, that method is unusable on JSC for every N-API caller — and JSC is the default engine on macOS and iOS.

Reference behaviour (V8)

js_native_api_v8.cc matches the Node-API specification: enumerable, string-keyed properties, including the prototype chain.

obj->GetPropertyNames(
    context,
    v8::KeyCollectionMode::kIncludePrototypes,
    static_cast<v8::PropertyFilter>(
        v8::PropertyFilter::ONLY_ENUMERABLE |
        v8::PropertyFilter::SKIP_SYMBOLS),
    v8::IndexFilter::kIncludeIndices,
    v8::KeyConversionMode::kConvertToString);

JavaScriptCore — throws

Core/Node-API/Source/js_native_api_javascriptcore.cc:

CHECK_NAPI(napi_get_named_property(env, object_ctor, "getOwnPropertyNames", &function));
CHECK_NAPI(napi_call_function(env, object_ctor, function, 0, nullptr, result));

The object parameter is never used. The call passes argc 0 / argv nullptr, so it evaluates Object.getOwnPropertyNames(undefined), which throws TypeError. Note there is also no CHECK_ARG(env, object).

Passing the object is necessary but not sufficient. Object.getOwnPropertyNames is own-only and includes non-enumerable properties, so it disagrees with V8 on both axes. A conforming JSC implementation needs enumerable properties across the prototype chain — the semantics of for...in filtered to string keys.

Chakra — own-only, includes non-enumerables

js_native_api_chakra.cc uses JsGetOwnPropertyNames, which is own-only and does not filter to enumerable properties. Wrong on both axes, though it does not throw.

QuickJS — own-only

js_native_api_quickjs.cc uses JS_GetOwnPropertyNames with JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY. Enumerability is correct; the prototype chain is missing.

Summary

backend enumerable-only includes prototype chain throws
V8 yes yes no
JavaScriptCore n/a n/a yes
Chakra no no no
QuickJS yes no no

Repro

const proto = { inherited: 1 };
const obj = Object.create(proto);
obj.own = 2;
Object.defineProperty(obj, "hidden", { value: 3, enumerable: false });
// napi_get_property_names(obj) should yield exactly ["own", "inherited"].

On JSC this throws instead of returning. On Chakra it yields ["own", "hidden"]. On QuickJS it yields ["own"].

Impact / workaround

Encountered in Babylon Native while enumerating a plain JS data object (BabylonJS/BabylonNative#1797). The workaround is to fetch Object.keys from the global and call it via napi_call_function, which behaves consistently across engines for plain data objects:

const auto objectCtor = env.Global().Get("Object").As<Napi::Object>();
const auto keys = objectCtor.Get("keys").As<Napi::Function>();

That is only equivalent for own-enumerable cases, so it is a local workaround rather than a fix.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions