-
Notifications
You must be signed in to change notification settings - Fork 23
Fix napi_get_property_names conformance on JSC, Chakra 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
11
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
11 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 cbe9c16
Address property-name review feedback
Copilot 0d7ab06
Terminate the prototype walk on a cycle and report last_error consist…
bkaradzic efc20cb
Scope the new property-name tests to the backends they describe
bkaradzic fdd36a9
Coerce the argument in napi_get_prototype on JavaScriptCore
bkaradzic 26a4a99
Correct the JSObjectCopyPropertyNames comment
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,167 @@ | ||
| #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; | ||
| } | ||
|
|
||
| // Whether `value` is strictly equal to something already in `seen`. | ||
| napi_status Contains(napi_env env, const std::vector<napi_value>& seen, napi_value value, bool& result) { | ||
| for (const napi_value candidate : seen) { | ||
| bool equal{}; | ||
| RETURN_IF_NOT_OK(napi_strict_equals(env, candidate, value, &equal)); | ||
| if (equal) { | ||
| result = true; | ||
| return napi_ok; | ||
| } | ||
| } | ||
|
|
||
| result = false; | ||
| 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)); | ||
|
bkaradzic-microsoft marked this conversation as resolved.
|
||
| 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::vector<napi_value> visited{}; | ||
| 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; | ||
| } | ||
|
|
||
| // A `getPrototypeOf` Proxy trap can return an object that is already on | ||
| // the chain -- nothing in the specification forbids it, so | ||
| // `Object.getPrototypeOf(p) === p` is reachable from script -- which | ||
| // makes this walk cyclic. V8 recurses and so terminates with a | ||
| // `RangeError`; this loop is iterative and would spin forever. | ||
| // | ||
| // Stopping at the repeat is exact rather than a bail-out: every level | ||
| // adds its own property names to `shadowed` before the walk continues, | ||
| // so a level visited a second time can only re-encounter names that are | ||
| // already shadowed. Breaking here therefore yields the same result the | ||
| // non-terminating walk converges on. | ||
| bool alreadyVisited{}; | ||
| RETURN_IF_NOT_OK(Contains(env, visited, current, alreadyVisited)); | ||
| if (alreadyVisited) { | ||
| break; | ||
| } | ||
| visited.push_back(current); | ||
|
|
||
| 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 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; | ||
| } | ||
|
|
||
| *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, | ||
| // Chakra and QuickJS have no equivalent, so this walks the prototype | ||
| // chain explicitly. See https://github.com/BabylonJS/JsRuntimeHost/issues/216. | ||
|
bkaradzic-microsoft marked this conversation as resolved.
|
||
| // | ||
| // `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
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.
Uh oh!
There was an error while loading. Please reload this page.