Skip to content

Commit

Permalink
Add API for setting/getting native state
Browse files Browse the repository at this point in the history
Summary:
Add API for setting/getting native state.

When present, the internal NativeState property of an object always stores a NativeState with a pointer to a heap-allocated shared_ptr + a finalizer that simply `delete`s it.

Changelog:
[Internal][Added] - JSI API for setting/getting native state on a JS object

Reviewed By: jpporto

Differential Revision: D36499239

fbshipit-source-id: a1ff1905811db1aac99ece3f928b81d0abfb342b
  • Loading branch information
kodafb authored and facebook-github-bot committed Jul 18, 2022
1 parent 4187008 commit 4efad65
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 0 deletions.
3 changes: 3 additions & 0 deletions API/hermes/TracingRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class TracingRuntime : public jsi::RuntimeDecorator<jsi::Runtime> {
jsi::Object createObject() override;
jsi::Object createObject(std::shared_ptr<jsi::HostObject> ho) override;

// Note that the NativeState methods do not need to be traced since they
// cannot be observed in JS.

jsi::String createStringFromAscii(const char *str, size_t length) override;
jsi::String createStringFromUtf8(const uint8_t *utf8, size_t length) override;

Expand Down
83 changes: 83 additions & 0 deletions API/hermes/hermes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "hermes/VM/JSLib.h"
#include "hermes/VM/JSLib/RuntimeCommonStorage.h"
#include "hermes/VM/JSLib/RuntimeJSONUtils.h"
#include "hermes/VM/NativeState.h"
#include "hermes/VM/Operations.h"
#include "hermes/VM/Profiler/CodeCoverageProfiler.h"
#include "hermes/VM/Profiler/SamplingProfiler.h"
Expand Down Expand Up @@ -734,6 +735,11 @@ class HermesRuntimeImpl final : public HermesRuntime,
jsi::Object createObject(std::shared_ptr<jsi::HostObject> ho) override;
std::shared_ptr<jsi::HostObject> getHostObject(const jsi::Object &) override;
jsi::HostFunctionType &getHostFunction(const jsi::Function &) override;
bool hasNativeState(const jsi::Object &) override;
std::shared_ptr<jsi::NativeState> getNativeState(
const jsi::Object &) override;
void setNativeState(const jsi::Object &, std::shared_ptr<jsi::NativeState>)
override;
jsi::Value getProperty(const jsi::Object &, const jsi::PropNameID &name)
override;
jsi::Value getProperty(const jsi::Object &, const jsi::String &name) override;
Expand Down Expand Up @@ -1736,6 +1742,83 @@ std::shared_ptr<jsi::HostObject> HermesRuntimeImpl::getHostObject(
return static_cast<const JsiProxy *>(proxy)->ho_;
}

bool HermesRuntimeImpl::hasNativeState(const jsi::Object &obj) {
vm::GCScope gcScope(runtime_);
auto h = handle(obj);
if (h->isProxyObject() || h->isHostObject()) {
return false;
}
vm::NamedPropertyDescriptor desc;
return vm::JSObject::getOwnNamedDescriptor(
h,
runtime_,
vm::Predefined::getSymbolID(vm::Predefined::InternalPropertyNativeState),
desc);
}

static void deleteShared(void *context) {
delete reinterpret_cast<std::shared_ptr<void> *>(context);
}

void HermesRuntimeImpl::setNativeState(
const jsi::Object &obj,
std::shared_ptr<jsi::NativeState> state) {
return maybeRethrow([&] {
vm::GCScope gcScope(runtime_);
auto h = handle(obj);
if (h->isProxyObject()) {
throw jsi::JSINativeException("native state unsupported on Proxy");
} else if (h->isHostObject()) {
throw jsi::JSINativeException("native state unsupported on HostObject");
}
// Allocate a shared_ptr on the C++ heap and use it as context of
// NativeState.
std::shared_ptr<void> *ptr = new std::shared_ptr<void>(state);
auto ns = runtime_.makeHandle(
vm::NativeState::create(runtime_, ptr, deleteShared));
auto res = vm::JSObject::defineOwnProperty(
h,
runtime_,
vm::Predefined::getSymbolID(
vm::Predefined::InternalPropertyNativeState),
vm::DefinePropertyFlags::getDefaultNewPropertyFlags(),
ns);
// NB: If setting the property failed, then the NativeState cell will soon
// be unreachable, and when it's later finalized, the shared_ptr will be
// deleted.
checkStatus(res.getStatus());
if (!*res) {
throw jsi::JSINativeException(
"failed to define internal native state property");
}
});
}

std::shared_ptr<jsi::NativeState> HermesRuntimeImpl::getNativeState(
const jsi::Object &obj) {
return maybeRethrow([&] {
vm::GCScope gcScope(runtime_);
assert(hasNativeState(obj) && "object lacks native state");
auto h = handle(obj);
vm::NamedPropertyDescriptor desc;
bool exists = vm::JSObject::getOwnNamedDescriptor(
h,
runtime_,
vm::Predefined::getSymbolID(
vm::Predefined::InternalPropertyNativeState),
desc);
(void)exists;
assert(exists && "hasNativeState lied");
// Raw pointers below.
vm::NoAllocScope scope(runtime_);
vm::NativeState *ns = vm::vmcast<vm::NativeState>(
vm::JSObject::getNamedSlotValueUnsafe(*h, runtime_, desc)
.getObject(runtime_));
return std::shared_ptr(
*reinterpret_cast<std::shared_ptr<jsi::NativeState> *>(ns->context()));
});
}

jsi::Value HermesRuntimeImpl::getProperty(
const jsi::Object &obj,
const jsi::String &name) {
Expand Down
11 changes: 11 additions & 0 deletions API/jsi/jsi/decorator.h
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,17 @@ class RuntimeDecorator : public Base, private jsi::Instrumentation {
return dhf.target<DecoratedHostFunction>()->plainHF_;
};

bool hasNativeState(const Object& o) override {
return plain_.hasNativeState(o);
}
std::shared_ptr<NativeState> getNativeState(const Object& o) override {
return plain_.getNativeState(o);
}
void setNativeState(const Object& o, std::shared_ptr<NativeState> state)
override {
plain_.setNativeState(o, state);
}

Value getProperty(const Object& o, const PropNameID& name) override {
return plain_.getProperty(o, name);
};
Expand Down
18 changes: 18 additions & 0 deletions API/jsi/jsi/jsi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,24 @@ inline std::shared_ptr<HostObject> Object::getHostObject<HostObject>(
return runtime.getHostObject(*this);
}

template <typename T>
inline bool Object::hasNativeState(Runtime& runtime) const {
return runtime.hasNativeState(*this) &&
std::dynamic_pointer_cast<T>(runtime.getNativeState(*this));
}

template <typename T>
inline std::shared_ptr<T> Object::getNativeState(Runtime& runtime) const {
assert(hasNativeState<T>(runtime));
return std::static_pointer_cast<T>(runtime.getNativeState(*this));
}

inline void Object::setNativeState(
Runtime& runtime,
std::shared_ptr<NativeState> state) const {
runtime.setNativeState(*this, state);
}

inline Array Object::getPropertyNames(Runtime& runtime) const {
return runtime.getPropertyNames(*this);
}
Expand Down
2 changes: 2 additions & 0 deletions API/jsi/jsi/jsi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ void HostObject::set(Runtime& rt, const PropNameID& name, const Value&) {

HostObject::~HostObject() {}

NativeState::~NativeState() {}

Runtime::~Runtime() {}

Instrumentation& Runtime::instrumentation() {
Expand Down
32 changes: 32 additions & 0 deletions API/jsi/jsi/jsi.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ class JSI_EXPORT HostObject {
virtual std::vector<PropNameID> getPropertyNames(Runtime& rt);
};

/// Native state (and destructor) that can be attached to any JS object
/// using setNativeState.
class JSI_EXPORT NativeState {
public:
virtual ~NativeState();
};

/// Represents a JS runtime. Movable, but not copyable. Note that
/// this object may not be thread-aware, but cannot be used safely from
/// multiple threads at once. The application is responsible for
Expand Down Expand Up @@ -296,6 +303,12 @@ class JSI_EXPORT Runtime {
virtual std::shared_ptr<HostObject> getHostObject(const jsi::Object&) = 0;
virtual HostFunctionType& getHostFunction(const jsi::Function&) = 0;

virtual bool hasNativeState(const jsi::Object&) = 0;
virtual std::shared_ptr<NativeState> getNativeState(const jsi::Object&) = 0;
virtual void setNativeState(
const jsi::Object&,
std::shared_ptr<NativeState> state) = 0;

virtual Value getProperty(const Object&, const PropNameID& name) = 0;
virtual Value getProperty(const Object&, const String& name) = 0;
virtual bool hasProperty(const Object&, const PropNameID& name) = 0;
Expand Down Expand Up @@ -711,6 +724,25 @@ class JSI_EXPORT Object : public Pointer {
template <typename T = HostObject>
std::shared_ptr<T> asHostObject(Runtime& runtime) const;

/// \return whether this object has native state of type T previously set by
/// \c setNativeState.
template <typename T = NativeState>
bool hasNativeState(Runtime& runtime) const;

/// \return a shared_ptr to the state previously set by \c setNativeState.
/// If \c hasNativeState<T> is false, this will assert. Note that this does a
/// type check and will assert if the native state isn't of type \c T
template <typename T = NativeState>
std::shared_ptr<T> getNativeState(Runtime& runtime) const;

/// Set the internal native state property of this object, overwriting any old
/// value. Creates a new shared_ptr to the object managed by \p state, which
/// will live until the value at this property becomes unreachable.
///
/// Throws a type error if this object is a proxy or host object.
void setNativeState(Runtime& runtime, std::shared_ptr<NativeState> state)
const;

/// \return same as \c getProperty(name).asObject(), except with
/// a better exception message.
Object getPropertyAsObject(Runtime& runtime, const char* name) const;
Expand Down
1 change: 1 addition & 0 deletions include/hermes/VM/InternalProperties.def
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ PROP(6)
NAMED_PROP(NamedPropForUnitTestOnly)
NAMED_PROP(CapturedError)
NAMED_PROP(IntlNativeType)
NAMED_PROP(NativeState)

#undef PROP
#undef NAMED_PROP
4 changes: 4 additions & 0 deletions include/hermes/VM/NativeState.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class NativeState final : public GCCell {
finalizePtr_(context_);
}

void *context() {
return context_;
}

private:
static void _finalizeImpl(GCCell *cell, GC &gc);

Expand Down
1 change: 1 addition & 0 deletions lib/VM/JSObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "hermes/VM/InternalProperty.h"
#include "hermes/VM/JSArray.h"
#include "hermes/VM/JSProxy.h"
#include "hermes/VM/NativeState.h"
#include "hermes/VM/Operations.h"
#include "hermes/VM/PropertyAccessor.h"

Expand Down
35 changes: 35 additions & 0 deletions unittests/API/APITest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,41 @@ TEST_F(HermesRuntimeTest, HostObjectAsParentTest) {
eval("var subClass = {__proto__: ho}; subClass.prop1 == 10;").getBool());
}

TEST_F(HermesRuntimeTest, NativeStateTest) {
class C : public facebook::jsi::NativeState {
public:
int *dtors;
C(int *_dtors) : dtors(_dtors) {}
virtual ~C() override {
++*dtors;
}
};
int dtors1 = 0;
int dtors2 = 0;
{
Object obj = eval("({one: 1})").getObject(*rt);
ASSERT_FALSE(obj.hasNativeState<C>(*rt));
{
// Set some state.
obj.setNativeState(*rt, std::make_shared<C>(&dtors1));
ASSERT_TRUE(obj.hasNativeState<C>(*rt));
auto ptr = obj.getNativeState<C>(*rt);
EXPECT_EQ(ptr->dtors, &dtors1);
}
{
// Overwrite the state.
obj.setNativeState(*rt, std::make_shared<C>(&dtors2));
ASSERT_TRUE(obj.hasNativeState<C>(*rt));
auto ptr = obj.getNativeState<C>(*rt);
EXPECT_EQ(ptr->dtors, &dtors2);
}
} // closing scope -> obj unreachable
// should finalize both
eval("gc()");
EXPECT_EQ(1, dtors1);
EXPECT_EQ(1, dtors2);
}

TEST_F(HermesRuntimeTest, PropNameIDFromSymbol) {
auto strProp = PropNameID::forAscii(*rt, "a");
auto secretProp = PropNameID::forSymbol(
Expand Down

0 comments on commit 4efad65

Please sign in to comment.