Skip to content

Commit 87cfd38

Browse files
KunalFarmah98facebook-github-bot
authored andcommitted
Added additional builder method receiving arguments for using jsc or hermes to correctly decide which DSO to load at app startup. (#33952)
Summary: The current implementation of **getDefaultJSExecutorFactory** relies solely on try catch to load the correct .so file for jsc or hermes based on the project configuration. Relying solely on try catch block and loading jsc even when project is using hermes can lead to launch time crashes especially in monorepo architectures and hybrid apps using both native android and react native. So we can make use of an additional **ReactInstanceManager :: setJsEngineAsHermes** method that accepts a Boolean argument from the host app while building ReactInstanceManager which can tell which library to load at startup in **ReactInstanceManagerBuilder** which will now have an enhanced getDefaultJSExecutorFactory method that will combine the old logic with the new one to load the dso files. The code snippet in **ReactInstanceManager** for adding a new setter method: ``` /** * Sets the jsEngine as JSC or HERMES as per the setJsEngineAsHermes call * Uses the enum {link JSInterpreter} * param jsEngine */ private void setJSEngine(JSInterpreter jsEngine){ this.jsEngine = jsEngine; } /** * Utility setter to set the required JSEngine as HERMES or JSC * Defaults to OLD_LOGIC if not called by the host app * param hermesEnabled * hermesEnabled = true sets the JS Engine as HERMES and JSC otherwise */ public ReactInstanceManagerBuilder setJsEngineAsHermes(boolean hermesEnabled){ if(hermesEnabled){ setJSEngine(JSInterpreter.HERMES); } else{ setJSEngine(JSInterpreter.JSC); } return this; } ``` The code snippet for the new logic in **ReactInstanceManagerBuilder**: 1) Setting up the new logic: Adding a new enum class : ``` public enum JSInterpreter { OLD_LOGIC, JSC, HERMES } ``` A setter getting boolean value telling whether to use hermes or not and calling a private setter to update the enum variable. ``` /** * Sets the jsEngine as JSC or HERMES as per the setJsEngineAsHermes call * Uses the enum {link JSInterpreter} * param jsEngine */ private void setJSEngine(JSInterpreter jsEngine){ this.jsEngine = jsEngine; } /** * Utility setter to set the required JSEngine as HERMES or JSC * Defaults to OLD_LOGIC if not called by the host app * param hermesEnabled * hermesEnabled = true sets the JS Engine as HERMES and JSC otherwise */ public ReactInstanceManagerBuilder setJsEngineAsHermes(boolean hermesEnabled){ if(hermesEnabled){ setJSEngine(JSInterpreter.HERMES); } else{ setJSEngine(JSInterpreter.JSC); } return this; } ``` 2) Modifying the getDefaultJSExecutorFactory method to incorporate the new logic with the old one: ``` private JavaScriptExecutorFactory getDefaultJSExecutorFactory( String appName, String deviceName, Context applicationContext) { // Relying solely on try catch block and loading jsc even when // project is using hermes can lead to launch-time crashes especially in // monorepo architectures and hybrid apps using both native android // and react native. // So we can use the value of enableHermes received by the constructor // to decide which library to load at launch // if nothing is specified, use old loading method // else load the required engine if (jsEngine == JSInterpreter.OLD_LOGIC) { try { // If JSC is included, use it as normal initializeSoLoaderIfNecessary(applicationContext); JSCExecutor.loadLibrary(); return new JSCExecutorFactory(appName, deviceName); } catch (UnsatisfiedLinkError jscE) { if (jscE.getMessage().contains("__cxa_bad_typeid")) { throw jscE; } HermesExecutor.loadLibrary(); return new HermesExecutorFactory(); } } else if (jsEngine == JSInterpreter.HERMES) { HermesExecutor.loadLibrary(); return new HermesExecutorFactory(); } else { JSCExecutor.loadLibrary(); return new JSCExecutorFactory(appName, deviceName); } } ``` ### **Suggested changes in any Android App's MainApplication that extends ReactApplication to take advantage of this fix** ``` builder = ReactInstanceManager.builder() .setApplication(this) .setJsEngineAsHermes(BuildConfig.HERMES_ENABLED) .setBundleAssetName("index.android.bundle") .setJSMainModulePath("index") ``` where HERMES_ENABLED is a buildConfigField based on the enableHermes flag in build.gradle: `def enableHermes = project.ext.react.get("enableHermes", true) ` and then ``` defaultConfig{ if(enableHermes) { buildConfigField("boolean", "HERMES_ENABLED", "true") } else{ buildConfigField("boolean", "HERMES_ENABLED", "false") } } ``` Our app was facing a similar issue as listed in this list: **https://github.com/facebook/react-native/issues?q=is%3Aissue+is%3Aopen+DSO**. Which was react-native trying to load jsc even when our project used hermes when a debug build was deployed on a device using android studio play button. This change can possibly solve many of the issues listed in the list as it solved ours. ## Changelog [GENERAL] [ADDED] - An enum JSInterpreter in com.facebook.react package: ``` /** * An enum that specifies the JS Engine to be used in the app * Old Logic uses the legacy code * JSC/HERMES loads the respective engine using the revamped logic */ public enum JSInterpreter { OLD_LOGIC, JSC, HERMES } ``` [GENERAL] [ADDED] - An enum variable storing the default value of Js Engine loading mechanism in ReactInstanceManagerBuilder: ``` private JSInterpreter jsEngine = JSInterpreter.OLD_LOGIC; ``` [GENERAL] [ADDED] - A new setter method and a helper method to set the js engine in ReactInstanceManagerBuilder: ``` /** * Sets the jsEngine as JSC or HERMES as per the setJsEngineAsHermes call * Uses the enum {link JSInterpreter} * param jsEngine */ private void setJSEngine(JSInterpreter jsEngine){ this.jsEngine = jsEngine; } /** * Utility setter to set the required JSEngine as HERMES or JSC * Defaults to OLD_LOGIC if not called by the host app * param hermesEnabled * hermesEnabled = true sets the JS Engine as HERMES and JSC otherwise */ public ReactInstanceManagerBuilder setJsEngineAsHermes(boolean hermesEnabled){ if(hermesEnabled){ setJSEngine(JSInterpreter.HERMES); } else{ setJSEngine(JSInterpreter.JSC); } return this; } ``` [GENERAL] [ADDED] - Modified **getDefaultJSExecutorFactory** method ``` private JavaScriptExecutorFactory getDefaultJSExecutorFactory( String appName, String deviceName, Context applicationContext) { // Relying solely on try catch block and loading jsc even when // project is using hermes can lead to launch-time crashes especially in // monorepo architectures and hybrid apps using both native android // and react native. // So we can use the value of enableHermes received by the constructor // to decide which library to load at launch // if nothing is specified, use old loading method // else load the required engine if (jsEngine == JSInterpreter.OLD_LOGIC) { try { // If JSC is included, use it as normal initializeSoLoaderIfNecessary(applicationContext); JSCExecutor.loadLibrary(); return new JSCExecutorFactory(appName, deviceName); } catch (UnsatisfiedLinkError jscE) { if (jscE.getMessage().contains("__cxa_bad_typeid")) { throw jscE; } HermesExecutor.loadLibrary(); return new HermesExecutorFactory(); } } else if (jsEngine == JSInterpreter.HERMES) { HermesExecutor.loadLibrary(); return new HermesExecutorFactory(); } else { JSCExecutor.loadLibrary(); return new JSCExecutorFactory(appName, deviceName); } } ``` Pull Request resolved: #33952 Test Plan: The testing for this change might be tricky but can be done by following the reproduction steps in the issues related to DSO loading here: https://github.com/facebook/react-native/issues?q=is%3Aissue+is%3Aopen+DSO Generally, the app will not crash anymore on deploying debug using android studio if we are removing libjsc and its related libraries in **packagingOptions** in build.gradle and using hermes in the project. It can be like: ``` packagingOptions { if (enableHermes) { exclude "**/libjsc*.so" } } ``` Reviewed By: lunaleaps Differential Revision: D37191981 Pulled By: cortinico fbshipit-source-id: c528ead126939f1d788af7523f3798ed2a14f36e
1 parent fa814d4 commit 87cfd38

