BabylonJS / BabylonJS/JsRuntimeHost
napi_get_property_names: throws on JavaScriptCore; inconsistent enumerability/prototype semantics on Chakra and QuickJS
- 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
Assessment
This issue has not been assessed yet.