From 90263538e7fd1d50b7d48e7d23eaf372cfa2e99e Mon Sep 17 00:00:00 2001 From: Tomasz Sapeta Date: Fri, 13 Nov 2026 11:27:09 +0100 Subject: [PATCH] Add Object::getPropertyNameIDs to JSI getPropertyNameIDs returns the names of the enumerable properties of an object as PropNameIDs, in the order of getPropertyNames, without creating a JS array. It's an IRuntime method with a default implementation in jsi::Runtime, built on getPropertyNames, like getStringData. The decorators forward it, and TracingRuntime uses the default implementation, so traces record the calls that it's equivalent to. Hermes keeps the for-in names of ordinary objects as symbols, so it returns them directly instead of converting them to strings, and the property values can be read with getProperty(PropNameID) without converting the names back. --- API/hermes/TracingRuntime.h | 7 ++++++ API/hermes/hermes.cpp | 45 ++++++++++++++++++++++++++++++++++++ API/jsi/jsi/decorator.h | 7 ++++++ API/jsi/jsi/jsi-inl.h | 5 ++++ API/jsi/jsi/jsi.cpp | 12 ++++++++++ API/jsi/jsi/jsi.h | 9 ++++++++ API/jsi/jsi/test/testlib.cpp | 41 ++++++++++++++++++++++++++++++++ 7 files changed, 126 insertions(+) diff --git a/API/hermes/TracingRuntime.h b/API/hermes/TracingRuntime.h index 59c72b17089..c7c455889a8 100644 --- a/API/hermes/TracingRuntime.h +++ b/API/hermes/TracingRuntime.h @@ -140,6 +140,13 @@ class TracingRuntime : public jsi::RuntimeDecorator jsi::Value getPrototypeOf(const jsi::Object &object) override; jsi::Array getPropertyNames(const jsi::Object &o) override; + // Uses the default implementation of jsi::Runtime, which calls the traced + // getPropertyNames and the other traced methods, so replay sees the same + // calls. + std::vector getPropertyNameIDs( + const jsi::Object &o) override { + return jsi::Runtime::getPropertyNameIDs(o); + } jsi::WeakObject createWeakObject(const jsi::Object &o) override; diff --git a/API/hermes/hermes.cpp b/API/hermes/hermes.cpp index 0f96516c5f2..370de573d4d 100644 --- a/API/hermes/hermes.cpp +++ b/API/hermes/hermes.cpp @@ -882,6 +882,8 @@ class HermesRuntimeImpl final : public HermesRuntime, bool isHostObject(const jsi::Object &) const override; bool isHostFunction(const jsi::Function &) const override; jsi::Array getPropertyNames(const jsi::Object &) override; + std::vector getPropertyNameIDs( + const jsi::Object &obj) override; void setPrototypeOf(const jsi::Object &object, const jsi::Value &prototype) override; @@ -3118,6 +3120,49 @@ jsi::Array HermesRuntimeImpl::getPropertyNames(const jsi::Object &obj) { return ret; } +std::vector HermesRuntimeImpl::getPropertyNameIDs( + const jsi::Object &obj) { + ExecutionScopeRAII scopeRAII(mutatorScope); + vm::GCScope gcScope(runtime_); + uint32_t beginIndex; + uint32_t endIndex; + vm::CallResult> cr = + vm::getForInPropertyNames(runtime_, handle(obj), beginIndex, endIndex); + checkStatus(cr.getStatus()); + vm::Handle names = *cr; + + std::vector result; + result.reserve(endIndex - beginIndex); + for (uint32_t i = beginIndex; i < endIndex; ++i) { + vm::GCScopeMarkerRAII marker{runtime_}; + vm::SmallHermesValue name = names->at(i); + if (name.isSymbol()) { + // for-in keeps the names of ordinary objects as symbols, which are what + // a PropNameID holds. + result.push_back(add( + vm::HermesValue::encodeSymbolValue(name.getSymbol()))); + continue; + } + vm::StringPrimitive *str; + if (name.isString()) { + str = name.getString(runtime_); + } else if (name.isNumber()) { + // May allocate. 'name' must not be used afterwards. + auto strRes = + vm::numberToStringPrimitive(runtime_, name.getNumber(runtime_)); + checkStatus(strRes.getStatus()); + str = strRes->get(); + } else { + llvm_unreachable("property name is not String, Symbol or Number"); + } + auto symbolRes = + vm::stringToSymbolID(runtime_, vm::createPseudoHandle(str)); + checkStatus(symbolRes.getStatus()); + result.push_back(add(symbolRes->getHermesValue())); + } + return result; +} + jsi::WeakObject HermesRuntimeImpl::createWeakObject(const jsi::Object &obj) { ExecutionScopeRAII scopeRAII(mutatorScope); return addWeak( diff --git a/API/jsi/jsi/decorator.h b/API/jsi/jsi/decorator.h index f91720b4194..5ee17d5153e 100644 --- a/API/jsi/jsi/decorator.h +++ b/API/jsi/jsi/decorator.h @@ -403,6 +403,9 @@ class RuntimeDecorator : public Base, private jsi::Instrumentation { Array getPropertyNames(const Object& o) override { return plain_.getPropertyNames(o); } + std::vector getPropertyNameIDs(const Object& o) override { + return plain_.getPropertyNameIDs(o); + } WeakObject createWeakObject(const Object& o) override { return plain_.createWeakObject(o); @@ -1043,6 +1046,10 @@ class WithRuntimeDecorator : public RuntimeDecorator { Around around{with_}; return RD::getPropertyNames(o); } + std::vector getPropertyNameIDs(const Object& o) override { + Around around{with_}; + return RD::getPropertyNameIDs(o); + } WeakObject createWeakObject(const Object& o) override { Around around{with_}; diff --git a/API/jsi/jsi/jsi-inl.h b/API/jsi/jsi/jsi-inl.h index 6a29f64798b..b5323b9ff54 100644 --- a/API/jsi/jsi/jsi-inl.h +++ b/API/jsi/jsi/jsi-inl.h @@ -297,6 +297,11 @@ inline Array Object::getPropertyNames(IRuntime& runtime) const { return runtime.getPropertyNames(*this); } +inline std::vector Object::getPropertyNameIDs( + IRuntime& runtime) const { + return runtime.getPropertyNameIDs(*this); +} + inline Value WeakObject::lock(IRuntime& runtime) const { return runtime.lockWeakObject(*this); } diff --git a/API/jsi/jsi/jsi.cpp b/API/jsi/jsi/jsi.cpp index 521001a8980..726ae8cf370 100644 --- a/API/jsi/jsi/jsi.cpp +++ b/API/jsi/jsi/jsi.cpp @@ -459,6 +459,18 @@ void Runtime::deleteProperty(const Object& object, const Value& name) { } } +std::vector Runtime::getPropertyNameIDs(const Object& object) { + Array names = getPropertyNames(object); + size_t count = names.size(*this); + std::vector result; + result.reserve(count); + for (size_t i = 0; i < count; ++i) { + result.push_back(PropNameID::forString( + *this, names.getValueAtIndex(*this, i).getString(*this))); + } + return result; +} + size_t Runtime::push(const Array& arr, const Value* elements, size_t count) { size_t newSize = size(arr); for (size_t i = 0; i < count; i++) { diff --git a/API/jsi/jsi/jsi.h b/API/jsi/jsi/jsi.h index bf98028fb3f..2ab45328374 100644 --- a/API/jsi/jsi/jsi.h +++ b/API/jsi/jsi/jsi.h @@ -558,6 +558,9 @@ class JSI_EXPORT IRuntime : public ICast { virtual bool isHostObject(const jsi::Object&) const = 0; virtual bool isHostFunction(const jsi::Function&) const = 0; virtual Array getPropertyNames(const Object&) = 0; + /// Returns the same names as getPropertyNames, in the same order, as + /// PropNameIDs and without creating a JS array. + virtual std::vector getPropertyNameIDs(const Object& object) = 0; virtual WeakObject createWeakObject(const Object&) = 0; virtual Value lockWeakObject(const WeakObject&) = 0; @@ -731,6 +734,7 @@ class JSI_EXPORT Runtime : public IRuntime { void deleteProperty(const Object&, const PropNameID& name) override; void deleteProperty(const Object&, const String& name) override; void deleteProperty(const Object&, const Value& name) override; + std::vector getPropertyNameIDs(const Object& object) override; void setRuntimeDataImpl( const UUID& uuid, @@ -1419,6 +1423,11 @@ class JSI_EXPORT Object : public Pointer { /// works. I only need it in one place.) Array getPropertyNames(IRuntime& runtime) const; + /// \return the names of all enumerable properties in the object and its + /// prototype chain, as getPropertyNames does, but as PropNameIDs, which can + /// be used to get the property values without converting the names again. + std::vector getPropertyNameIDs(IRuntime& runtime) const; + /// Inform the runtime that there is additional memory associated with a given /// JavaScript object that is not visible to the GC. This can be used if an /// object is known to retain some native memory, and may be used to guide diff --git a/API/jsi/jsi/test/testlib.cpp b/API/jsi/jsi/test/testlib.cpp index 37b71538b87..53d215b4f8f 100644 --- a/API/jsi/jsi/test/testlib.cpp +++ b/API/jsi/jsi/test/testlib.cpp @@ -2222,6 +2222,47 @@ TEST_P(JSITest, GetStringDataTest) { EXPECT_EQ(buf, str.utf16(rd)); } +TEST_P(JSITest, GetPropertyNameIDsTest) { + // This Runtime Decorator is used to test the default getPropertyNameIDs + // implementation for VMs that do not provide their own implementation + class RD : public RuntimeDecorator { + public: + explicit RD(Runtime& rt) : RuntimeDecorator(rt) {} + + std::vector getPropertyNameIDs(const Object& object) override { + return Runtime::getPropertyNameIDs(object); + } + }; + RD rd(rt); + + for (const char* source : + {"({title: 1, 7: 2, '\\u00e9t\\u00e9': 3, [Symbol('s')]: 4, x: null})", + "[10, 20, 30]", + "(function() {" + " var o = Object.create({inherited: 1});" + " o.own = 2;" + " Object.defineProperty(o, 'getter', {" + " get: function() { return 3; }, enumerable: true});" + " Object.defineProperty(o, 'hidden', {value: 4, enumerable: false});" + " return o;" + "})()", + "({})"}) { + Object obj = eval(source).getObject(rt); + Array names = obj.getPropertyNames(rt); + for (Runtime* runtime : {static_cast(&rt), static_cast(&rd)}) { + std::vector ids = obj.getPropertyNameIDs(*runtime); + ASSERT_EQ(ids.size(), names.size(rt)) << source; + for (size_t i = 0; i < ids.size(); ++i) { + String name = names.getValueAtIndex(rt, i).getString(rt); + EXPECT_EQ(ids[i].utf8(rt), name.utf8(rt)) << source; + EXPECT_TRUE(Value::strictEquals( + rt, obj.getProperty(rt, ids[i]), obj.getProperty(rt, name))) + << source; + } + } + } +} + TEST_P(JSITest, ObjectSetPrototype) { // This Runtime Decorator is used to test the default implementation of // Object.setPrototypeOf