BabylonJS / BabylonJS/JsRuntimeHost

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

Open
#216 0 comments 0 reactions 1 assignee Claimed by @bkaradzic-microsoft View on GitHub
bug
Dominant language
C++
Stars
22
Forks
23
Avg merge
3d 14h
Merged PRs (30d)
6

Description

`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**.

```cpp
obj->GetPropertyNames(
context,
v8::KeyCollectionMode::kIncludePrototypes,
static_cast(
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`:

```cpp
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

```js
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:

```cpp
const auto objectCtor = env.Global().Get("Object").As();
const auto keys = objectCtor.Get("keys").As();
```

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

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.