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.