Skip to content

Commit

Permalink
Support NativeState in JSC (#40746)
Browse files Browse the repository at this point in the history
Summary:
X-link: facebook/hermes#1151

Pull Request resolved: #40746

This feature was missing in JSC's JSI implementation, which is preventing from rolling out NativeState-based features in React Native.

Changelog: [General][Added] JSC support for the NativeState API in JSI

Reviewed By: neildhar

Differential Revision: D49229022

fbshipit-source-id: 1787c1d1b4803212d84da8f55b7d5a460a9d33c2
  • Loading branch information
javache authored and facebook-github-bot committed Oct 10, 2023
1 parent 28b089e commit 7b7f128
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 43 deletions.
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

0 comments on commit 7b7f128

Please sign in to comment.