Skip to content

Commit 3809f27

Browse files
fkgozalifacebook-github-bot
authored andcommitted
TM iOS: Properly clean up CallbackWrapper during reload
Summary: There is a race condition between tearing down the bridge vs CallbackWrapper invoking the wrapped jsi::Function. This means the wrapper can be stale and unsafe to access after the bridge dies. To avoid unsafe access, let's clean it up properly using weak ref. Reviewed By: RSNara Differential Revision: D17380360 fbshipit-source-id: f91ce75d945bf8ed0b141c593bcc674ff465aa8c
1 parent 0c7defc commit 3809f27

File tree

3 files changed

+61
-44
lines changed

3 files changed

+61
-44
lines changed

ReactCommon/turbomodule/core/TurboModuleBinding.cpp

+1-7
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,7 @@ void TurboModuleBinding::install(
4444
}
4545

4646
void TurboModuleBinding::invalidate() const {
47-
// TODO (T45804587): Revisit this invalidation logic.
48-
// The issue was that Promise resolve/reject functions that are invoked in
49-
// some distance future might end up accessing PromiseWrapper that was already
50-
// destroyed, if the binding invalidation removed it from the
51-
// LongLivedObjectCollection.
52-
53-
// LongLivedObjectCollection::get().clear();
47+
LongLivedObjectCollection::get().clear();
5448
}
5549

