From bc7c85f1534668cad7b708eb36bacd63aa426412 Mon Sep 17 00:00:00 2001 From: Ramanpreet Nara Date: Fri, 2 Aug 2019 17:01:55 -0700 Subject: [PATCH] Delete jsi::Functions before jsi::Runtime gets deleted Summary: ## The Problem 1. `CatalystInstanceImpl` indirectly holds on to the `jsi::Runtime`. When you destroy `CatalystInstanceImpl`, you destroy the `jsi::Runtime`. As a part of reloading React Native, we destroy and re-create `CatalystInstanceImpl`, which destroys and re-creates the `jsi::Runtime`. 2. When JS passes in a callback to a TurboModule method, we take that callback (a `jsi::Function`) and wrap it in a Java `Callback` (implemented by `JCxxCallbackImpl`). This Java `Callback`, when executed, schedules the `jsi::Function` to be invoked on a Java thread at a later point in time. **Note:** The Java NativeModule can hold on to the Java `Callback` (and, by transitivity, the `jsi::Function`) for potentially forever. 3. It is a requirement of `jsi::Runtime` that all objects associated with the Runtime (ex: `jsi::Function`) must be destroyed before the Runtime itself is destroyed. See: https://fburl.com/m3mqk6wt ### jsi.h ``` /// .................................................... In addition, to /// make shutdown safe, destruction of objects associated with the Runtime /// must be destroyed before the Runtime is destroyed, or from the /// destructor of a managed HostObject or HostFunction. Informally, this /// means that the main source of unsafe behavior is to hold a jsi object /// in a non-Runtime-managed object, and not clean it up before the Runtime /// is shut down. If your lifecycle is such that avoiding this is hard, /// you will probably need to do use your own locks. class Runtime { public: virtual ~Runtime(); ``` Therefore, when you delete `CatalystInstanceImpl`, you could end up with a situation where the `jsi::Runtime` is destroyed before all `jsi::Function`s are destroyed. In dev, this leads the program to crash when you reload the app after having used a TurboModule method that uses callbacks. ## The Solution If the only reference to a `HostObject` or a `HostFunction` is in the JS Heap, then the `HostObject` and `HostFunction` destructors can destroy JSI objects. The TurboModule cache is the only thing, aside from the JS Heap, that holds a reference to all C++ TurboModules. But that cache (and the entire native side of `TurboModuleManager`) is destroyed when we call `mHybridData.resetNative()` in `TurboModuleManager.onCatalystInstanceDestroy()` in D16552730. (I verified this by commenting out `mHybridData.resetNative()` and placing a breakpoint in the destructor of `JavaTurboModule`). So, when we're cleaning up `TurboModuleManager`, the only reference to a Java TurboModule is the JS Heap. Therefore, it's safe and correct for us to destroy all `jsi::Function`s created by the Java TurboModule in `~JavaTurboModule`. So, in this diff, I keep a set of all `CallbackWrappers`, and explicitly call `destroy()` on them in the `JavaTurboModule` destructor. Note that since `~JavaTurboModule` accesses `callbackWrappers_`, it must be executed on the JS Thread, since `createJavaCallbackFromJSIFunction` also accesses `callbackWrappers_` on the JS Thread. For additional safety, I also eagerly destroyed the `jsi::Function` after it's been invoked once. I'm not yet sure if we only want JS callbacks to only ever be invoked once. So, I've created a Task to document this work: T48128233. Reviewed By: mdvacca Differential Revision: D16623340 fbshipit-source-id: 3a4c3efc70b9b3c8d329f19fdf4b4423c489695b --- .../react/bridge/CatalystInstanceImpl.java | 65 ++++-- .../react/bridge/JSIModuleRegistry.java | 10 +- .../turbomodule/core/TurboCxxModule.cpp | 101 +++++++--- .../turbomodule/core/TurboModuleUtils.h | 65 +++++- .../core/platform/android/JavaTurboModule.cpp | 62 ++++-- .../core/platform/android/JavaTurboModule.h | 30 ++- .../core/platform/ios/RCTTurboModule.mm | 187 +++++++++++------- 7 files changed, 375 insertions(+), 145 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java index e96d24270e..d2aa466665 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java @@ -23,6 +23,7 @@ import com.facebook.react.bridge.queue.ReactQueueConfigurationImpl; import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.turbomodule.core.JSCallInvokerHolderImpl; import com.facebook.react.turbomodule.core.interfaces.TurboModule; @@ -359,24 +360,56 @@ public class CatalystInstanceImpl implements CatalystInstance { listener.onBridgeDestroyed(); } } - AsyncTask.execute( - new Runnable() { - @Override - public void run() { - // Kill non-UI threads from neutral third party - // potentially expensive, so don't run on UI thread - // contextHolder is used as a lock to guard against other users of the JS VM - // having - // the VM destroyed underneath them, so notify them before we resetNative - mJavaScriptContextHolder.clear(); + final JSIModule turboModuleManager = + ReactFeatureFlags.useTurboModules + ? mJSIModuleRegistry.getModule(JSIModuleType.TurboModuleManager) + : null; - mHybridData.resetNative(); - getReactQueueConfiguration().destroy(); - Log.d(ReactConstants.TAG, "CatalystInstanceImpl.destroy() end"); - ReactMarker.logMarker(ReactMarkerConstants.DESTROY_CATALYST_INSTANCE_END); - } - }); + getReactQueueConfiguration() + .getJSQueueThread() + .runOnQueue( + new Runnable() { + @Override + public void run() { + // We need to destroy the TurboModuleManager on the JS Thread + if (turboModuleManager != null) { + turboModuleManager.onCatalystInstanceDestroy(); + } + + getReactQueueConfiguration() + .getUIQueueThread() + .runOnQueue( + new Runnable() { + @Override + public void run() { + // AsyncTask.execute must be executed from the UI Thread + AsyncTask.execute( + new Runnable() { + @Override + public void run() { + // Kill non-UI threads from neutral third party + // potentially expensive, so don't run on UI thread + + // contextHolder is used as a lock to guard against + // other users of the JS VM having the VM destroyed + // underneath them, so notify them before we reset + // Native + mJavaScriptContextHolder.clear(); + + mHybridData.resetNative(); + getReactQueueConfiguration().destroy(); + Log.d( + ReactConstants.TAG, + "CatalystInstanceImpl.destroy() end"); + ReactMarker.logMarker( + ReactMarkerConstants.DESTROY_CATALYST_INSTANCE_END); + } + }); + } + }); + } + }); } }); diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSIModuleRegistry.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSIModuleRegistry.java index 7c4ded025f..1a27b44637 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSIModuleRegistry.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSIModuleRegistry.java @@ -32,7 +32,15 @@ public class JSIModuleRegistry { } public void notifyJSInstanceDestroy() { - for (JSIModuleHolder moduleHolder : mModules.values()) { + for (Map.Entry entry : mModules.entrySet()) { + JSIModuleType moduleType = entry.getKey(); + + // Don't call TurboModuleManager.onCatalystInstanceDestroy + if (moduleType == JSIModuleType.TurboModuleManager) { + continue; + } + + JSIModuleHolder moduleHolder = entry.getValue(); moduleHolder.notifyJSInstanceDestroy(); } } diff --git a/ReactCommon/turbomodule/core/TurboCxxModule.cpp b/ReactCommon/turbomodule/core/TurboCxxModule.cpp index 56663ccc99..267b5179ed 100644 --- a/ReactCommon/turbomodule/core/TurboCxxModule.cpp +++ b/ReactCommon/turbomodule/core/TurboCxxModule.cpp @@ -22,35 +22,49 @@ static CxxModule::Callback makeTurboCxxModuleCallback( jsi::Runtime &runtime, std::shared_ptr callbackWrapper) { return [callbackWrapper](std::vector args) { - callbackWrapper->jsInvoker->invokeAsync([callbackWrapper, args]() { + callbackWrapper->jsInvoker().invokeAsync([callbackWrapper, args]() { std::vector innerArgs; for (auto &a : args) { - innerArgs.push_back(jsi::valueFromDynamic(callbackWrapper->runtime, a)); + innerArgs.push_back( + jsi::valueFromDynamic(callbackWrapper->runtime(), a)); } - callbackWrapper->callback.call(callbackWrapper->runtime, (const jsi::Value *)innerArgs.data(), innerArgs.size()); + callbackWrapper->callback().call( + callbackWrapper->runtime(), + (const jsi::Value *)innerArgs.data(), + innerArgs.size()); }); }; } -TurboCxxModule::TurboCxxModule(std::unique_ptr cxxModule, std::shared_ptr jsInvoker) - : TurboModule(cxxModule->getName(), jsInvoker), - cxxMethods_(cxxModule->getMethods()), - cxxModule_(std::move(cxxModule)) {} +TurboCxxModule::TurboCxxModule( + std::unique_ptr cxxModule, + std::shared_ptr jsInvoker) + : TurboModule(cxxModule->getName(), jsInvoker), + cxxMethods_(cxxModule->getMethods()), + cxxModule_(std::move(cxxModule)) {} -jsi::Value TurboCxxModule::get(jsi::Runtime& runtime, const jsi::PropNameID& propName) { +jsi::Value TurboCxxModule::get( + jsi::Runtime &runtime, + const jsi::PropNameID &propName) { std::string propNameUtf8 = propName.utf8(runtime); if (propNameUtf8 == "getConstants") { - // This is special cased because `getConstants()` is already a part of CxxModule. + // This is special cased because `getConstants()` is already a part of + // CxxModule. return jsi::Function::createFromHostFunction( runtime, propName, 0, - [this](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) { + [this]( + jsi::Runtime &rt, + const jsi::Value &thisVal, + const jsi::Value *args, + size_t count) { jsi::Object result(rt); auto constants = cxxModule_->getConstants(); for (auto &pair : constants) { - result.setProperty(rt, pair.first.c_str(), jsi::valueFromDynamic(rt, pair.second)); + result.setProperty( + rt, pair.first.c_str(), jsi::valueFromDynamic(rt, pair.second)); } return result; }); @@ -62,7 +76,11 @@ jsi::Value TurboCxxModule::get(jsi::Runtime& runtime, const jsi::PropNameID& pro runtime, propName, 0, - [this, propNameUtf8](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) { + [this, propNameUtf8]( + jsi::Runtime &rt, + const jsi::Value &thisVal, + const jsi::Value *args, + size_t count) { return invokeMethod(rt, VoidKind, propNameUtf8, args, count); }); } @@ -77,7 +95,6 @@ jsi::Value TurboCxxModule::invokeMethod( const std::string &methodName, const jsi::Value *args, size_t count) { - auto it = cxxMethods_.begin(); for (; it != cxxMethods_.end(); it++) { auto method = *it; @@ -87,7 +104,8 @@ jsi::Value TurboCxxModule::invokeMethod( } if (it == cxxMethods_.end()) { - throw std::runtime_error("Function '" + methodName + "' cannot be found on cxxmodule: " + name_); + throw std::runtime_error( + "Function '" + methodName + "' cannot be found on cxxmodule: " + name_); } auto method = *it; @@ -97,23 +115,37 @@ jsi::Value TurboCxxModule::invokeMethod( for (size_t i = 0; i < count; i++) { innerArgs.push_back(jsi::dynamicFromValue(runtime, args[i])); } - return jsi::valueFromDynamic(runtime, method.syncFunc(std::move(innerArgs))); + return jsi::valueFromDynamic( + runtime, method.syncFunc(std::move(innerArgs))); } else if (method.func && !method.isPromise) { // Async method. CxxModule::Callback first; CxxModule::Callback second; if (count < method.callbacks) { - throw std::invalid_argument(folly::to("Expected ", method.callbacks, - " callbacks, but only ", count, " parameters provided")); + throw std::invalid_argument(folly::to( + "Expected ", + method.callbacks, + " callbacks, but only ", + count, + " parameters provided")); } if (method.callbacks == 1) { - auto wrapper = std::make_shared(args[count - 1].getObject(runtime).getFunction(runtime), runtime, jsInvoker_); + auto wrapper = std::make_shared( + args[count - 1].getObject(runtime).getFunction(runtime), + runtime, + jsInvoker_); first = makeTurboCxxModuleCallback(runtime, wrapper); } else if (method.callbacks == 2) { - auto wrapper1 = std::make_shared(args[count - 2].getObject(runtime).getFunction(runtime), runtime, jsInvoker_); - auto wrapper2 = std::make_shared(args[count - 1].getObject(runtime).getFunction(runtime), runtime, jsInvoker_); + auto wrapper1 = std::make_shared( + args[count - 2].getObject(runtime).getFunction(runtime), + runtime, + jsInvoker_); + auto wrapper2 = std::make_shared( + args[count - 1].getObject(runtime).getFunction(runtime), + runtime, + jsInvoker_); first = makeTurboCxxModuleCallback(runtime, wrapper1); second = makeTurboCxxModuleCallback(runtime, wrapper2); } @@ -125,19 +157,26 @@ jsi::Value TurboCxxModule::invokeMethod( method.func(std::move(innerArgs), first, second); } else if (method.isPromise) { - return createPromiseAsJSIValue(runtime, [method, args, count, this](jsi::Runtime &rt, std::shared_ptr promise) { - auto resolveWrapper = std::make_shared(promise->resolve_.getFunction(rt), rt, jsInvoker_); - auto rejectWrapper = std::make_shared(promise->reject_.getFunction(rt), rt, jsInvoker_); - CxxModule::Callback resolve = makeTurboCxxModuleCallback(rt, resolveWrapper); - CxxModule::Callback reject = makeTurboCxxModuleCallback(rt, rejectWrapper); + return createPromiseAsJSIValue( + runtime, + [method, args, count, this]( + jsi::Runtime &rt, std::shared_ptr promise) { + auto resolveWrapper = std::make_shared( + promise->resolve_.getFunction(rt), rt, jsInvoker_); + auto rejectWrapper = std::make_shared( + promise->reject_.getFunction(rt), rt, jsInvoker_); + CxxModule::Callback resolve = + makeTurboCxxModuleCallback(rt, resolveWrapper); + CxxModule::Callback reject = + makeTurboCxxModuleCallback(rt, rejectWrapper); - auto innerArgs = folly::dynamic::array(); - for (size_t i = 0; i < count; i++) { - innerArgs.push_back(jsi::dynamicFromValue(rt, args[i])); - } + auto innerArgs = folly::dynamic::array(); + for (size_t i = 0; i < count; i++) { + innerArgs.push_back(jsi::dynamicFromValue(rt, args[i])); + } - method.func(std::move(innerArgs), resolve, reject); - }); + method.func(std::move(innerArgs), resolve, reject); + }); } return jsi::Value::undefined(); diff --git a/ReactCommon/turbomodule/core/TurboModuleUtils.h b/ReactCommon/turbomodule/core/TurboModuleUtils.h index b998aa05c3..ee26f199fe 100644 --- a/ReactCommon/turbomodule/core/TurboModuleUtils.h +++ b/ReactCommon/turbomodule/core/TurboModuleUtils.h @@ -7,8 +7,10 @@ #pragma once +#include #include +#include #include #include @@ -32,18 +34,61 @@ struct Promise { jsi::Function reject_; }; -using PromiseSetupFunctionType = std::function)>; -jsi::Value createPromiseAsJSIValue(jsi::Runtime &rt, const PromiseSetupFunctionType func); +using PromiseSetupFunctionType = + std::function)>; +jsi::Value createPromiseAsJSIValue( + jsi::Runtime &rt, + const PromiseSetupFunctionType func); // Helper for passing jsi::Function arg to other methods. -struct CallbackWrapper { - CallbackWrapper(jsi::Function callback, jsi::Runtime &runtime, std::shared_ptr jsInvoker) - : callback(std::move(callback)), - runtime(runtime), - jsInvoker(jsInvoker) {} - jsi::Function callback; - jsi::Runtime &runtime; - std::shared_ptr jsInvoker; +class CallbackWrapper { + private: + struct Data { + Data( + jsi::Function callback, + jsi::Runtime &runtime, + std::shared_ptr jsInvoker) + : callback(std::move(callback)), + runtime(runtime), + jsInvoker(std::move(jsInvoker)) {} + + jsi::Function callback; + jsi::Runtime &runtime; + std::shared_ptr jsInvoker; + }; + + folly::Optional data_; + + public: + CallbackWrapper( + jsi::Function callback, + jsi::Runtime &runtime, + std::shared_ptr jsInvoker) + : data_(Data{std::move(callback), runtime, jsInvoker}) {} + + // Delete the enclosed jsi::Function + void destroy() { + data_ = folly::none; + } + + bool isDestroyed() { + return !data_.hasValue(); + } + + jsi::Function &callback() { + assert(!isDestroyed()); + return data_->callback; + } + + jsi::Runtime &runtime() { + assert(!isDestroyed()); + return data_->runtime; + } + + react::JSCallInvoker &jsInvoker() { + assert(!isDestroyed()); + return *(data_->jsInvoker); + } }; } // namespace react diff --git a/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.cpp b/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.cpp index 86a01ef072..9c97fe2c05 100644 --- a/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.cpp +++ b/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.cpp @@ -13,9 +13,7 @@ #include #include -#include #include -#include #include #include #include @@ -31,35 +29,69 @@ JavaTurboModule::JavaTurboModule( std::shared_ptr jsInvoker) : TurboModule(name, jsInvoker), instance_(jni::make_global(instance)) {} -jni::local_ref createJavaCallbackFromJSIFunction( +jni::local_ref +JavaTurboModule::createJavaCallbackFromJSIFunction( jsi::Function &function, jsi::Runtime &rt, std::shared_ptr jsInvoker) { auto wrapper = std::make_shared( std::move(function), rt, jsInvoker); - std::function fn = [wrapper](folly::dynamic responses) { - if (wrapper == nullptr) { + callbackWrappers_.insert(wrapper); + + std::function fn = [this, + wrapper](folly::dynamic responses) { + if (wrapper->isDestroyed()) { throw std::runtime_error("callback arg cannot be called more than once"); } - std::shared_ptr rw = wrapper; - wrapper->jsInvoker->invokeAsync([rw, responses]() { + + wrapper->jsInvoker().invokeAsync([this, wrapper, responses]() { + if (wrapper->isDestroyed()) { + return; + } + // TODO (T43155926) valueFromDynamic already returns a Value array. Don't // iterate again - jsi::Value args = jsi::valueFromDynamic(rw->runtime, responses); - auto argsArray = args.getObject(rw->runtime).asArray(rw->runtime); + jsi::Value args = jsi::valueFromDynamic(wrapper->runtime(), responses); + auto argsArray = + args.getObject(wrapper->runtime()).asArray(wrapper->runtime()); std::vector result; - for (size_t i = 0; i < argsArray.size(rw->runtime); i++) { + for (size_t i = 0; i < argsArray.size(wrapper->runtime()); i++) { result.emplace_back( - rw->runtime, argsArray.getValueAtIndex(rw->runtime, i)); + wrapper->runtime(), + argsArray.getValueAtIndex(wrapper->runtime(), i)); } - rw->callback.call( - rw->runtime, (const jsi::Value *)result.data(), result.size()); + wrapper->callback().call( + wrapper->runtime(), (const jsi::Value *)result.data(), result.size()); + + /** + * Eagerly destroy the jsi::Function since it's already been invoked. + * TODO(T48128233) Do we want callbacks to be invoked only once? + * + * NOTE: ~JavaTurboModule and this function run on the same thread. + * If you reach this point, you know that the destructor wasn't run + * because the current wrapper wasn't destroyed. Therefore, it's + * safe to access callbackWrappers_. + */ + wrapper->destroy(); + callbackWrappers_.erase(wrapper); }); }; - wrapper = nullptr; return JCxxCallbackImpl::newObjectCxxArgs(fn); } +JavaTurboModule::~JavaTurboModule() { + /** + * Delete all jsi::Functions that haven't yet been invoked by Java. + * So long as nothing else aside from the JS heap is holding on to this + * JavaTurboModule, this destructor is guaranteed to execute before the + * jsi::Runtime is deleted. + */ + for (auto it = callbackWrappers_.begin(); it != callbackWrappers_.end(); + it++) { + (*it)->destroy(); + } +} + namespace { template @@ -184,7 +216,7 @@ std::vector getMethodArgTypesFromSignature( // needs to be done again // TODO (axe) Reuse existing implementation as needed - the exist in // MethodInvoker.cpp -std::vector convertJSIArgsToJNIArgs( +std::vector JavaTurboModule::convertJSIArgsToJNIArgs( JNIEnv *env, jsi::Runtime &rt, std::string methodName, diff --git a/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.h b/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.h index 9e3648bcfc..e088943754 100644 --- a/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.h +++ b/ReactCommon/turbomodule/core/platform/android/JavaTurboModule.h @@ -8,10 +8,13 @@ #pragma once #include +#include #include +#include #include #include +#include namespace facebook { namespace react { @@ -35,9 +38,34 @@ class JSI_EXPORT JavaTurboModule : public TurboModule { const jsi::Value *args, size_t count); + /** + * This dtor must be called from the JS Thread, since it accesses + * callbackWrappers_, which createJavaCallbackFromJSIFunction also accesses + * from the JS Thread. + */ + virtual ~JavaTurboModule(); + private: jni::global_ref instance_; - jclass findClass(JNIEnv *env) const; + std::unordered_set> callbackWrappers_; + + /** + * This method must be called from the JS Thread, since it accesses + * callbackWrappers_. + */ + jni::local_ref createJavaCallbackFromJSIFunction( + jsi::Function &function, + jsi::Runtime &rt, + std::shared_ptr jsInvoker); + std::vector convertJSIArgsToJNIArgs( + JNIEnv *env, + jsi::Runtime &rt, + std::string methodName, + std::vector methodArgTypes, + const jsi::Value *args, + size_t count, + std::shared_ptr jsInvoker, + TurboModuleMethodValueKind valueKind); }; } // namespace react diff --git a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.mm b/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.mm index 5d5e4ecb59..af31001e76 100644 --- a/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.mm +++ b/ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.mm @@ -28,20 +28,24 @@ using namespace facebook; /** * All static helper functions are ObjC++ specific. */ -static jsi::Value convertNSNumberToJSIBoolean(jsi::Runtime &runtime, NSNumber *value) { +static jsi::Value convertNSNumberToJSIBoolean(jsi::Runtime &runtime, NSNumber *value) +{ return jsi::Value((bool)[value boolValue]); } -static jsi::Value convertNSNumberToJSINumber(jsi::Runtime &runtime, NSNumber *value) { +static jsi::Value convertNSNumberToJSINumber(jsi::Runtime &runtime, NSNumber *value) +{ return jsi::Value([value doubleValue]); } -static jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value) { +static jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value) +{ return jsi::String::createFromUtf8(runtime, [value UTF8String] ?: ""); } static jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value); -static jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, NSDictionary *value) { +static jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, NSDictionary *value) +{ jsi::Object result = jsi::Object(runtime); for (NSString *k in value) { result.setProperty(runtime, [k UTF8String], convertObjCObjectToJSIValue(runtime, value[k])); @@ -49,7 +53,8 @@ static jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, NSDicti return result; } -static jsi::Array convertNSArrayToJSIArray(jsi::Runtime &runtime, NSArray *value) { +static jsi::Array convertNSArrayToJSIArray(jsi::Runtime &runtime, NSArray *value) +{ jsi::Array result = jsi::Array(runtime, value.count); for (size_t i = 0; i < value.count; i++) { result.setValueAtIndex(runtime, i, convertObjCObjectToJSIValue(runtime, value[i])); @@ -57,7 +62,8 @@ static jsi::Array convertNSArrayToJSIArray(jsi::Runtime &runtime, NSArray *value return result; } -static std::vector convertNSArrayToStdVector(jsi::Runtime &runtime, NSArray *value) { +static std::vector convertNSArrayToStdVector(jsi::Runtime &runtime, NSArray *value) +{ std::vector result; for (size_t i = 0; i < value.count; i++) { result.emplace_back(convertObjCObjectToJSIValue(runtime, value[i])); @@ -65,7 +71,8 @@ static std::vector convertNSArrayToStdVector(jsi::Runtime &runtime, return result; } -static jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value) { +static jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value) +{ if ([value isKindOfClass:[NSString class]]) { return convertNSStringToJSIString(runtime, (NSString *)value); } else if ([value isKindOfClass:[NSNumber class]]) { @@ -83,22 +90,35 @@ static jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value) { return jsi::Value::undefined(); } -static id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, std::shared_ptr jsInvoker); -static NSString *convertJSIStringToNSString(jsi::Runtime &runtime, const jsi::String &value) { +static id convertJSIValueToObjCObject( + jsi::Runtime &runtime, + const jsi::Value &value, + std::shared_ptr jsInvoker); +static NSString *convertJSIStringToNSString(jsi::Runtime &runtime, const jsi::String &value) +{ return [NSString stringWithUTF8String:value.utf8(runtime).c_str()]; } -static NSArray *convertJSIArrayToNSArray(jsi::Runtime &runtime, const jsi::Array &value, std::shared_ptr jsInvoker) { +static NSArray *convertJSIArrayToNSArray( + jsi::Runtime &runtime, + const jsi::Array &value, + std::shared_ptr jsInvoker) +{ size_t size = value.size(runtime); NSMutableArray *result = [NSMutableArray new]; for (size_t i = 0; i < size; i++) { // Insert kCFNull when it's `undefined` value to preserve the indices. - [result addObject:convertJSIValueToObjCObject(runtime, value.getValueAtIndex(runtime, i), jsInvoker) ?: (id)kCFNull]; + [result + addObject:convertJSIValueToObjCObject(runtime, value.getValueAtIndex(runtime, i), jsInvoker) ?: (id)kCFNull]; } return [result copy]; } -static NSDictionary *convertJSIObjectToNSDictionary(jsi::Runtime &runtime, const jsi::Object &value, std::shared_ptr jsInvoker) { +static NSDictionary *convertJSIObjectToNSDictionary( + jsi::Runtime &runtime, + const jsi::Object &value, + std::shared_ptr jsInvoker) +{ jsi::Array propertyNames = value.getPropertyNames(runtime); size_t size = propertyNames.size(runtime); NSMutableDictionary *result = [NSMutableDictionary new]; @@ -113,8 +133,15 @@ static NSDictionary *convertJSIObjectToNSDictionary(jsi::Runtime &runtime, const return [result copy]; } -static RCTResponseSenderBlock convertJSIFunctionToCallback(jsi::Runtime &runtime, const jsi::Function &value, std::shared_ptr jsInvoker); -static id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &value, std::shared_ptr jsInvoker) { +static RCTResponseSenderBlock convertJSIFunctionToCallback( + jsi::Runtime &runtime, + const jsi::Function &value, + std::shared_ptr jsInvoker); +static id convertJSIValueToObjCObject( + jsi::Runtime &runtime, + const jsi::Value &value, + std::shared_ptr jsInvoker) +{ if (value.isUndefined() || value.isNull()) { return nil; } @@ -141,7 +168,11 @@ static id convertJSIValueToObjCObject(jsi::Runtime &runtime, const jsi::Value &v throw std::runtime_error("Unsupported jsi::jsi::Value kind"); } -static RCTResponseSenderBlock convertJSIFunctionToCallback(jsi::Runtime &runtime, const jsi::Function &value, std::shared_ptr jsInvoker) { +static RCTResponseSenderBlock convertJSIFunctionToCallback( + jsi::Runtime &runtime, + const jsi::Function &value, + std::shared_ptr jsInvoker) +{ __block auto wrapper = std::make_shared(value.getFunction(runtime), runtime, jsInvoker); return ^(NSArray *responses) { if (wrapper == nullptr) { @@ -149,9 +180,9 @@ static RCTResponseSenderBlock convertJSIFunctionToCallback(jsi::Runtime &runtime } std::shared_ptr rw = wrapper; - wrapper->jsInvoker->invokeAsync([rw, responses]() { - std::vector args = convertNSArrayToStdVector(rw->runtime, responses); - rw->callback.call(rw->runtime, (const jsi::Value *)args.data(), args.size()); + wrapper->jsInvoker().invokeAsync([rw, responses]() { + std::vector args = convertNSArrayToStdVector(rw->runtime(), responses); + rw->callback().call(rw->runtime(), (const jsi::Value *)args.data(), args.size()); }); // The callback is single-use, so force release it here. @@ -167,7 +198,8 @@ struct PromiseWrapper : public react::LongLivedObject { jsi::Function resolve, jsi::Function reject, jsi::Runtime &runtime, - std::shared_ptr jsInvoker) { + std::shared_ptr jsInvoker) + { auto instance = std::make_shared(std::move(resolve), std::move(reject), runtime, jsInvoker); // This instance needs to live longer than the caller's scope, since the resolve/reject functions may not // be called immediately. Doing so keeps it alive at least until resolve/reject is called, or when the @@ -181,12 +213,15 @@ struct PromiseWrapper : public react::LongLivedObject { jsi::Function reject, jsi::Runtime &runtime, std::shared_ptr jsInvoker) - : resolveWrapper(std::make_shared(std::move(resolve), runtime, jsInvoker)), - rejectWrapper(std::make_shared(std::move(reject), runtime, jsInvoker)), - runtime(runtime), - jsInvoker(jsInvoker) {} + : resolveWrapper(std::make_shared(std::move(resolve), runtime, jsInvoker)), + rejectWrapper(std::make_shared(std::move(reject), runtime, jsInvoker)), + runtime(runtime), + jsInvoker(jsInvoker) + { + } - RCTPromiseResolveBlock resolveBlock() { + RCTPromiseResolveBlock resolveBlock() + { return ^(id result) { if (resolveWrapper == nullptr) { throw std::runtime_error("Promise resolve arg cannot be called more than once"); @@ -195,9 +230,9 @@ struct PromiseWrapper : public react::LongLivedObject { // Retain the resolveWrapper so that it stays alive inside the lambda. std::shared_ptr retainedWrapper = resolveWrapper; jsInvoker->invokeAsync([retainedWrapper, result]() { - jsi::Runtime &rt = retainedWrapper->runtime; + jsi::Runtime &rt = retainedWrapper->runtime(); jsi::Value arg = convertObjCObjectToJSIValue(rt, result); - retainedWrapper->callback.call(rt, arg); + retainedWrapper->callback().call(rt, arg); }); // Prevent future invocation of the same resolve() function. @@ -205,7 +240,8 @@ struct PromiseWrapper : public react::LongLivedObject { }; } - RCTPromiseRejectBlock rejectBlock() { + RCTPromiseRejectBlock rejectBlock() + { return ^(NSString *code, NSString *message, NSError *error) { // TODO: There is a chance `this` is no longer valid when this block executes. if (rejectWrapper == nullptr) { @@ -216,9 +252,9 @@ struct PromiseWrapper : public react::LongLivedObject { std::shared_ptr retainedWrapper = rejectWrapper; NSDictionary *jsError = RCTJSErrorFromCodeMessageAndNSError(code, message, error); jsInvoker->invokeAsync([retainedWrapper, jsError]() { - jsi::Runtime &rt = retainedWrapper->runtime; + jsi::Runtime &rt = retainedWrapper->runtime(); jsi::Value arg = convertNSDictionaryToJSIObject(rt, jsError); - retainedWrapper->callback.call(rt, arg); + retainedWrapper->callback().call(rt, arg); }); // Prevent future invocation of the same resolve() function. @@ -226,23 +262,26 @@ struct PromiseWrapper : public react::LongLivedObject { }; } - void cleanup() { + void cleanup() + { resolveWrapper = nullptr; rejectWrapper = nullptr; allowRelease(); } - // CallbackWrapper is used here instead of just holding on the jsi jsi::Function in order to force release it after either - // the resolve() or the reject() is called. jsi jsi::Function does not support explicit releasing, so we need an extra - // mechanism to control that lifecycle. + // CallbackWrapper is used here instead of just holding on the jsi jsi::Function in order to force release it after + // either the resolve() or the reject() is called. jsi jsi::Function does not support explicit releasing, so we need + // an extra mechanism to control that lifecycle. std::shared_ptr resolveWrapper; std::shared_ptr rejectWrapper; jsi::Runtime &runtime; std::shared_ptr jsInvoker; }; -using PromiseInvocationBlock = void (^)(jsi::Runtime& rt, std::shared_ptr wrapper); -static jsi::Value createPromise(jsi::Runtime &runtime, std::shared_ptr jsInvoker, PromiseInvocationBlock invoke) { +using PromiseInvocationBlock = void (^)(jsi::Runtime &rt, std::shared_ptr wrapper); +static jsi::Value +createPromise(jsi::Runtime &runtime, std::shared_ptr jsInvoker, PromiseInvocationBlock invoke) +{ if (!invoke) { return jsi::Value::undefined(); } @@ -293,8 +332,8 @@ jsi::Value performMethodInvocation( TurboModuleMethodValueKind valueKind, const id module, std::shared_ptr jsInvoker, - NSMutableArray *retainedObjectsForInvocation) { - + NSMutableArray *retainedObjectsForInvocation) +{ __block id result; jsi::Runtime *rt = &runtime; void (^block)() = ^{ @@ -370,7 +409,8 @@ jsi::Value performMethodInvocation( * Note: This is only being introduced for backward compatibility. It will be removed * in the future. */ -NSString* ObjCTurboModule::getArgumentTypeName(NSString* methodName, int argIndex) { +NSString *ObjCTurboModule::getArgumentTypeName(NSString *methodName, int argIndex) +{ if (!methodArgumentTypeNames_) { NSMutableDictionary *> *methodArgumentTypeNames = [NSMutableDictionary new]; @@ -381,7 +421,7 @@ NSString* ObjCTurboModule::getArgumentTypeName(NSString* methodName, int argInde if (methods) { for (unsigned int i = 0; i < numberOfMethods; i++) { SEL s = method_getName(methods[i]); - NSString* mName = NSStringFromSelector(s); + NSString *mName = NSStringFromSelector(s); if (![mName hasPrefix:@"__rct_export__"]) { continue; } @@ -393,7 +433,7 @@ NSString* ObjCTurboModule::getArgumentTypeName(NSString* methodName, int argInde NSArray *arguments; NSString *otherMethodName = RCTParseMethodSignature(methodInfo->objcName, &arguments); - NSMutableArray* argumentTypes = [NSMutableArray arrayWithCapacity:[arguments count]]; + NSMutableArray *argumentTypes = [NSMutableArray arrayWithCapacity:[arguments count]]; for (int j = 0; j < [arguments count]; j += 1) { [argumentTypes addObject:arguments[j].type]; } @@ -417,16 +457,18 @@ NSString* ObjCTurboModule::getArgumentTypeName(NSString* methodName, int argInde } NSInvocation *ObjCTurboModule::getMethodInvocation( - jsi::Runtime &runtime, - TurboModuleMethodValueKind valueKind, - const id module, - std::shared_ptr jsInvoker, - const std::string& methodName, - SEL selector, - const jsi::Value *args, - size_t count, - NSMutableArray *retainedObjectsForInvocation) { - NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[[module class] instanceMethodSignatureForSelector:selector]]; + jsi::Runtime &runtime, + TurboModuleMethodValueKind valueKind, + const id module, + std::shared_ptr jsInvoker, + const std::string &methodName, + SEL selector, + const jsi::Value *args, + size_t count, + NSMutableArray *retainedObjectsForInvocation) +{ + NSInvocation *inv = + [NSInvocation invocationWithMethodSignature:[[module class] instanceMethodSignatureForSelector:selector]]; [inv setSelector:selector]; NSMethodSignature *methodSignature = [[module class] instanceMethodSignatureForSelector:selector]; @@ -504,7 +546,7 @@ NSInvocation *ObjCTurboModule::getMethodInvocation( * Convert objects using RCTConvert. */ if (objCArgType[0] == _C_ID) { - NSString* argumentType = getArgumentTypeName(methodNameNSString, i); + NSString *argumentType = getArgumentTypeName(methodNameNSString, i); if (argumentType != nil) { NSString *rctConvertMethodName = [NSString stringWithFormat:@"%@:", argumentType]; SEL rctConvertSelector = NSSelectorFromString(rctConvertMethodName); @@ -556,8 +598,9 @@ ObjCTurboModule::ObjCTurboModule( const std::string &name, id instance, std::shared_ptr jsInvoker) - : TurboModule(name, jsInvoker), - instance_(instance) {} + : TurboModule(name, jsInvoker), instance_(instance) +{ +} jsi::Value ObjCTurboModule::invokeObjCMethod( jsi::Runtime &runtime, @@ -568,39 +611,41 @@ jsi::Value ObjCTurboModule::invokeObjCMethod( size_t count) { NSMutableArray *retainedObjectsForInvocation = [NSMutableArray arrayWithCapacity:count + 2]; - NSInvocation *inv = getMethodInvocation(runtime, valueKind, instance_, jsInvoker_, methodName, selector, args, count, retainedObjectsForInvocation); + NSInvocation *inv = getMethodInvocation( + runtime, valueKind, instance_, jsInvoker_, methodName, selector, args, count, retainedObjectsForInvocation); if (valueKind == PromiseKind) { // Promise return type is special cased today, i.e. it needs extra 2 function args for resolve() and reject(), to // be passed to the actual ObjC++ class method. - return createPromise( - runtime, - jsInvoker_, - ^(jsi::Runtime &rt, std::shared_ptr wrapper) { - RCTPromiseResolveBlock resolveBlock = wrapper->resolveBlock(); - RCTPromiseRejectBlock rejectBlock = wrapper->rejectBlock(); - [inv setArgument:(void *)&resolveBlock atIndex:count + 2]; - [inv setArgument:(void *)&rejectBlock atIndex:count + 3]; - [retainedObjectsForInvocation addObject:resolveBlock]; - [retainedObjectsForInvocation addObject:rejectBlock]; - // The return type becomes void in the ObjC side. - performMethodInvocation(rt, inv, VoidKind, instance_, jsInvoker_, retainedObjectsForInvocation); - }); + return createPromise(runtime, jsInvoker_, ^(jsi::Runtime &rt, std::shared_ptr wrapper) { + RCTPromiseResolveBlock resolveBlock = wrapper->resolveBlock(); + RCTPromiseRejectBlock rejectBlock = wrapper->rejectBlock(); + [inv setArgument:(void *)&resolveBlock atIndex:count + 2]; + [inv setArgument:(void *)&rejectBlock atIndex:count + 3]; + [retainedObjectsForInvocation addObject:resolveBlock]; + [retainedObjectsForInvocation addObject:rejectBlock]; + // The return type becomes void in the ObjC side. + performMethodInvocation(rt, inv, VoidKind, instance_, jsInvoker_, retainedObjectsForInvocation); + }); } return performMethodInvocation(runtime, inv, valueKind, instance_, jsInvoker_, retainedObjectsForInvocation); } -BOOL ObjCTurboModule::hasMethodArgConversionSelector(NSString *methodName, int argIndex) { - return methodArgConversionSelectors_ && methodArgConversionSelectors_[methodName] && ![methodArgConversionSelectors_[methodName][argIndex] isEqual:[NSNull null]]; +BOOL ObjCTurboModule::hasMethodArgConversionSelector(NSString *methodName, int argIndex) +{ + return methodArgConversionSelectors_ && methodArgConversionSelectors_[methodName] && + ![methodArgConversionSelectors_[methodName][argIndex] isEqual:[NSNull null]]; } -SEL ObjCTurboModule::getMethodArgConversionSelector(NSString *methodName, int argIndex) { +SEL ObjCTurboModule::getMethodArgConversionSelector(NSString *methodName, int argIndex) +{ assert(hasMethodArgConversionSelector(methodName, argIndex)); return (SEL)((NSValue *)methodArgConversionSelectors_[methodName][argIndex]).pointerValue; } -void ObjCTurboModule::setMethodArgConversionSelector(NSString *methodName, int argIndex, NSString *fnName) { +void ObjCTurboModule::setMethodArgConversionSelector(NSString *methodName, int argIndex, NSString *fnName) +{ if (!methodArgConversionSelectors_) { methodArgConversionSelectors_ = [NSMutableDictionary new]; }