File tree

2 files changed

+68
-30
lines changed

2 files changed

+68
-30
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react;
9+
10+
/**
11+
* An enum that specifies the JS Engine to be used in the app Old Logic uses the legacy code
12+
* JSC/HERMES loads the respective engine using the revamped logic
13+
*/
14+
public enum JSInterpreter {
15+
OLD_LOGIC,
16+
JSC,
17+
HERMES
18+
}

ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManagerBuilder.java

+50-30
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public class ReactInstanceManagerBuilder {
6666
private @Nullable Map<String, RequestHandler> mCustomPackagerCommandHandlers;
6767
private @Nullable ReactPackageTurboModuleManagerDelegate.Builder mTMMDelegateBuilder;
6868
private @Nullable SurfaceDelegateFactory mSurfaceDelegateFactory;
69+
private JSInterpreter jsInterpreter = JSInterpreter.OLD_LOGIC;
6970

7071
/* package protected */ ReactInstanceManagerBuilder() {}
7172

@@ -125,6 +126,31 @@ public ReactInstanceManagerBuilder setJSBundleLoader(JSBundleLoader jsBundleLoad
125126
return this;
126127
}
127128

129+
/**
130+
* Sets the jsEngine as JSC or HERMES as per the setJsEngineAsHermes call Uses the enum {@link
131+
* JSInterpreter}
132+
*
133+
* @param jsInterpreter
134+
*/
135+
private void setJSEngine(JSInterpreter jsInterpreter) {
136+
this.jsInterpreter = jsInterpreter;
137+
}
138+
139+
/**
140+
* Utility setter to set the required JSEngine as HERMES or JSC Defaults to OLD_LOGIC if not
141+
* called by the host app
142+
*
143+
* @param hermesEnabled hermesEnabled = true sets the JS Engine as HERMES and JSC otherwise
144+
*/
145+
public ReactInstanceManagerBuilder setJsEngineAsHermes(boolean hermesEnabled) {
146+
if (hermesEnabled) {
147+
setJSEngine(JSInterpreter.HERMES);
148+
} else {
149+
setJSEngine(JSInterpreter.JSC);
150+
}
151+
return this;
152+
}
153+
128154
/**
129155
* Path to your app's main module on Metro. This is used when reloading JS during development. All
130156
* paths are relative to the root folder the packager is serving files from. Examples: {@code
@@ -345,41 +371,35 @@ public ReactInstanceManager build() {
345371

346372
private JavaScriptExecutorFactory getDefaultJSExecutorFactory(
347373
String appName, String deviceName, Context applicationContext) {
348-
try {
349-
// If JSC is included, use it as normal
350-
initializeSoLoaderIfNecessary(applicationContext);
351-
JSCExecutor.loadLibrary();
352-
return new JSCExecutorFactory(appName, deviceName);
353-
} catch (UnsatisfiedLinkError jscE) {
354-
// https://github.com/facebook/hermes/issues/78 shows that
355-
// people who aren't trying to use Hermes are having issues.
356-
// https://github.com/facebook/react-native/issues/25923#issuecomment-554295179
357-
// includes the actual JSC error in at least one case.
358-
//
359-
// So, if "__cxa_bad_typeid" shows up in the jscE exception
360-
// message, then we will assume that's the failure and just
361-
// throw now.
362-
363-
if (jscE.getMessage().contains("__cxa_bad_typeid")) {
364-
throw jscE;
365-
}
366374

367-
// Otherwise use Hermes
375+
// Relying solely on try catch block and loading jsc even when
376+
// project is using hermes can lead to launch-time crashes especially in
377+
// monorepo architectures and hybrid apps using both native android
378+
// and react native.
379+
// So we can use the value of enableHermes received by the constructor
380+
// to decide which library to load at launch
381+
382+
// if nothing is specified, use old loading method
383+
// else load the required engine
384+
if (jsInterpreter == JSInterpreter.OLD_LOGIC) {
368385
try {
386+
// If JSC is included, use it as normal
387+
initializeSoLoaderIfNecessary(applicationContext);
388+
JSCExecutor.loadLibrary();
389+
return new JSCExecutorFactory(appName, deviceName);
390+
} catch (UnsatisfiedLinkError jscE) {
391+
if (jscE.getMessage().contains("__cxa_bad_typeid")) {
392+
throw jscE;
393+
}
369394
HermesExecutor.loadLibrary();
370395
return new HermesExecutorFactory();
371-
} catch (UnsatisfiedLinkError hermesE) {
372-
// If we get here, either this is a JSC build, and of course
373-
// Hermes failed (since it's not in the APK), or it's a Hermes
374-
// build, and Hermes had a problem.
375-
376-
// We suspect this is a JSC issue (it's the default), so we
377-
// will throw that exception, but we will print hermesE first,
378-
// since it could be a Hermes issue and we don't want to
379-
// swallow that.
380-
hermesE.printStackTrace();
381-
throw jscE;
382396
}
397+
} else if (jsInterpreter == JSInterpreter.HERMES) {
398+
HermesExecutor.loadLibrary();
399+
return new HermesExecutorFactory();
400+
} else {
401+
JSCExecutor.loadLibrary();
402+
return new JSCExecutorFactory(appName, deviceName);
383403
}
384404
}
385405
}

0 commit comments

Comments
 (0)