Skip to content
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

Support NativeState in JSC #40746

Closed
wants to merge 1 commit into from
Closed
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
147 changes: 104 additions & 43 deletions packages/react-native/ReactCommon/jsc/JSCRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ class JSCRuntime : public jsi::Runtime {
jsi::Runtime::PointerValue* makeStringValue(JSStringRef str) const;
jsi::Runtime::PointerValue* makeObjectValue(JSObjectRef obj) const;

JSValueRef getNativeStateSymbol();

void checkException(JSValueRef exc);
void checkException(JSValueRef res, JSValueRef exc);
void checkException(JSValueRef exc, const char* msg);
Expand All @@ -264,6 +266,7 @@ class JSCRuntime : public jsi::Runtime {
JSGlobalContextRef ctx_;
std::atomic<bool> ctxInvalid_;
std::string desc_;
JSValueRef nativeStateSymbol_ = nullptr;
#ifndef NDEBUG
mutable std::atomic<intptr_t> objectCounter_;
mutable std::atomic<intptr_t> symbolCounter_;
Expand Down Expand Up @@ -384,6 +387,8 @@ JSCRuntime::~JSCRuntime() {
// atomic<bool> to avoid unsafe unprotects happening after shutdown
// has started.
ctxInvalid_ = true;
// No need to unprotect nativeStateSymbol_ since the heap is getting torn down
// anyway
JSGlobalContextRelease(ctx_);
#ifndef NDEBUG
assert(
Expand Down Expand Up @@ -450,25 +455,6 @@ bool JSCRuntime::isInspectable() {
return false;
}

namespace {

bool smellsLikeES6Symbol(JSGlobalContextRef ctx, JSValueRef ref) {
// Since iOS 13, JSValueGetType will return kJSTypeSymbol
// Before: Empirically, an es6 Symbol is not an object, but its type is
// object. This makes no sense, but we'll run with it.
// https://github.com/WebKit/webkit/blob/master/Source/JavaScriptCore/API/JSValueRef.cpp#L79-L82

JSType type = JSValueGetType(ctx, ref);

if (type == /* kJSTypeSymbol */ 6) {
return true;
}

return (!JSValueIsObject(ctx, ref) && type == kJSTypeObject);
}

} // namespace

JSCRuntime::JSCSymbolValue::JSCSymbolValue(
JSGlobalContextRef ctx,
const std::atomic<bool>& ctxInvalid,
Expand All @@ -486,7 +472,7 @@ JSCRuntime::JSCSymbolValue::JSCSymbolValue(
counter_(counter)
#endif
{
assert(smellsLikeES6Symbol(ctx_, sym_));
assert(JSValueIsSymbol(ctx_, sym_));
JSValueProtect(ctx_, sym_);
#ifndef NDEBUG
counter_ += 1;
Expand Down Expand Up @@ -723,7 +709,7 @@ jsi::Object JSCRuntime::createObject() {
}

// HostObject details
namespace detail {
namespace {
struct HostObjectProxyBase {
HostObjectProxyBase(
JSCRuntime& rt,
Expand All @@ -733,15 +719,13 @@ struct HostObjectProxyBase {
JSCRuntime& runtime;
std::shared_ptr<jsi::HostObject> hostObject;
};
} // namespace detail

namespace {
std::once_flag hostObjectClassOnceFlag;
JSClassRef hostObjectClass{};
} // namespace

jsi::Object JSCRuntime::createObject(std::shared_ptr<jsi::HostObject> ho) {
struct HostObjectProxy : public detail::HostObjectProxyBase {
struct HostObjectProxy : public HostObjectProxyBase {
static JSValueRef getProperty(
JSContextRef ctx,
JSObjectRef object,
Expand Down Expand Up @@ -873,25 +857,107 @@ std::shared_ptr<jsi::HostObject> JSCRuntime::getHostObject(
// We are guaranteed at this point to have isHostObject(obj) == true
// so the private data should be HostObjectMetadata
JSObjectRef object = objectRef(obj);
auto metadata =
static_cast<detail::HostObjectProxyBase*>(JSObjectGetPrivate(object));
auto metadata = static_cast<HostObjectProxyBase*>(JSObjectGetPrivate(object));
assert(metadata);
return metadata->hostObject;
}

bool JSCRuntime::hasNativeState(const jsi::Object&) {
throw std::logic_error("Not implemented");
// NativeState details
namespace {
struct NativeStateContainer {
NativeStateContainer(std::shared_ptr<jsi::NativeState> state)
: nativeState(std::move(state)) {}

std::shared_ptr<jsi::NativeState> nativeState;

static void finalize(JSObjectRef obj) {
auto container =
static_cast<NativeStateContainer*>(JSObjectGetPrivate(obj));
delete container;
}
};

JSClassRef getNativeStateClass() {
static JSClassRef nativeStateClass = [] {
JSClassDefinition nativeStateClassDef = kJSClassDefinitionEmpty;
nativeStateClassDef.version = 0;
nativeStateClassDef.attributes = kJSClassAttributeNoAutomaticPrototype;
nativeStateClassDef.finalize = NativeStateContainer::finalize;
return JSClassCreate(&nativeStateClassDef);
}();
return nativeStateClass;
}
} // namespace

JSValueRef JSCRuntime::getNativeStateSymbol() {
if (!nativeStateSymbol_) {
JSStringRef symbolName =
JSStringCreateWithUTF8CString("__internal_nativeState");
JSValueRef symbol = JSValueMakeSymbol(ctx_, symbolName);
JSValueProtect(ctx_, symbol);
nativeStateSymbol_ = symbol;
JSStringRelease(symbolName);
}
return nativeStateSymbol_;
}

bool JSCRuntime::hasNativeState(const jsi::Object& obj) {
JSValueRef exc = nullptr;
JSValueRef state = JSObjectGetPropertyForKey(
ctx_, objectRef(obj), getNativeStateSymbol(), &exc);
checkException(exc);

return JSValueIsObjectOfClass(ctx_, state, getNativeStateClass());
}

std::shared_ptr<jsi::NativeState> JSCRuntime::getNativeState(
const jsi::Object&) {
throw std::logic_error("Not implemented");
const jsi::Object& obj) {
JSValueRef exc = nullptr;
JSValueRef state = JSObjectGetPropertyForKey(
ctx_, objectRef(obj), getNativeStateSymbol(), &exc);
checkException(exc);

JSObjectRef stateObj = JSValueToObject(ctx_, state, &exc);
checkException(exc);

auto container =
static_cast<NativeStateContainer*>(JSObjectGetPrivate(stateObj));
assert(container);
return container->nativeState;
}

void JSCRuntime::setNativeState(
const jsi::Object&,
std::shared_ptr<jsi::NativeState>) {
throw std::logic_error("Not implemented");
const jsi::Object& obj,
std::shared_ptr<jsi::NativeState> nativeState) {
JSValueRef nativeStateSymbol = getNativeStateSymbol();

JSValueRef exc = nullptr;
JSValueRef state =
JSObjectGetPropertyForKey(ctx_, objectRef(obj), nativeStateSymbol, &exc);
checkException(exc);
if (JSValueIsUndefined(ctx_, state)) {
JSObjectRef stateObj = JSObjectMake(
ctx_,
getNativeStateClass(),
new NativeStateContainer(std::move(nativeState)));
JSObjectSetPropertyForKey(
ctx_,
objectRef(obj),
nativeStateSymbol,
stateObj,
kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontEnum |
kJSPropertyAttributeDontDelete,
&exc);
checkException(exc);
} else {
JSObjectRef stateObj = JSValueToObject(ctx_, state, &exc);
checkException(exc);

auto container =
static_cast<NativeStateContainer*>(JSObjectGetPrivate(stateObj));
assert(container);
container->nativeState = std::move(nativeState);
}
}

jsi::Value JSCRuntime::getProperty(
Expand Down Expand Up @@ -1415,16 +1481,11 @@ jsi::Value JSCRuntime::createValue(JSValueRef value) const {
JSObjectRef objRef = JSValueToObject(ctx_, value, nullptr);
return jsi::Value(createObject(objRef));
}
// TODO: Uncomment this when all supported JSC versions have this symbol
// case kJSTypeSymbol:
default: {
if (smellsLikeES6Symbol(ctx_, value)) {
return jsi::Value(createSymbol(value));
} else {
// WHAT ARE YOU
abort();
}
}
case kJSTypeSymbol:
return jsi::Value(createSymbol(value));
default:
// WHAT ARE YOU
abort();
}
}

Expand Down
68 changes: 68 additions & 0 deletions packages/react-native/ReactCommon/jsi/jsi/test/testlib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1452,6 +1452,74 @@ TEST_P(JSITest, ArrayBufferSizeTest) {
EXPECT_EQ(ab.size(rt), 10);
}

namespace {

struct IntState : public NativeState {
explicit IntState(int value) : value(value) {}
int value;
};

} // namespace

TEST_P(JSITest, NativeState) {
Object holder(rt);
EXPECT_FALSE(holder.hasNativeState(rt));

auto stateValue = std::make_shared<IntState>(42);
holder.setNativeState(rt, stateValue);
EXPECT_TRUE(holder.hasNativeState(rt));
EXPECT_EQ(
std::dynamic_pointer_cast<IntState>(holder.getNativeState(rt))->value,
42);

stateValue = std::make_shared<IntState>(21);
holder.setNativeState(rt, stateValue);
EXPECT_TRUE(holder.hasNativeState(rt));
EXPECT_EQ(
std::dynamic_pointer_cast<IntState>(holder.getNativeState(rt))->value,
21);

// There's currently way to "delete" the native state of a component fully
// Even when reset with nullptr, hasNativeState will still return true
holder.setNativeState(rt, nullptr);
EXPECT_TRUE(holder.hasNativeState(rt));
EXPECT_TRUE(holder.getNativeState(rt) == nullptr);
}

TEST_P(JSITest, NativeStateSymbolOverrides) {
Object holder(rt);

auto stateValue = std::make_shared<IntState>(42);
holder.setNativeState(rt, stateValue);

// Attempting to change configurable attribute of unconfigurable property
try {
function(
"function (obj) {"
" var mySymbol = Symbol();"
" obj[mySymbol] = 'foo';"
" var allSymbols = Object.getOwnPropertySymbols(obj);"
" for (var sym of allSymbols) {"
" Object.defineProperty(obj, sym, {configurable: true, writable: true});"
" obj[sym] = 'bar';"
" }"
"}")
.call(rt, holder);
} catch (const JSError& ex) {
// On JSC this throws, but it doesn't on Hermes
std::string exc = ex.what();
EXPECT_NE(
exc.find(
"Attempting to change configurable attribute of unconfigurable property"),
std::string::npos);
}

EXPECT_TRUE(holder.hasNativeState(rt));
EXPECT_EQ(
std::dynamic_pointer_cast<IntState>(holder.getNativeState(rt))->value,
42);
}

INSTANTIATE_TEST_CASE_P(
Runtimes,
JSITest,
Expand Down