From 415b45be510463fdb42011e68e7e6d9cc7092840 Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Wed, 10 Feb 2016 02:08:30 -0800 Subject: [PATCH] Use un-patched RN for Android by installing hook into JSC This works by installing some assembly into JSGlobalContextCreateInGroup() that will immediately jump out into our own wrapper function so we always can inject the Realm constructor into the context. --- .../ReactExample/android/app/build.gradle | 2 +- react-native/android/build.gradle | 2 +- react-native/android/publish_android_template | 2 +- .../io/realm/react/RealmReactAndroid.java | 49 +++++----- react-native/android/src/main/jni/Android.mk | 1 + .../io_realm_react_RealmReactAndroid.cpp | 90 +++++------------- .../io_realm_react_RealmReactAndroid.h | 15 ++- src/android/jsc_override.cpp | 94 +++++++++++++++++++ tests/react-test-app/android/app/build.gradle | 2 +- 9 files changed, 151 insertions(+), 106 deletions(-) create mode 100644 src/android/jsc_override.cpp diff --git a/examples/ReactExample/android/app/build.gradle b/examples/ReactExample/android/app/build.gradle index 51daba44..18f845a3 100644 --- a/examples/ReactExample/android/app/build.gradle +++ b/examples/ReactExample/android/app/build.gradle @@ -73,6 +73,6 @@ android { dependencies { compile 'com.android.support:appcompat-v7:23.0.1' - compile 'com.facebook.react:react-native:0.18.0-patched' + compile 'com.facebook.react:react-native:0.18.0' compile project(':realm') } diff --git a/react-native/android/build.gradle b/react-native/android/build.gradle index 8fba13f8..0ae44443 100644 --- a/react-native/android/build.gradle +++ b/react-native/android/build.gradle @@ -285,5 +285,5 @@ afterEvaluate { project -> dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'org.nanohttpd:nanohttpd:2.2.0' - compile 'com.facebook.react:react-native:0.18.0-patched' + compile 'com.facebook.react:react-native:0.18.0' } diff --git a/react-native/android/publish_android_template b/react-native/android/publish_android_template index fd855c05..ec5e9c81 100644 --- a/react-native/android/publish_android_template +++ b/react-native/android/publish_android_template @@ -29,5 +29,5 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'org.nanohttpd:nanohttpd:2.2.0' - compile 'com.facebook.react:react-native:0.18.0-patched' + compile 'com.facebook.react:react-native:0.18.0' } diff --git a/react-native/android/src/main/java/io/realm/react/RealmReactAndroid.java b/react-native/android/src/main/java/io/realm/react/RealmReactAndroid.java index 6f51abb0..cd96a6f8 100644 --- a/react-native/android/src/main/java/io/realm/react/RealmReactAndroid.java +++ b/react-native/android/src/main/java/io/realm/react/RealmReactAndroid.java @@ -21,19 +21,23 @@ public class RealmReactAndroid extends ReactContextBaseJavaModule { private static final int DEFAULT_PORT = 8082; private AndroidWebServer webServer; - private long rpcServerPtr; - - private String filesDirPath; private Handler handler = new Handler(Looper.getMainLooper()); + static { + SoLoader.loadLibrary("realmreact"); + } + public RealmReactAndroid(ReactApplicationContext reactContext) { super(reactContext); + + String fileDir; try { - filesDirPath = getReactApplicationContext().getFilesDir().getCanonicalPath(); + fileDir = reactContext.getFilesDir().getCanonicalPath(); } catch (IOException e) { throw new IllegalStateException(e); } - SoLoader.loadLibrary("realmreact"); + + setDefaultRealmFileDirectory(fileDir); } @Override @@ -43,31 +47,18 @@ public class RealmReactAndroid extends ReactContextBaseJavaModule { @Override public Map getConstants() { - long contexts = injectRealmJsContext(filesDirPath); - - if (contexts == -1) { - Log.e("RealmReactAndroid", "Error during initializing Realm context"); - throw new IllegalStateException("Error during initializing Realm context"); - } - - Log.i("RealmReactAndroid", "Initialized " + contexts + " contexts"); - - if (contexts == 0) {// we're running in Chrome debug mode - startWebServer(); - } + // FIXME: Only start web server when in Chrome debug mode! + startWebServer(); return Collections.EMPTY_MAP; } @Override public void onCatalystInstanceDestroy() { - if (webServer != null) { - Log.i("RealmReactAndroid", "Stopping the debugging Webserver"); - webServer.stop(); - } + stopWebServer(); } private void startWebServer() { - rpcServerPtr = setupChromeDebugModeRealmJsContext(); + setupChromeDebugModeRealmJsContext(); webServer = new AndroidWebServer(DEFAULT_PORT); try { webServer.start(); @@ -77,7 +68,13 @@ public class RealmReactAndroid extends ReactContextBaseJavaModule { } } - // WebServer + private void stopWebServer() { + if (webServer != null) { + Log.i("RealmReactAndroid", "Stopping the webserver"); + webServer.stop(); + } + } + class AndroidWebServer extends NanoHTTPD { public AndroidWebServer(int port) { super(port); @@ -105,7 +102,7 @@ public class RealmReactAndroid extends ReactContextBaseJavaModule { handler.post(new Runnable() { @Override public void run() { - jsonResponse[0] = processChromeDebugCommand(rpcServerPtr, cmdUri, json); + jsonResponse[0] = processChromeDebugCommand(cmdUri, json); latch.countDown(); } }); @@ -122,11 +119,11 @@ public class RealmReactAndroid extends ReactContextBaseJavaModule { } // fileDir: path of the internal storage of the application - private native long injectRealmJsContext(String fileDir); + private native void setDefaultRealmFileDirectory(String fileDir); // responsible for creating the rpcServer that will accept the chrome Websocket command private native long setupChromeDebugModeRealmJsContext(); // this receives one command from Chrome debug then return the processing we should post back - private native String processChromeDebugCommand(long rpcServerPointer, String cmd, String args); + private native String processChromeDebugCommand(String cmd, String args); } diff --git a/react-native/android/src/main/jni/Android.mk b/react-native/android/src/main/jni/Android.mk index 253b5da6..2ee3a48f 100644 --- a/react-native/android/src/main/jni/Android.mk +++ b/react-native/android/src/main/jni/Android.mk @@ -25,6 +25,7 @@ LOCAL_SRC_FILES := \ src/rpc.cpp \ src/android/platform.cpp \ src/android/io_realm_react_RealmReactAndroid.cpp \ + src/android/jsc_override.cpp \ src/object-store/index_set.cpp \ src/object-store/list.cpp \ src/object-store/object_schema.cpp \ diff --git a/src/android/io_realm_react_RealmReactAndroid.cpp b/src/android/io_realm_react_RealmReactAndroid.cpp index bdede139..c5f454e2 100644 --- a/src/android/io_realm_react_RealmReactAndroid.cpp +++ b/src/android/io_realm_react_RealmReactAndroid.cpp @@ -3,88 +3,42 @@ */ #include -#include -#include -#include - -#include "io_realm_react_RealmReactAndroid.h" -#include "js_init.h" -#include "rpc.hpp" -#include "platform.hpp" -#include "shared_realm.hpp" -#include #include -namespace facebook { - namespace react { - class JSCExecutor; - } -} +#include "io_realm_react_RealmReactAndroid.h" +#include "rpc.hpp" +#include "platform.hpp" -/* - * Class: io_realm_react_RealmReactAndroid - * Method: injectRealmJsContext - * Signature: ()Ljava/lang/String; - */ -JNIEXPORT jlong JNICALL Java_io_realm_react_RealmReactAndroid_injectRealmJsContext +static realm_js::RPCServer *s_rpc_server; + +JNIEXPORT void JNICALL Java_io_realm_react_RealmReactAndroid_setDefaultRealmFileDirectory (JNIEnv *env, jclass, jstring fileDir) - { - __android_log_print(ANDROID_LOG_DEBUG, "JSRealm", "Java_io_realm_react_RealmReactAndroid_injectRealmJsContext"); - void* handle = dlopen ("libreactnativejni.so", RTLD_LAZY); - if (!handle) { - __android_log_print(ANDROID_LOG_ERROR, "JSRealm", "Cannot open libreactnativejni.so"); - return -1; - } +{ + __android_log_print(ANDROID_LOG_VERBOSE, "JSRealm", "setDefaultRealmFileDirectory"); - // Getting the internal storage path for the application - const char* strFileDir = env->GetStringUTFChars(fileDir , NULL); + // Setting the internal storage path for the application + const char* strFileDir = env->GetStringUTFChars(fileDir, NULL); realm::set_default_realm_file_directory(strFileDir); env->ReleaseStringUTFChars(fileDir , strFileDir); - // load the symbol - typedef std::unordered_map (*get_jsc_context_t)(); + __android_log_print(ANDROID_LOG_DEBUG, "JSRealm", "Absolute path: %s", realm::default_realm_file_directory().c_str()); +} - get_jsc_context_t get_jsc_context = (get_jsc_context_t) dlsym(handle, "get_jsc_context"); - - if (get_jsc_context != NULL) { - // clearing previous instances - realm::Realm::s_global_cache.clear(); - std::unordered_map s_globalContextRefToJSCExecutor = get_jsc_context(); - for (auto pair : s_globalContextRefToJSCExecutor) { - RJSInitializeInContext(pair.first); - } - return s_globalContextRefToJSCExecutor.size(); - } else { - __android_log_print(ANDROID_LOG_ERROR, "JSRealm", "Cannot find symbol get_jsc_context"); - return -1; - } - } - -/* - * Class: io_realm_react_RealmReactAndroid - * Method: setupChromeDebugModeRealmJsContext - */ - static realm_js::RPCServer *s_rpc_server; - JNIEXPORT jlong JNICALL Java_io_realm_react_RealmReactAndroid_setupChromeDebugModeRealmJsContext +JNIEXPORT jlong JNICALL Java_io_realm_react_RealmReactAndroid_setupChromeDebugModeRealmJsContext (JNIEnv *, jclass) - { - __android_log_print(ANDROID_LOG_VERBOSE, "JSRealm", "Java_com_reacttests_RealmReactAndroid_setupChromeDebugModeRealmJsContext"); +{ + __android_log_print(ANDROID_LOG_VERBOSE, "JSRealm", "setupChromeDebugModeRealmJsContext"); if (s_rpc_server) { - delete s_rpc_server; + delete s_rpc_server; } s_rpc_server = new realm_js::RPCServer(); return (jlong)s_rpc_server; - } +} -/* - * Class: io_realm_react_RealmReactAndroid - * Method: processChromeDebugCommand - */ - - JNIEXPORT jstring JNICALL Java_io_realm_react_RealmReactAndroid_processChromeDebugCommand - (JNIEnv *env, jclass, jlong rpc_server_ptr, jstring chrome_cmd, jstring chrome_args) - { - __android_log_print(ANDROID_LOG_VERBOSE, "JSRealm", "Java_io_realm_react_RealmReactAndroid_processChromeDebugCommand"); +JNIEXPORT jstring JNICALL Java_io_realm_react_RealmReactAndroid_processChromeDebugCommand + (JNIEnv *env, jclass, jstring chrome_cmd, jstring chrome_args) +{ + __android_log_print(ANDROID_LOG_VERBOSE, "JSRealm", "processChromeDebugCommand"); const char* cmd = env->GetStringUTFChars(chrome_cmd, NULL); const char* args = env->GetStringUTFChars(chrome_args, NULL); realm_js::json json = realm_js::json::parse(args); @@ -92,4 +46,4 @@ JNIEXPORT jlong JNICALL Java_io_realm_react_RealmReactAndroid_injectRealmJsConte env->ReleaseStringUTFChars(chrome_cmd, cmd); env->ReleaseStringUTFChars(chrome_args, args); return env->NewStringUTF(response.dump().c_str()); - } +} diff --git a/src/android/io_realm_react_RealmReactAndroid.h b/src/android/io_realm_react_RealmReactAndroid.h index 33f11ef7..4c5276e5 100644 --- a/src/android/io_realm_react_RealmReactAndroid.h +++ b/src/android/io_realm_react_RealmReactAndroid.h @@ -7,12 +7,12 @@ #ifdef __cplusplus extern "C" { #endif + /* - * Class: io_realm_react_RealmReactAndroid - * Method: injectRealmJsContext - * Signature: ()Ljava/lang/String; - */ -JNIEXPORT jlong JNICALL Java_io_realm_react_RealmReactAndroid_injectRealmJsContext +* Class: io_realm_react_RealmReactAndroid +* Method: setDefaultRealmFileDirectory +*/ +JNIEXPORT void JNICALL Java_io_realm_react_RealmReactAndroid_setDefaultRealmFileDirectory (JNIEnv *, jclass, jstring); /* @@ -24,11 +24,10 @@ JNIEXPORT jlong JNICALL Java_io_realm_react_RealmReactAndroid_setupChromeDebugMo /* * Class: io_realm_react_RealmReactAndroid - * Method: processsetupChromeDebugCommand + * Method: processChromeDebugCommand */ JNIEXPORT jstring JNICALL Java_io_realm_react_RealmReactAndroid_processChromeDebugCommand - (JNIEnv *, jclass, jlong, jstring, jstring); - + (JNIEnv *, jclass, jstring, jstring); #ifdef __cplusplus } diff --git a/src/android/jsc_override.cpp b/src/android/jsc_override.cpp new file mode 100644 index 00000000..1d5f90c0 --- /dev/null +++ b/src/android/jsc_override.cpp @@ -0,0 +1,94 @@ +/* Copyright 2016 Realm Inc - All Rights Reserved + * Proprietary and Confidential + */ + +#include +#include +#include +#include +#include +#include + +#include "js_init.h" +#include "shared_realm.hpp" + +#if __arm__ +#define HOOK_SIZE 8 +#else +#define HOOK_SIZE 5 +#endif + +static void swap_function() __attribute__((constructor)); + +static JSGlobalContextRef create_context(JSContextGroupRef group, JSClassRef global_class) +{ + static std::mutex s_mutex; + std::lock_guard lock(s_mutex); + + // Replace JSGlobalContextCreateInGroup with its original implementation and call it. + swap_function(); + JSGlobalContextRef ctx = JSGlobalContextCreateInGroup(group, global_class); + + // Reinstall the hook. + swap_function(); + + // Clear cache from previous instances. + realm::Realm::s_global_cache.clear(); + + RJSInitializeInContext(ctx); + return ctx; +} + +static void swap_function() +{ + static int8_t s_orig_code[HOOK_SIZE]; + static bool s_swapped = false; + + int8_t *orig_func = (int8_t*)&JSGlobalContextCreateInGroup; + int8_t *new_func = (int8_t*)&create_context; + +#if __arm__ + bool orig_thumb = (uintptr_t)orig_func % 4 != 0; + if (orig_thumb) { + orig_func--; + } +#endif + + size_t page_size = sysconf(_SC_PAGESIZE); + uintptr_t page_start = (uintptr_t)orig_func & ~(page_size - 1); + uintptr_t code_end = (uintptr_t)orig_func + HOOK_SIZE; + + // Make this memory region writable. + mprotect((void*)page_start, code_end - page_start, PROT_READ | PROT_WRITE | PROT_EXEC); + + if (s_swapped) { + // Copy original code back into place. + memcpy(orig_func, s_orig_code, HOOK_SIZE); + } else { + // Store the original code before replacing it. + memcpy(s_orig_code, orig_func, HOOK_SIZE); + +#if __arm__ + if (orig_thumb) { + // LDR PC, [PC, #0]; BX PC; + memcpy(orig_func, "\x00\x4f\x38\x47", 4); + memcpy(orig_func + 4, &new_func, 4); + } else { + // LDR PC, [PC, #0]; + memcpy(orig_func, "\x00\xf0\x9f\xe5", 4); + memcpy(orig_func + 4, &new_func, 4); + } +#else + int32_t jmp_offset = (int64_t)new_func - (int64_t)orig_func - HOOK_SIZE; + + // Change original function to jump to our new one. + *orig_func = 0xE9; // JMP + *(int32_t*)(orig_func + 1) = jmp_offset; +#endif + } + + s_swapped = !s_swapped; + + // Return this region to no longer being writable. + mprotect((void*)page_start, code_end - page_start, PROT_READ | PROT_EXEC); +} diff --git a/tests/react-test-app/android/app/build.gradle b/tests/react-test-app/android/app/build.gradle index 94982d4e..ea383ba9 100644 --- a/tests/react-test-app/android/app/build.gradle +++ b/tests/react-test-app/android/app/build.gradle @@ -74,7 +74,7 @@ android { dependencies { compile 'com.android.support:appcompat-v7:23.0.1' - compile 'com.facebook.react:react-native:0.18.0-patched' + compile 'com.facebook.react:react-native:0.18.0' compile project(':realm') compile project(':react-native-fs') }