Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/ios-emit-before-module-ready.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@callstack/react-native-brownfield': patch
---

Fix an iOS crash (`std::bad_function_call`) when native code posts a message to JS while the Brownfield module's JS emitter is still being set up. If the Brownfield module exists but its JS emitter is not ready when delivery happens, the message is dropped and a log line is written. If the module does not exist yet, the message is dropped without a log line.
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ container.addView(reactView);

##### `postMessage`

Send a JSON string to the React Native JS layer. The message is delivered as a `brownfieldMessage` DeviceEventEmitter event and can be received using `ReactNativeBrownfield.onMessage()` on the JS side.
Send a JSON string to the React Native JS layer. Messages can be received using `ReactNativeBrownfield.onMessage()`.

| Param | Required | Type | Description |
| ------- | -------- | -------- | ----------------------------------- |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ AndroidView(

##### `postMessage`

Send a JSON string to the React Native JS layer. The message is delivered as a `brownfieldMessage` DeviceEventEmitter event and can be received using `ReactNativeBrownfield.onMessage()` on the JS side.
Send a JSON string to the React Native JS layer. Messages can be received using `ReactNativeBrownfield.onMessage()`.

| Param | Required | Type | Description |
| ------- | -------- | -------- | ----------------------------------- |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ Returns: `BOOL` – return value is passed through from the runtime.

##### `postMessage`

Send a JSON string to the React Native JS layer. The message is delivered as a `brownfieldMessage` DeviceEventEmitter event.
Send a JSON string to the React Native JS layer. Messages can be received using `ReactNativeBrownfield.onMessage()`. If the Brownfield module exists but its JS emitter is not ready when delivery happens, the message is dropped and a log line is written. If the module does not exist yet, the message is dropped without a log line.

| Param | Required | Type | Description |
| --------- | -------- | ---------- | ----------------------------------- |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ let view = ReactNativeBrownfield.shared.view(

##### `postMessage`

Send a JSON string to the React Native JS layer. The message is delivered as a `brownfieldMessage` DeviceEventEmitter event and can be received using `ReactNativeBrownfield.onMessage()` on the JS side.
Send a JSON string to the React Native JS layer. Messages can be received using `ReactNativeBrownfield.onMessage()`. If the Brownfield module exists but its JS emitter is not ready when delivery happens, the message is dropped and a log line is written. If the module does not exist yet, the message is dropped without a log line.

| Param | Required | Type | Description |
| --------- | -------- | -------- | ----------------------------------- |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,9 @@ internal import Expo

/**
* Send a serialized JSON message to the React Native JS application.
* The message is delivered as a `brownfieldMessage` DeviceEventEmitter event.
* Messages can be received using `ReactNativeBrownfield.onMessage()`.
* If the Brownfield module exists but its JS emitter is not ready when delivery happens, the message is dropped and a log line is written.
* If the module does not exist yet, the message is dropped without a log line.
*
* @param message - The serialized JSON message to send to the React Native JS application.
* @example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ @implementation ReactNativeBrownfieldModule
- (instancetype)init {
self = [super init];
if (self) {
_sharedInstance = self;
@synchronized (ReactNativeBrownfieldModule.class) {
_sharedInstance = self;
}

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNativeToJSMessage:)
Expand All @@ -28,8 +30,19 @@ - (instancetype)init {
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];

if (_sharedInstance == self) {
_sharedInstance = nil;
@synchronized (ReactNativeBrownfieldModule.class) {
if (_sharedInstance == self) {
_sharedInstance = nil;
}
}
}

// React Native calls invalidate on modules during teardown (_invalidateModules):
// https://github.com/facebook/react-native/blob/4bc2473f5d0233ea5384c1ef24f6a55615de2220/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm#L944-L985
// Clearing the callback sends later messages down the not-ready drop path.
- (void)invalidate {
@synchronized (ReactNativeBrownfieldModule.class) {
_eventEmitterCallback = nullptr;
}
}

Expand All @@ -41,10 +54,28 @@ - (void)handleNativeToJSMessage:(NSNotification *)notification {
}

+ (void)emitMessageFromNative:(NSString *)message {
if (_sharedInstance) {
// The singleton and the callback are read and written under one lock.
// postMessage delivers on the main queue, while getTurboModule: (below) installs the
// callback when JS first requires the module. React Native calls getTurboModule: from
// https://github.com/facebook/react-native/blob/4bc2473f5d0233ea5384c1ef24f6a55615de2220/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm#L336-L345
// and the JS require path is
// https://github.com/facebook/react-native/blob/4bc2473f5d0233ea5384c1ef24f6a55615de2220/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm#L845-L860.
@synchronized (ReactNativeBrownfieldModule.class) {
if (!_sharedInstance) {
NSLog(@"ReactNativeBrownfieldModule is not initialized, dropping message");
return;
}

// The codegen base class declares the callback as a @protected ivar:
// https://github.com/facebook/react-native/blob/4bc2473f5d0233ea5384c1ef24f6a55615de2220/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js#L44
// Calling an empty std::function throws std::bad_function_call:
// https://en.cppreference.com/w/cpp/utility/functional/function/operator%28%29.html
if (!_sharedInstance->_eventEmitterCallback) {
NSLog(@"ReactNativeBrownfieldModule is not ready for JS yet, dropping message");
return;
}

[_sharedInstance emitOnBrownfieldMessage:@{ @"text": message }];
} else {
NSLog(@"ReactNativeBrownfieldModule is not initialized, dropping message");
}
}

Expand All @@ -65,7 +96,14 @@ + (void)emitMessageFromNative:(NSString *)message {
RCT_EXPORT_METHOD(removeListeners:(double)count) {}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<facebook::react::NativeReactNativeBrownfieldModuleSpecJSI>(params);
// Building the spec installs _eventEmitterCallback. The spec constructor calls setEventEmitterCallback
// (https://github.com/facebook/react-native/blob/4bc2473f5d0233ea5384c1ef24f6a55615de2220/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/source/serializeModule.js#L88),
// ObjCTurboModule forwards it (https://github.com/facebook/react-native/blob/4bc2473f5d0233ea5384c1ef24f6a55615de2220/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm#L924-L931),
// and the generated setter assigns the ivar (https://github.com/facebook/react-native/blob/4bc2473f5d0233ea5384c1ef24f6a55615de2220/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/source/serializeModule.js#L44).
// Hold the same lock as emitMessageFromNative: so the write and the read do not race.
@synchronized (ReactNativeBrownfieldModule.class) {
return std::make_shared<facebook::react::NativeReactNativeBrownfieldModuleSpecJSI>(params);
}
}

@end