facebook/react-native

RCT_EXTERN_MODULE Swift modules broken in Xcode 10.2

kdawgwilk opened this issue ยท 65 comments

๐Ÿ› 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:

#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }

It looks like you are using an older version of React Native. Please update to the latest release, v0.59 and verify if the issue still exists.

The "Resolution: Old Version" label will be removed automatically once you edit your original post with the results of running `react-native info` on a project using the latest release.

same issue

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.

Same here, upgrade to 10.2 and it appears

same issue here

same issue

same issue here

same issue here

same issue

same issue

same issue

Anyone got a temporary hack to get around this one?

same issue

same here, this needs to be fixed asap, we're stuck because of this.

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

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]));
  }
}

I found this while researching the issue, I'm not sure if it's relevant: pmusolino/Wormholy#52

I tried to implement a similar solution, but my Objective-C is not very good, so I'm not sure how to call RCTRegisterModule(self) if we have to create a C/C++ function using attribute((constructor)). Perhaps someone smarter than I could work it out!

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.

@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.

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

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.

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

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

Yeah, I had to rename my iPhone XR simulator. Apple had changed it to iPhone Xส€. They used some special character that renders a smaller, yet still uppercase, letter. Fancy, Apple... fancy.

Not only am I a module maker, but also a module user.
So, does this means that all swift modules out there will fail with XCode 10.2 ?
I can't see how patching RN will automatically fix third party modules. Am I wrong?

EDIT : I've removed a comment below. Here is what I was talking about.

I've applied @zienag fix and now all my native modules - including React Native - are broken. I have a ton of warnings like

2019-03-27 16:15:21.013 [info][tid:main][RCTBridge.m:134] Class RCTNativeAnimatedModule was not exported. Did you forget to use RCT_EXPORT_MODULE()?
2019-03-27 16:15:21.012721+0100 MyHeartSigns[456:31950] Class RCTNativeAnimatedModule was not exported. Did you forget to use RCT_EXPORT_MODULE()?
2019-03-27 16:15:21.013 [info][tid:main][RCTBridge.m:134] Class RCTModalManager was not exported. Did you forget to use RCT_EXPORT_MODULE()?
2019-03-27 16:15:21.012839+0100 MyHeartSigns[456:31950] Class RCTModalManager was not exported. Did you forget to use RCT_EXPORT_MODULE()?
2019-03-27 16:15:21.013 [info][tid:main][RCTBridge.m:134] Class RCTLocationObserver was not exported. Did you forget to use RCT_EXPORT_MODULE()?
2019-03-27 16:15:21.013139+0100 MyHeartSigns[456:31950] Class RCTLocationObserver was not exported. Did you forget to use RCT_EXPORT_MODULE()?
2019-03-27 16:15:21.013 [info][tid:main][RCTBridge.m:134] Class RCTLinkingManager was not exported. Did you forget to use RCT_EXPORT_MODULE()?

@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 ๐Ÿ™‚

@omatrot I have applied the fix and am also using RNFetchBlob and everything is working exactly as expected, are you positive you have made the change correctly?

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

@omatrot zienag's fix worked for me as well. Maybe you are missing something else.
BTW I used https://github.com/ds300/patch-package to patch my react installation, I highly recommend it

@brenwell @juliandramirez Are you using CocoaPods for your native dependencies?

@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 ๐Ÿ™‚

Actually this is not entirely true, as it doesn't change "binary code" in react-native itself, it patches code that generates from macros in users code.
To fix be applied you need to recompile all the modules. That means, if you have dependencies that are distributing in binary form (static or dynamic lib) you need to recompile all dependencies with fix applied.
@omatrot do you have binary dependencies?

@zienag I would say no, because my Podfile contains:

use_frameworks!

@zienag I would say no, because my Podfile contains:

use_frameworks!

AFAIK, that is not related to this. Binary dependency means that it is prebuilt and there is no available code (aka "proprietary", for cocoapods it usually means that instead of code you have framework, For example, different ads/tracking libs are distributed like this, Fabric or Google Mobile Ads SDK). If you have prebuilt dependency that uses RCT_EXTERN_MODULE from react-native it won't work until recompiled with fix.

@zienag Now I'm confused because it is not about RCT_EXTERN_MODULE but RCT_EXPORT_MODULE in the error messages. Are you sure the fix is not breaking this macro?

@zienag Now I'm confused because it is not about RCT_EXTERN_MODULE but RCT_EXPORT_MODULE in the error messages. Are you sure the fix is not breaking this macro?

