react-native: RCT_EXTERN_MODULE Swift modules broken in Xcode 10.2

šŸ› Bug Report

It appears Obj-C runtime got more strict with Swift ABI stability in Xcode 10.2 and now causes a crash at runtime when trying to load modules that have Swift implementations. This is the crash error:

Swift class extensions and categories on Swift classes are not allowed to have +load methods

To Reproduce

Create a RN NativeModule e.g.

BadgeHandler.m

#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(BadgeHandler, NSObject)
    RCT_EXTERN_METHOD(setBadgeNumber:(int *)badgeNumber)
@end

BadgeHandler.swift

import UIKit

@objc(BadgeHandler)
class BadgeHandler: NSObject, RCTBridgeModule {
    static func moduleName() -> String! {
        return "BadgeHandler";
    }

    static func requiresMainQueueSetup() -> Bool {
        return true
    }
    
    @objc func setBadgeNumber(_ badgeNumber: Int) {
        DispatchQueue.main.async {
            UIApplication.shared.applicationIconBadgeNumber = badgeNumber
        }
    }
}

AppName-Bridging-Header.h

#ifndef AppName_Bridging_Header_h
#define AppName_Bridging_Header_h

#import <React/RCTBridgeModule.h>

#endif /* AppName_Bridging_Header_h */

Expected Behavior

The NativeModule would load fine at runtime like it did in previous versions of Xcode

Code Example

See above ^

Environment

  React Native Environment Info:
    System:
      OS: macOS 10.14.3
      CPU: (12) x64 Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
      Memory: 31.36 MB / 32.00 GB
      Shell: 5.3 - /bin/zsh
    Binaries:
      Node: 11.6.0 - /usr/local/bin/node
      Yarn: 1.13.0 - /usr/local/bin/yarn
      npm: 6.5.0 - /usr/local/bin/npm
      Watchman: 4.9.0 - /usr/local/bin/watchman
    SDKs:
      iOS SDK:
        Platforms: iOS 12.2, macOS 10.14, tvOS 12.2, watchOS 5.2
      Android SDK:
        API Levels: 23, 25, 26, 27, 28
        Build Tools: 27.0.3, 28.0.2, 28.0.3
        System Images: android-28 | Google APIs Intel x86 Atom
    IDEs:
      Xcode: 10.2/10E125 - /usr/bin/xcodebuild
    npmPackages:
      react: 16.8.3 => 16.8.3
      react-native: 0.59.1 => 0.59.1

Other

This seems to point to this macro in RCTBridgeModule.h:

https://github.com/facebook/react-native/blob/d9eae2a8093156c8faae0c5ae14546bbc95e7a3d/React/Base/RCTBridgeModule.h#L71-L74

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 77
  • Comments: 65 (7 by maintainers)

Commits related to this issue

Most upvoted comments

Hi! Iā€™m not very familiar with react-native, but I found myself here because Iā€™m blocked from upgrading to 10.2 by one of dependency that uses react-native.

I have an idea for a fix though:

#define RCT_EXPORT_MODULE(js_name, objc_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
__attribute__((constructor)) \
static void RCT_CONCAT(initialize_, objc_name)() { RCTRegisterModule([objc_name class]); }

The problem with this is that RCT_EXPORT_MODULE macro will require objc_name parameter now. As I understand, this is not very convenient, as it is used in many places by users. So another solution is to introduce another macro:

#define RCT_EXPORT_MODULE_NO_LOAD(js_name, objc_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
__attribute__((constructor)) \
static void RCT_CONCAT(initialize_, objc_name)() { RCTRegisterModule([objc_name class]); }

And replace call in RCT_EXTERN_REMAP_MODULE to new macro:

#define RCT_EXTERN_REMAP_MODULE(js_name, objc_name, objc_supername) \
  objc_name : objc_supername \
  @end \
  @interface objc_name (RCTExternModule) <RCTBridgeModule> \
  @end \
  @implementation objc_name (RCTExternModule) \
  RCT_EXPORT_MODULE_NO_LOAD(js_name, objc_name)

Iā€™m ok to make a pr, but it will take some time until I figure out how to setup everything.

Hey everyone - two things:

  1. writing comments like ā€œsame issueā€ doesnā€™t help at all. There are reactions for that. To prevent more spam, Iā€™ll lock the issue for now

  2. if you want to help, ideal case would be to submit a PR to fix the issue. Feel free to @ me in the PR so that we can try to have it reviewā€™d and merged quickly.

  3. Please make sure you are using RN 0.59.2 because thatā€™s the only version we are going to patch once the PR with the fix lands - at least for now. To upgrade please refer to the documentation -> https://facebook.github.io/react-native/docs/upgrading#react-native-projects (if you are upgrading from an earlier version than 0.59.0 refer to https://github.com/pvinis/rn-diff-purge)

Weā€™ll write more comments when we have more info on this.

Iā€™ve implemented a (temporary) workaround, but Iā€™m not sure how to fix it properly in React.

In your (Swift based) bridge modules, use the marco RCT_EXPORT_PRE_REGISTERED_MODULE() instead of RCT_EXPORT_MODULE. This prevents the + (void)load method to be implemented on it.

Then, in your main.m, just register the Swift modules manually:

#import <UIKit/UIKit.h>
#import <React/RCTDefines.h>

#import "AppDelegate.h"
#import "<YourProjectName>-Swift.h"

