-
Notifications
You must be signed in to change notification settings - Fork 23
Fix napi_get_property_names conformance on JSC, ChakraCore and QuickJS #218
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bkaradzic-microsoft
wants to merge
6
commits into
BabylonJS:main
Choose a base branch
from
bkaradzic-microsoft:fix/get-property-names-conformance
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
2fac54e
Fix napi_get_property_names conformance on JSC, ChakraCore and QuickJS
bkaradzic 9e3c304
Fix napi_get_prototype on JSC and skip the for...in oracle where it i…
bkaradzic 3b857e7
Implement Object::GetPropertyNames in the JSI Node-API adapter
bkaradzic c3def88
Test the ToObject coercion, and make null/undefined consistent
bkaradzic ad868a2
Skip the primitive-wrapping cases on Hermes
bkaradzic 1767446
Define JSRUNTIMEHOST_NAPI_ENGINE for the Android build too
bkaradzic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| #include "js_native_api_shared.h" | ||
|
|
||
| #include <napi/js_native_api.h> | ||
|
|
||
| #include <string> | ||
| #include <unordered_set> | ||
| #include <vector> | ||
|
|
||
| namespace napi_shared { | ||
| namespace { | ||
| #define RETURN_IF_NOT_OK(expression) \ | ||
| do { \ | ||
| const napi_status status__{(expression)}; \ | ||
| if (status__ != napi_ok) { \ | ||
| return status__; \ | ||
| } \ | ||
| } while (0) | ||
|
|
||
| napi_status GetUtf8Value(napi_env env, napi_value value, std::string& result) { | ||
| size_t length{}; | ||
| RETURN_IF_NOT_OK(napi_get_value_string_utf8(env, value, nullptr, 0, &length)); | ||
|
|
||
| std::vector<char> buffer(length + 1); | ||
| size_t copied{}; | ||
| RETURN_IF_NOT_OK(napi_get_value_string_utf8(env, value, buffer.data(), buffer.size(), &copied)); | ||
|
|
||
| result.assign(buffer.data(), copied); | ||
| return napi_ok; | ||
| } | ||
|
|
||
| napi_status IsObjectLike(napi_env env, napi_value value, bool& result) { | ||
| napi_valuetype type{}; | ||
| RETURN_IF_NOT_OK(napi_typeof(env, value, &type)); | ||
| result = (type == napi_object || type == napi_function || type == napi_external); | ||
| return napi_ok; | ||
| } | ||
|
|
||
| // Appends every element of the string array `names` to `shadowed`. | ||
| napi_status AddAll(napi_env env, napi_value names, std::unordered_set<std::string>& shadowed) { | ||
| uint32_t count{}; | ||
| RETURN_IF_NOT_OK(napi_get_array_length(env, names, &count)); | ||
|
|
||
| std::string key{}; | ||
| for (uint32_t index = 0; index < count; ++index) { | ||
| napi_value name{}; | ||
| RETURN_IF_NOT_OK(napi_get_element(env, names, index, &name)); | ||
| RETURN_IF_NOT_OK(GetUtf8Value(env, name, key)); | ||
| shadowed.insert(std::move(key)); | ||
| } | ||
|
|
||
| return napi_ok; | ||
| } | ||
| } | ||
|
|
||
| napi_status GetEnumerablePropertyNames(napi_env env, napi_value object, napi_value* result) { | ||
| // `Object.keys` reports one level's own enumerable string-keyed properties | ||
| // in specification order, which is exactly what `for...in` visits at that | ||
| // level. `Object.getOwnPropertyNames` additionally reports the | ||
| // non-enumerable ones: `for...in` does not visit those, but they still | ||
| // shadow same-named properties further up the prototype chain, so they have | ||
| // to be tracked as well. | ||
| napi_value global{}; | ||
| napi_value objectConstructor{}; | ||
| napi_value keys{}; | ||
| napi_value getOwnPropertyNames{}; | ||
| RETURN_IF_NOT_OK(napi_get_global(env, &global)); | ||
| RETURN_IF_NOT_OK(napi_get_named_property(env, global, "Object", &objectConstructor)); | ||
| RETURN_IF_NOT_OK(napi_get_named_property(env, objectConstructor, "keys", &keys)); | ||
| RETURN_IF_NOT_OK(napi_get_named_property(env, objectConstructor, "getOwnPropertyNames", &getOwnPropertyNames)); | ||
|
|
||
| napi_value names{}; | ||
| RETURN_IF_NOT_OK(napi_create_array(env, &names)); | ||
| uint32_t nameCount{}; | ||
|
|
||
| std::unordered_set<std::string> shadowed{}; | ||
| std::string key{}; | ||
|
|
||
| // `ToObject` is what the specification (and the V8 implementation) applies | ||
| // to the argument, so a primitive is wrapped and its properties reported. | ||
| // `null` and `undefined` have no wrapper, and V8 reports that as | ||
| // `napi_object_expected`; check explicitly rather than relying on | ||
| // `napi_coerce_to_object`, whose behaviour for those two values differs | ||
| // between engines (QuickJS yields an empty object, JavaScriptCore throws). | ||
| napi_valuetype type{}; | ||
| RETURN_IF_NOT_OK(napi_typeof(env, object, &type)); | ||
| if (type == napi_null || type == napi_undefined) { | ||
| return napi_object_expected; | ||
| } | ||
|
|
||
| napi_value current{}; | ||
| RETURN_IF_NOT_OK(napi_coerce_to_object(env, object, ¤t)); | ||
|
|
||
| while (true) { | ||
| bool isObjectLike{}; | ||
| RETURN_IF_NOT_OK(IsObjectLike(env, current, isObjectLike)); | ||
| if (!isObjectLike) { | ||
| break; | ||
| } | ||
|
|
||
| napi_value ownEnumerableNames{}; | ||
| RETURN_IF_NOT_OK(napi_call_function(env, objectConstructor, keys, 1, ¤t, &ownEnumerableNames)); | ||
|
|
||
| uint32_t ownEnumerableCount{}; | ||
| RETURN_IF_NOT_OK(napi_get_array_length(env, ownEnumerableNames, &ownEnumerableCount)); | ||
| for (uint32_t index = 0; index < ownEnumerableCount; ++index) { | ||
| napi_value name{}; | ||
| RETURN_IF_NOT_OK(napi_get_element(env, ownEnumerableNames, index, &name)); | ||
| RETURN_IF_NOT_OK(GetUtf8Value(env, name, key)); | ||
| if (shadowed.find(key) == shadowed.end()) { | ||
| RETURN_IF_NOT_OK(napi_set_element(env, names, nameCount++, name)); | ||
| } | ||
| } | ||
|
|
||
| napi_value ownNames{}; | ||
| RETURN_IF_NOT_OK(napi_call_function(env, objectConstructor, getOwnPropertyNames, 1, ¤t, &ownNames)); | ||
| RETURN_IF_NOT_OK(AddAll(env, ownNames, shadowed)); | ||
|
|
||
| RETURN_IF_NOT_OK(napi_get_prototype(env, current, ¤t)); | ||
| } | ||
|
|
||
| *result = names; | ||
| return napi_ok; | ||
| } | ||
|
|
||
| #undef RETURN_IF_NOT_OK | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,22 @@ | ||||||||||||||||||
| #pragma once | ||||||||||||||||||
|
|
||||||||||||||||||
| #include <napi/js_native_api_types.h> | ||||||||||||||||||
|
|
||||||||||||||||||
| // Engine-agnostic pieces of the Node-API surface, implemented purely in terms | ||||||||||||||||||
| // of the public `napi_*` entry points so that every backend behaves the same. | ||||||||||||||||||
| // Backends whose engine offers a faithful native equivalent should keep using | ||||||||||||||||||
| // it; these helpers exist for the ones that do not. | ||||||||||||||||||
| namespace napi_shared { | ||||||||||||||||||
| // Implements `napi_get_property_names` semantics: the names of all | ||||||||||||||||||
| // enumerable string-keyed properties of `object` and of its prototype chain, | ||||||||||||||||||
| // as an array of strings, matching a `for...in` enumeration. | ||||||||||||||||||
| // | ||||||||||||||||||
| // V8 gets this from a single `GetPropertyNames` call configured with | ||||||||||||||||||
| // `kIncludePrototypes | ONLY_ENUMERABLE | SKIP_SYMBOLS`. JavaScriptCore, | ||||||||||||||||||
| // ChakraCore and QuickJS have no equivalent, so this walks the prototype | ||||||||||||||||||
| // chain explicitly. See https://github.com/BabylonJS/JsRuntimeHost/issues/216. | ||||||||||||||||||
|
Comment on lines
+14
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Reviewed by Copilot on behalf of @bghgary] This is Chakra, not ChakraCore - the backend links
Suggested change
|
||||||||||||||||||
| // | ||||||||||||||||||
| // `object` is coerced with `napi_coerce_to_object`, as V8's `CHECK_TO_OBJECT` | ||||||||||||||||||
| // does. Callers are expected to have already validated `env` and `result`. | ||||||||||||||||||
| napi_status GetEnumerablePropertyNames(napi_env env, napi_value object, napi_value* result); | ||||||||||||||||||
| } | ||||||||||||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Reviewed by Copilot on behalf of @bghgary]
shadowedis rebuilt at the last level too, where nothing can consult it - for{a: 1, b: 2}that is 12 of 16 name materializations wasted onObject.prototype, each costing twonapi_get_value_string_utf8calls and an allocation.Fetching the prototype first and only populating
shadowedwhen the walk continues avoids it:napi_value next{}; RETURN_IF_NOT_OK(napi_get_prototype(env, current, &next)); bool hasNextLevel{}; RETURN_IF_NOT_OK(IsObjectLike(env, next, hasNextLevel)); if (hasNextLevel) { napi_value ownNames{}; RETURN_IF_NOT_OK(napi_call_function(env, objectConstructor, getOwnPropertyNames, 1, ¤t, &ownNames)); RETURN_IF_NOT_OK(AddAll(env, ownNames, shadowed)); } current = next;Nit.