I see, I were wrong, sorry. Although issue that I described exists, that is not your case. It seems like __attribute__((constructor)) is not called. Are you sure that you are not using RCT_EXPORT_PRE_REGISTERED_MODULE from previous workaround? If yes, can you run function with constructor attribute? Something like this, put it anywhere in your objc code, build and run, it should print log before entering main:

__attribute__((constructor)) static void justConstructor() {
  NSLog(@"hello from function with constructor attribute");
}

@zienag I'm sure I'm not using the previous workaround, because I wont patch third party modules. I'm using your modifications in RCTBridgeModule.h. I'm not experiencing crash at startup anymore but all these error messages about modules not exported.

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

@omatrot Which react-native version are you using? You might have issues if you use RCTBridgeModule.h from master against a previous version of RN

I'm on RN 0.59.1.

Hmm, RCTNativeAnimatedModule doesn't even use RCT_EXTERN_MODULE macro. It uses RCT_EXPORT_MODULE, pr doesn't touch it at all.

@omatrot Can you try only adding @zienag 's changes to your RCTBridgeModule.h?

@zienag I know this is weird.
@yassinecc This is what I've done.

  • Without the changes I'm experiencing crashes at startup with the same error highlighted in this issue.
  • With the changes, no crash but none of the third party modules I'm using are exported anymore...

I would say that we need to patch RCT_EXPORT_MODULE as well. What do you think?

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.

@omatrot yep using cocoapods on 0.59.2. I build for simulator, device and made a release. No problems

@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 ๐Ÿค—

I would say that we need to patch RCT_EXPORT_MODULE as well. What do you think?

I wrote small comment in pr about it. As addition, changing RCT_EXPORT_MODULE probably will not help you, unless we just discovered a serious bug in objc runtime. I think you can try two things:

  1. Update react native to 59.2
  2. If it will not help, try to run preprocessor in one of the files that are not registering. For example, given file:
MyModule.m:
@implementation MyModule
...
RCT_EXPORT_MODULE("MyModule")
...
@end

Running preprocessor gives something like this:

@implementation MyModule
...
extern __attribute__((visibility("default"))) void RCTRegisterModule(Class); 
+ (NSString *)moduleName { return @"MyModule"; } 
+ (void)load { 
  RCTRegisterModule(self); 
}
...
@end

If you compare code before and after fix โ€“ it should be different. If it is not, that is not related to fix from pr.

@zienag I've done point 1 already. No change. I was thinking about running the preprocessor. I'll do that.

@davethomas11 solution helped me. Thanks!! ๐Ÿ‘

(let's keep this open until the PR lands)

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

@zienag

Here we go:

Message:

2019-03-27 19:34:11.944 [info][tid:main][RCTBridge.m:134] Class RNDeviceInfo was not exported. Did you forget to use RCT_EXPORT_MODULE()?

Preprocessor result on file RNDeviceInfo.m :

extern __attribute__((visibility("default"))) void RCTRegisterModule(Class); + (NSString *)moduleName { return @"RNDeviceInfo"; } + (void)load { RCTRegisterModule(self); }

The files are compiled correctly :(

@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

Okay, I started from scratch with a new project. I still have the warnings but the app runs.

So was this fixed or not?

@mrTuomoK it looks like a PR with the fix landed (ff66600), 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

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

@githubdoramon we just releases 0.59.3, for 0.60 it will be a few more weeks I feel, we are still working on one key feature we want to have in it (you can always keep an eye on -> https://github.com/react-native-community/react-native-releases)

Btw, pls LMK if now it's fixed in 0.59.3

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

@kelset Having this on 0.59.3 was more than enough. Thanks a lot... Also working here

Anybody have not a problem with NativeAppEventEmitter in RN 0.53 after this patch?
(I applied this patch on my old RN manually)
It does not trigger JS listener.

No, my bad. Sorry guys.
It's working fine.

I'm getting the same problem. I've narrowed it down to the point where you add an Obj-C implementation file wrapped in RCT_EXTERN_MODULE. This causes a crash. Adding the file itself does not cause a crash, it's not until you wrap the interface in RCT_EXTERN_MODULE.

This is consistent even when the class is empty.

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.

Could you explain a little bit more about where you'd use RCT_EXPORT_PRE_REGISTERED_MODULE please? Google returns only 2 results on it (and 1 of those is here).

Using it in my .m file in the same way that you'd use RCT_EXTERN_MODULE has no affect. So do you use it in your Swift file, or a .m?

Hi, guys. I solved this problem adding this 12.2 folder ( https://github.com/iGhibli/iOS-DeviceSupport/blob/master/DeviceSupport/12.2%20(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!