RCT_EXTERN void RCTRegisterModule(Class);

int main(int argc, char * argv[]) {
  @autoreleasepool {
    RCTRegisterModule([Module1 self]);
    RCTRegisterModule([Module2 self]);

    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
  }
}

If you canā€™t update your react-native version to get this fix when it is merged because you are using the Expo fork of react-native. Then add this code ( based on above PR ) to a header file:

/**
 * Same as RCT_EXPORT_MODULE, but uses __attribute__((constructor)) for module
 * registration. Useful for registering swift classes that forbids use of load
 * Used in RCT_EXTERN_REMAP_MODULE
 */
#define RCT_EXPORT_MODULE_NO_LOAD(js_name, objc_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
__attribute__((constructor)) static void \
RCT_CONCAT(initialize_, objc_name)() { RCTRegisterModule([objc_name class]); }

#define RCT_EXTERN_MODULE_2(objc_name, objc_supername) \
RCT_EXTERN_REMAP_MODULE_2(, objc_name, objc_supername)

/**
 * Like RCT_EXTERN_MODULE, but allows setting a custom JavaScript name.
 */
#define RCT_EXTERN_REMAP_MODULE_2(js_name, objc_name, objc_supername) \
objc_name : objc_supername \
@end \
@interface objc_name (RCTExternModule) <RCTBridgeModule> \
@end \
@implementation objc_name (RCTExternModule) \
RCT_EXPORT_MODULE_NO_LOAD(js_name, objc_name)

Then in your module .m objective C file import that header file you just created and use RCT_EXTERN_MODULE_2 instead of RCT_EXTERN_MODULE

Example:

#import "RctExternModule2HeaderFileThatYouCreatedReplaceFilenameHere.h"
#import "React/RCTViewManager.h"

@interface RCT_EXTERN_MODULE_2(SomeModuleNameOfYoursHere, RCTViewManager)
@end

Fixed in 0.59.3 for me (Xcode 10.2, iOS 12.2)

The (semi) quick fix is to downgrade to Xcode 10.1.

Youā€™ll find the download here: https://developer.apple.com/download/more/

After upgrading I had trouble with my simulators so I nuked them. The below will delete all simulators. You may then add back the ones you need. Use with care though. xcrun simctl list | grep -oh '[A-Z0-9]\{8\}-[A-Z0-9]\{4\}-[A-Z0-9]\{4\}-[A-Z0-9]\{4\}-[A-Z0-9]\{12\}' | xargs -n1 xcrun simctl delete

Hi, guys. I solved this problem adding this 12.2 folder ( https://github.com/iGhibli/iOS-DeviceSupport/blob/master/DeviceSupport/12.2 (16E226).zip ) to Xcode(in applications) -> Show Package Contents -> Contents -> Developer -> iPhoneIOs.platform -> Device Support (you will see list of folders: 8.0, 8.1, 8.2 etcā€¦). After you will add folder named 12.2 here - everything will work fine!

@mrTuomoK it looks like a PR with the fix landed (https://github.com/facebook/react-native/commit/ff66600224e78fec5d0e902f8a035b78ed31a961), Iā€™ll see if we can cherry-pick it for 0.59.3 but if itā€™s not possible it will be out in 0.60

I found a similar issue in one of the iconic-team repo. They just deleted all ā€œloadā€ methods in their code to avoid this problem. I searched inside the react-native folder and found THREE files with +(void)load method for ios. They are: React/Base/RCTBridgeModule.h Libraries/WebSocket/RCTReconnectingWebSocket.m RNTester/Swizzle_RCTFabricSurface.m

Hope it helps.

@brenwell Iā€™ve overwritten the whole node_modules/react-native/React/Base/RCTBridgeModule.h with the one coming from the pull requestā€¦

My current workaround was to install iOS 12.1 simulator from xcode => preferences => components, and after that just launched the app with older simulator. It seems that the newer simulators use a lower-case second letter and the older ones capital. for example Xs => XS

Specifically, the error doesnā€™t happen on devices/simulators prior to 12.2. So it doesnā€™t seem xcode version related. It seems more tied to swift version.

@zienag a PR would be super appreciated šŸ˜ƒ Iā€™ve tried to raise this with the FB team but they are all in US atm so it will take them a few more hours to reply to me.

@kelset are there any expectations on release dates (approximately) for 0.59.3 and 0.60?

@renzhezizhi can you comment on this PR about your finding -> #24155

Because I think that PR is trying to fix the issue in a similar way šŸ¤—

Sure. Have done. XD

Sorry about closing the issue here, looks like GitHub autoclose issues based on private references as well. šŸ˜–

(letā€™s keep this open until the PR lands)

@omatrot zienagā€™s proposed fix will automatically fix third part modules, because it modifies RCT_EXTERN_MODULE, which is how all Swift modules export themselves! If you simply include zienagā€™s fix manually it will allow you to keep working using iOS 12.2.

Alternatively using a simulator for iOS 12.1 will cause this not to be an error as the issue is caused by the iOS version, not the XCode version šŸ™‚

Iā€™d like to clarify that I donā€™t get this error on a physical device.

same issue, and I see the 0.59.2, + (void)load { RCTRegisterModule(self); } , has not changed Now I have reinstalled XCode 10.1 and xcode-select my env to XCode 10.1

update: I update RN to 0.59.3 and XCode 10.2. It work for me.