5650
std::shared_ptr<TurboModule> TurboModuleBinding::getModule(

ReactCommon/turbomodule/core/TurboModuleUtils.h

+15-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <jsi/jsi.h>
1515

1616
#include <ReactCommon/JSCallInvoker.h>
17+
#include <ReactCommon/LongLivedObject.h>
1718

1819
using namespace facebook;
1920

@@ -23,7 +24,7 @@ namespace react {
2324
jsi::Object deepCopyJSIObject(jsi::Runtime &rt, const jsi::Object &obj);
2425
jsi::Array deepCopyJSIArray(jsi::Runtime &rt, const jsi::Array &arr);
2526

26-
struct Promise {
27+
struct Promise : public LongLivedObject {
2728
Promise(jsi::Runtime &rt, jsi::Function resolve, jsi::Function reject);
2829

2930
void resolve(const jsi::Value &result);
@@ -41,7 +42,8 @@ jsi::Value createPromiseAsJSIValue(
4142
const PromiseSetupFunctionType func);
4243

4344
// Helper for passing jsi::Function arg to other methods.
44-
class CallbackWrapper {
45+
// TODO (ramanpreet): Simplify with weak_ptr<>
46+
class CallbackWrapper : public LongLivedObject {
4547
private:
4648
struct Data {
4749
Data(
@@ -60,6 +62,16 @@ class CallbackWrapper {
6062
folly::Optional<Data> data_;
6163

6264
public:
65+
static std::weak_ptr<CallbackWrapper> createWeak(
66+
jsi::Function callback,
67+
jsi::Runtime &runtime,
68+
std::shared_ptr<react::JSCallInvoker> jsInvoker) {
69+
auto wrapper = std::make_shared<CallbackWrapper>(
70+
std::move(callback), runtime, jsInvoker);
71+
LongLivedObjectCollection::get().add(wrapper);
72+
return wrapper;
73+
}
74+
6375
CallbackWrapper(
6476
jsi::Function callback,
6577
jsi::Runtime &runtime,
@@ -69,6 +81,7 @@ class CallbackWrapper {
6981
// Delete the enclosed jsi::Function
7082
void destroy() {
7183
data_ = folly::none;
84+
allowRelease();
7285
}
7386

7487
bool isDestroyed() {

ReactCommon/turbomodule/core/platform/ios/RCTTurboModule.mm

+45-35
Original file line numberDiff line numberDiff line change
@@ -173,22 +173,28 @@ static RCTResponseSenderBlock convertJSIFunctionToCallback(
173173
const jsi::Function &value,
174174
std::shared_ptr<react::JSCallInvoker> jsInvoker)
175175
{
176-
__block auto wrapper = std::make_shared<react::CallbackWrapper>(value.getFunction(runtime), runtime, jsInvoker);
176+
auto weakWrapper = react::CallbackWrapper::createWeak(value.getFunction(runtime), runtime, jsInvoker);
177+
BOOL __block wrapperWasCalled = NO;
177178
return ^(NSArray *responses) {
178-
if (wrapper == nullptr) {
179+
if (wrapperWasCalled) {
179180
throw std::runtime_error("callback arg cannot be called more than once");
180181
}
181182

182-
std::shared_ptr<react::CallbackWrapper> rw = wrapper;
183-
wrapper->jsInvoker().invokeAsync([rw, responses]() {
184-
std::vector<jsi::Value> args = convertNSArrayToStdVector(rw->runtime(), responses);
185-
rw->callback().call(rw->runtime(), (const jsi::Value *)args.data(), args.size());
186-
});
183+
auto strongWrapper = weakWrapper.lock();
184+
if (!strongWrapper) {
185+
return;
186+
}
187187

188-
// The callback is single-use, so force release it here.
189-
// Doing this also releases the jsi::jsi::Function early, since this block may not get released by ARC for a while,
190-
// because the method invocation isn't guarded with @autoreleasepool.
191-
wrapper = nullptr;
188+
strongWrapper->jsInvoker().invokeAsync([weakWrapper, responses]() {
189+
auto strongWrapper2 = weakWrapper.lock();
190+
if (!strongWrapper2) {
191+
return;
192+
}
193+
194+
std::vector<jsi::Value> args = convertNSArrayToStdVector(strongWrapper2->runtime(), responses);
195+
strongWrapper2->callback().call(strongWrapper2->runtime(), (const jsi::Value *)args.data(), args.size());
196+
strongWrapper2->destroy();
197+
});
192198
};
193199
}
194200

@@ -222,13 +228,13 @@ static RCTResponseSenderBlock convertJSIFunctionToCallback(
222228
return jsi::Value::undefined();
223229
}
224230

225-
std::shared_ptr<CallbackWrapper> resolveWrapper =
226-
std::make_shared<react::CallbackWrapper>(args[0].getObject(rt).getFunction(rt), rt, jsInvoker);
227-
std::shared_ptr<CallbackWrapper> rejectWrapper =
228-
std::make_shared<react::CallbackWrapper>(args[1].getObject(rt).getFunction(rt), rt, jsInvoker);
231+
auto weakResolveWrapper =
232+
react::CallbackWrapper::createWeak(args[0].getObject(rt).getFunction(rt), rt, jsInvoker);
233+
auto weakRejectWrapper =
234+
react::CallbackWrapper::createWeak(args[1].getObject(rt).getFunction(rt), rt, jsInvoker);
229235

230-
BOOL __block resolveWasCalled = NO;
231-
BOOL __block rejectWasCalled = NO;
236+
__block BOOL resolveWasCalled = NO;
237+
__block BOOL rejectWasCalled = NO;
232238

233239
RCTPromiseResolveBlock resolveBlock = ^(id result) {
234240
if (rejectWasCalled) {
@@ -239,23 +245,25 @@ static RCTResponseSenderBlock convertJSIFunctionToCallback(
239245
throw std::runtime_error("Tried to resolve a promise more than once.");
240246
}
241247

242-
// In the case that ObjC runtime first invokes this block after
243-
// the TurboModuleManager was invalidated, we should do nothing.
244-
if (resolveWrapper->isDestroyed()) {
248+
auto strongResolveWrapper = weakResolveWrapper.lock();
249+
auto strongRejectWrapper = weakRejectWrapper.lock();
250+
if (!strongResolveWrapper || !strongRejectWrapper) {
245251
return;
246252
}
247253

248-
resolveWrapper->jsInvoker().invokeAsync([resolveWrapper, rejectWrapper, result]() {
249-
if (resolveWrapper->isDestroyed()) {
254+
strongResolveWrapper->jsInvoker().invokeAsync([weakResolveWrapper, weakRejectWrapper, result]() {
255+
auto strongResolveWrapper2 = weakResolveWrapper.lock();
256+
auto strongRejectWrapper2 = weakRejectWrapper.lock();
257+
if (!strongResolveWrapper2 || !strongRejectWrapper2) {
250258
return;
251259
}
252260

253-
jsi::Runtime &rt = resolveWrapper->runtime();
261+
jsi::Runtime &rt = strongResolveWrapper2->runtime();
254262
jsi::Value arg = convertObjCObjectToJSIValue(rt, result);
255-
resolveWrapper->callback().call(rt, arg);
263+
strongResolveWrapper2->callback().call(rt, arg);
256264

257-
resolveWrapper->destroy();
258-
rejectWrapper->destroy();
265+
strongResolveWrapper2->destroy();
266+
strongRejectWrapper2->destroy();
259267
});
260268

261269
resolveWasCalled = YES;
@@ -270,24 +278,26 @@ static RCTResponseSenderBlock convertJSIFunctionToCallback(
270278
throw std::runtime_error("Tried to reject a promise more than once.");
271279
}
272280

273-
// In the case that ObjC runtime first invokes this block after
274-
// the TurboModuleManager was invalidated, we should do nothing.
275-
if (rejectWrapper->isDestroyed()) {
281+
auto strongResolveWrapper = weakResolveWrapper.lock();
282+
auto strongRejectWrapper = weakRejectWrapper.lock();
283+
if (!strongResolveWrapper || !strongRejectWrapper) {
276284
return;
277285
}
278286

279287
NSDictionary *jsError = RCTJSErrorFromCodeMessageAndNSError(code, message, error);
280-
rejectWrapper->jsInvoker().invokeAsync([rejectWrapper, resolveWrapper, jsError]() {
281-
if (rejectWrapper->isDestroyed()) {
288+
strongRejectWrapper->jsInvoker().invokeAsync([weakResolveWrapper, weakRejectWrapper, jsError]() {
289+
auto strongResolveWrapper2 = weakResolveWrapper.lock();
290+
auto strongRejectWrapper2 = weakRejectWrapper.lock();
291+
if (!strongResolveWrapper2 || !strongRejectWrapper2) {
282292
return;
283293
}
284294

285-
jsi::Runtime &rt = rejectWrapper->runtime();
295+
jsi::Runtime &rt = strongRejectWrapper2->runtime();
286296
jsi::Value arg = convertNSDictionaryToJSIObject(rt, jsError);
287-
rejectWrapper->callback().call(rt, arg);
297+
strongRejectWrapper2->callback().call(rt, arg);
288298

289-
rejectWrapper->destroy();
290-
resolveWrapper->destroy();
299+
strongResolveWrapper2->destroy();
300+
strongRejectWrapper2->destroy();
291301
});
292302

293303
rejectWasCalled = YES;

0 commit comments

Comments
 (0)