Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 87 additions & 2 deletions API/hermes/hermes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,8 @@ class HermesRuntimeImpl final : public HermesRuntime,
private IHermesTestHelpers,
private InstallHermesFatalErrorHandler,
private jsi::Instrumentation,
public ISetEventLoopControl
public ISetEventLoopControl,
public jsi::IPropertyEnumeration
#ifdef JSI_UNSTABLE
,
public jsi::ISerialization,
Expand Down Expand Up @@ -768,8 +769,19 @@ class HermesRuntimeImpl final : public HermesRuntime,
const jsi::Serialized &serialized) const override;
const std::shared_ptr<jsi::Serialized> makeSerialized(
vm::SerializedValue &value) const override;

#endif

void forEachProperty(
const jsi::Object &object,
void *ctx,
void (*cb)(
void *ctx,
bool ascii,
const void *name,
size_t length,
jsi::Value &value)) override;

void setEventLoopControl(IEventLoopControl *eventLoopControl) override;
IEventLoopControl *getEventLoopControl() override;

Expand Down Expand Up @@ -1728,7 +1740,9 @@ jsi::ICast *HermesRuntimeImpl::castInterface(const jsi::UUID &interfaceUUID) {
return static_cast<IHermesTracingHelpers *>(this);
}
#endif
else if (interfaceUUID == ISetEventLoopControl::uuid) {
else if (interfaceUUID == IPropertyEnumeration::uuid) {
return static_cast<IPropertyEnumeration *>(this);
} else if (interfaceUUID == ISetEventLoopControl::uuid) {
return static_cast<ISetEventLoopControl *>(this);
}
return nullptr;
Expand Down Expand Up @@ -3118,6 +3132,77 @@ jsi::Array HermesRuntimeImpl::getPropertyNames(const jsi::Object &obj) {
return ret;
}

void HermesRuntimeImpl::forEachProperty(
const jsi::Object &object,
void *ctx,
void (*cb)(
void *ctx,
bool ascii,
const void *name,
size_t length,
jsi::Value &value)) {
ExecutionScopeRAII scopeRAII(mutatorScope);
vm::GCScope gcScope(runtime_);
uint32_t beginIndex;
uint32_t endIndex;
auto objHandle = handle(object);
vm::CallResult<vm::Handle<vm::ArrayStorageSmall>> cr =
vm::getForInPropertyNames(runtime_, objHandle, beginIndex, endIndex);
checkStatus(cr.getStatus());
vm::Handle<vm::ArrayStorageSmall> names = *cr;

// The callback may use the runtime, which may move the strings, so each
// name is copied. The buffers are reused, so short names don't allocate.
std::string asciiName;
std::u16string utf16Name;
bool ascii = true;
auto copyName = [&](vm::StringView view) {
ascii = view.isASCII();
if (ascii) {
asciiName.assign(view.castToCharPtr(), view.length());
} else {
utf16Name.assign(view.castToChar16Ptr(), view.length());
}
};

for (uint32_t i = beginIndex; i < endIndex; ++i) {
vm::GCScopeMarkerRAII marker{runtime_};
vm::SmallHermesValue key = names->at(i);
jsi::Value value;
if (key.isSymbol()) {
// for-in keeps the names of ordinary objects as symbols. getProperty
// with a PropNameID reads them the same way.
vm::SymbolID id = key.getSymbol();
copyName(runtime_.getIdentifierTable().getStringView(runtime_, id));
auto valueRes = vm::JSObject::getNamedOrIndexed(objHandle, runtime_, id);
checkStatus(valueRes.getStatus());
value = valueFromHermesValue(valueRes->get());
} else {
vm::Handle<> keyHandle = runtime_.makeHandle(key.unboxToHV(runtime_));
if (key.isString()) {
copyName(vm::StringPrimitive::createStringView(
runtime_, vm::Handle<vm::StringPrimitive>::vmcast(keyHandle)));
} else if (key.isNumber()) {
// An index, whose name is its decimal digits.
ascii = true;
asciiName =
std::to_string(static_cast<uint32_t>(key.getNumber(runtime_)));
} else {
llvm_unreachable("property name is not String, Symbol or Number");
}
auto valueRes =
vm::JSObject::getComputed_RJS(objHandle, runtime_, keyHandle);
checkStatus(valueRes.getStatus());
value = valueFromHermesValue(valueRes->get());
}
if (ascii) {
cb(ctx, true, asciiName.data(), asciiName.size(), value);
} else {
cb(ctx, false, utf16Name.data(), utf16Name.size(), value);
}
}
}

jsi::WeakObject HermesRuntimeImpl::createWeakObject(const jsi::Object &obj) {
ExecutionScopeRAII scopeRAII(mutatorScope);
return addWeak(
Expand Down
43 changes: 43 additions & 0 deletions API/jsi/jsi/jsi.h
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,49 @@ class JSI_EXPORT ISerialization : public ICast {

#endif // JSI_UNSTABLE

/// Provides an API to read the enumerable properties of an object with
/// their values, without the array of getPropertyNames or a String for each
/// name.
class JSI_EXPORT IPropertyEnumeration : public ICast {
public:
static constexpr jsi::UUID uuid{
0xdf3c7545,
0x65b0,
0x44b4,
0xaa11,
0xe03358f93907};

/// Invokes the callback \p cb with \p ctx and the name and value of each
/// enumerable string-keyed property of \p object and its prototype chain.
/// The result is the same as calling getProperty for each name that
/// getPropertyNames returns, in that order: the names are collected before
/// the first call, getters and Proxy traps run, a property added during the
/// enumeration is not visited, and a property deleted before it's visited is
/// still visited with the value that getProperty returns at that time.
///
/// If \p ascii is true, \p name points to \p length ASCII characters.
/// Otherwise, it points to \p length UTF-16 code units, which may include
/// unpaired surrogates. The name is not null-terminated, and it is valid
/// only until \p cb returns. The callback may move \p value.
///
/// The callback may use the runtime, for example to allocate, call
/// functions or modify \p object. If a getter or Proxy trap throws, the
/// enumeration stops and the method throws a JSError. If the callback throws,
/// the enumeration stops and the exception propagates to the caller.
virtual void forEachProperty(
const Object& object,
void* ctx,
void (*cb)(
void* ctx,
bool ascii,
const void* name,
size_t length,
Value& value)) = 0;

protected:
~IPropertyEnumeration() = default;
};

/// An interface that provides various functionalities of the JS runtime.
/// The APIs must not be called from multiple threads concurrently. It is the
/// user's responsibility ensure thread safety when using IRuntime.
Expand Down
167 changes: 167 additions & 0 deletions unittests/API/APITest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2833,6 +2833,173 @@ TEST_P(HermesRuntimeTest, IsUint8ArrayTest) {
}
}

namespace {
/// A property that IPropertyEnumeration::forEachProperty visits, with its name
/// in UTF-16.
struct VisitedProperty {
std::u16string name;
Value value;
};

/// Converts a name that forEachProperty passes to UTF-16.
std::u16string nameToUTF16(bool ascii, const void *name, size_t length) {
if (ascii) {
const char *chars = static_cast<const char *>(name);
return std::u16string(chars, chars + length);
}
return std::u16string(static_cast<const char16_t *>(name), length);
}

/// The properties that getProperty returns for each name of getPropertyNames.
std::vector<VisitedProperty> getPropertiesByName(
Runtime &rt,
const Object &obj) {
std::vector<VisitedProperty> properties;
Array names = obj.getPropertyNames(rt);
for (size_t i = 0; i < names.size(rt); ++i) {
String name = names.getValueAtIndex(rt, i).getString(rt);
properties.push_back({name.utf16(rt), obj.getProperty(rt, name)});
}
return properties;
}
} // namespace

TEST_P(HermesRuntimeTest, ForEachPropertyMatchesGetProperty) {
auto *enumeration = castInterface<IPropertyEnumeration>(rt.get());
if (!enumeration) {
// HermesSandboxRuntime doesn't provide the interface.
return;
}
for (const char *source :
{"({title: 1, 7: 2, '\\u00e9t\\u00e9': 3, '\\ud800': 4,"
" [Symbol('s')]: 5, empty: 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;"
"})()",
"new Proxy({a: 1, b: 2}, {"
" get: function(target, key) { return key + '!'; }})",
"({})"}) {
Object obj = eval(source).getObject(*rt);
std::vector<VisitedProperty> expected = getPropertiesByName(*rt, obj);

struct Context {
Runtime &rt;
std::vector<VisitedProperty> visited;
} context{*rt, {}};
enumeration->forEachProperty(
obj,
&context,
[](void *ctx,
bool ascii,
const void *name,
size_t length,
Value &value) {
auto &c = *static_cast<Context *>(ctx);
std::u16string utf16 = nameToUTF16(ascii, name, length);
// The callback may use the runtime, which may allocate and move
// strings. The name stays valid during the call.
String::createFromUtf8(c.rt, "allocates");
c.rt.instrumentation().collectGarbage("forEachProperty");
EXPECT_EQ(nameToUTF16(ascii, name, length), utf16);
c.visited.push_back({std::move(utf16), std::move(value)});
});

ASSERT_EQ(context.visited.size(), expected.size()) << source;
for (size_t i = 0; i < expected.size(); ++i) {
EXPECT_EQ(context.visited[i].name, expected[i].name) << source;
EXPECT_TRUE(Value::strictEquals(
*rt, context.visited[i].value, expected[i].value))
<< source;
}
}
}

TEST_P(HermesRuntimeTest, ForEachPropertyWhenTheObjectChanges) {
auto *enumeration = castInterface<IPropertyEnumeration>(rt.get());
if (!enumeration) {
// HermesSandboxRuntime doesn't provide the interface.
return;
}
Object obj = eval("({a: 1, b: 2, c: 3})").getObject(*rt);

// While visiting `a`, the callback deletes `b` and adds `d`. `b` is still
// visited, with the value that getProperty returns, and `d` isn't visited.
struct Context {
Runtime &rt;
Object &obj;
std::vector<VisitedProperty> visited;
} context{*rt, obj, {}};
enumeration->forEachProperty(
obj,
&context,
[](void *ctx, bool ascii, const void *name, size_t length, Value &value) {
auto &c = *static_cast<Context *>(ctx);
if (c.visited.empty()) {
c.obj.deleteProperty(c.rt, "b");
c.obj.setProperty(c.rt, "d", 4);
}
c.visited.push_back(
{nameToUTF16(ascii, name, length), std::move(value)});
});

ASSERT_EQ(context.visited.size(), 3);
EXPECT_EQ(context.visited[0].name, u"a");
EXPECT_EQ(context.visited[0].value.getNumber(), 1);
EXPECT_EQ(context.visited[1].name, u"b");
EXPECT_TRUE(context.visited[1].value.isUndefined());
EXPECT_EQ(context.visited[2].name, u"c");
EXPECT_EQ(context.visited[2].value.getNumber(), 3);
}

TEST_P(HermesRuntimeTest, ForEachPropertyStopsOnExceptions) {
auto *enumeration = castInterface<IPropertyEnumeration>(rt.get());
if (!enumeration) {
// HermesSandboxRuntime doesn't provide the interface.
return;
}
auto countCalls =
[](void *ctx, bool, const void *, size_t, Value &) {
++*static_cast<int *>(ctx);
};

// A getter that throws stops the enumeration with a JSError.
Object withThrowingGetter = eval(
"({a: 1,"
" get b() { throw new Error('getter'); },"
" c: 3})")
.getObject(*rt);
int calls = 0;
EXPECT_THROW(
enumeration->forEachProperty(withThrowingGetter, &calls, countCalls),
JSError);
EXPECT_EQ(calls, 1);

// An exception from the callback stops the enumeration and propagates.
Object obj = eval("({a: 1, b: 2, c: 3})").getObject(*rt);
calls = 0;
EXPECT_THROW(
enumeration->forEachProperty(
obj,
&calls,
[](void *ctx, bool, const void *, size_t, Value &) {
++*static_cast<int *>(ctx);
throw std::runtime_error("callback");
}),
std::runtime_error);
EXPECT_EQ(calls, 1);

// The runtime stays usable.
calls = 0;
enumeration->forEachProperty(obj, &calls, countCalls);
EXPECT_EQ(calls, 3);
}

#ifdef JSI_UNSTABLE
class HermesSerializationTest : public HermesRuntimeTest {
public:
Expand Down