Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android React Native 64bit support #2366

Merged
merged 6 commits into from
May 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ NOTE: This release is only compatible with Realm Object Server 3.21.0 or later.

### Enhancements
* Add an optional parameter to the `SubscriptionOptions`: `inclusions` which is an array of linkingObjects properties. This tells subscriptions to include objects linked through these relationships as well (links and lists are already included by default). ([#2296](https://github.com/realm/realm-js/pull/2296)
* Support 64 bit for React Native Android. ([#2221](https://github.com/realm/realm-js/issues/2221)

### Fixed
* Making a query that compares two integer properties could cause a segmentation fault in the server or x86 node apps. ([realm-core#3253](https://github.com/realm/realm-core/issues/3253))
Expand Down
34 changes: 32 additions & 2 deletions react-native/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,37 @@ task downloadOpenSSL_x86(type: Download) {
dest new File(downloadsDir, "openssl-release-1.0.2k-Android-x86.tar.gz")
}

task downloadOpenSSL_x86_64(type: Download) {
src "https://static.realm.io/downloads/openssl/1.0.2k/Android/x86_64/openssl-release-1.0.2k-Android-x86_64.tar.gz"
onlyIfNewer true
overwrite true
dest new File(downloadsDir, "openssl-release-1.0.2k-Android-x86_64.tar.gz")
}

task prepareOpenSSL_x86(dependsOn: downloadOpenSSL_x86, type:Copy) {
from tarTree(downloadOpenSSL_x86.dest)
into "$coreDownloadDir/core"
}

task prepareOpenSSL_x86_64(dependsOn: downloadOpenSSL_x86_64, type:Copy) {
from tarTree(downloadOpenSSL_x86_64.dest)
into "$coreDownloadDir/core"
}

task downloadOpenSSL_arm(type: Download) {
src "https://static.realm.io/downloads/openssl/1.0.2k/Android/armeabi-v7a/openssl-release-1.0.2k-Android-armeabi-v7a.tar.gz"
onlyIfNewer true
overwrite true
dest new File(downloadsDir, "openssl-release-1.0.2k-Android-armeabi-v7a.tar.gz")
}

task downloadOpenSSL_arm_64(type: Download) {
src "https://static.realm.io/downloads/openssl/1.0.2k/Android/arm64-v8a/openssl-release-1.0.2k-Android-arm64-v8a.tar.gz"
onlyIfNewer true
overwrite true
dest new File(downloadsDir, "openssl-release-1.0.2k-Android-arm64-v8a.tar.gz")
}

task prepareOpenSSL_arm(dependsOn: downloadOpenSSL_arm, type:Copy) {
from tarTree(downloadOpenSSL_arm.dest)
into "$coreDownloadDir/core"
Expand All @@ -121,6 +140,15 @@ task prepareOpenSSL_arm(dependsOn: downloadOpenSSL_arm, type:Copy) {
}
}

task prepareOpenSSL_arm_64(dependsOn: downloadOpenSSL_arm_64, type:Copy) {
from tarTree(downloadOpenSSL_arm_64.dest)
into "$coreDownloadDir/core"
rename { String fileName ->
fileName.replace("-arm-", "-armeabi-")
}
}


def getDependenciesVersion(keyName) {
def inputFile = new File(buildscript.sourceFile.getParent() + "/../../dependencies.list")
def line
Expand Down Expand Up @@ -227,7 +255,7 @@ def getNdkBuildFullPath() {
return ndkBuildFullPath
}

task buildReactNdkLib(dependsOn: [downloadJSCHeaders,prepareRealmCore,prepareOpenSSL_x86,prepareOpenSSL_arm], type: Exec) {
task buildReactNdkLib(dependsOn: [downloadJSCHeaders, prepareRealmCore, prepareOpenSSL_x86, prepareOpenSSL_x86_64, prepareOpenSSL_arm, prepareOpenSSL_arm_64], type: Exec) {
inputs.files('src/main/jni')
outputs.dir("$buildDir/realm-react-ndk/all")
commandLine getNdkBuildFullPath(),
Expand Down Expand Up @@ -285,6 +313,8 @@ android {
}

task publishAndroid(dependsOn: [generateVersionClass, packageReactNdkLibs], type: Sync) {
group = 'Publishing'

// Copy task can only have one top level
into "$publishDir"

Expand All @@ -300,7 +330,7 @@ task publishAndroid(dependsOn: [generateVersionClass, packageReactNdkLibs], type
}

// copy gradle wrapper files
FileTree gradleWrapper = fileTree(projectDir).include('gradlew*').include('gradle/**')
FileTree gradleWrapper = fileTree(projectDir).include('gradlew*').include('gradle/**').include('settings.gradle')
into ('/') {
from gradleWrapper
}
Expand Down
1 change: 1 addition & 0 deletions react-native/android/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'RealmReactAndroid'
2 changes: 1 addition & 1 deletion react-native/android/src/main/jni/Application.mk
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
APP_BUILD_SCRIPT := Android.mk

APP_ABI := armeabi-v7a x86
APP_ABI := armeabi-v7a x86 x86_64 arm64-v8a
APP_PLATFORM := android-9

APP_MK_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
Expand Down
120 changes: 96 additions & 24 deletions src/android/jsc_override.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,92 @@
#include "shared_realm.hpp"
#include "impl/realm_coordinator.hpp"

#if __arm__
/**
`__attribute__((constructor))` will trigger a first call to swap_function() which will install the function hook.

The hook function is simply a technique (originally published in 1999 https://www.microsoft.com/en-us/research/project/detours/#!publications) to replace
the original function call by another one by substituting the address of the original function `JSGlobalContextCreateInGroup` by an assembly JUMP instruction in order to branch into our custom function `create_context`
(which has the same signature as the original).The custom function will then remove the hook to be able to invoke the original `JSGlobalContextCreateInGroup`
in order to obtain the JS context, needed to initialize Realm.

The assembly code to perform the jump is architecture specific, similarly for the size of this "Hook". Here's how it's calculated for the various architectures

- ARM 32 bit:
ARM supports two instruction mode, Thumb & ARM (with different size for the opcodes).
if we're using Thumb then the jump is performed using the BX instruction (see https://web.eecs.umich.edu/~prabal/teaching/eecs373-f10/readings/ARMv7-M_ARM.pdf)
BX allows to branch to a specific address stored in a global register with the option to switch instruction from Thumb to ARM. This is performed using this code
LDR R3, [PC, #0]; <---- This load the current address referenced by the current Program Counter address with a 0 offset
BX R3; <---- This will perform the jump

memcpy(orig_func, "\x00\x4b\x18\x47", 4);
memcpy(orig_func + 4, &new_func, 4);

For non-Thumb we simply set the current program PC to the address of the new function (swap_function)
LDR PC, [PC] <---- [PC] get the address of the current PC (remember the first call of swap_function is triggered automatically when loading the shared object) so when
execute this assembly later at the address of the original function, this will jump to swap_function

memcpy(orig_func, "\x00\xf0\x9f\xe5", 4);
memcpy(orig_func + 4, &new_func, 4);

- ARM 64 bit:
Doesn't have Thumb instruction, but also doesn't expose the program counter (PC) as a general register so it cannot be used anymore. The workaround is to use a `BR`
instruction (see https://static.docs.arm.com/ddi0596/a/DDI_0596_ARM_a64_instruction_set_architecture.pdf) (be careful to not use `BLR` accidentally BLR perform the same thing as BR
but as a side effect will set the PC to PC + 4 after the jump, 4 is the size of the instruction, the idea of BLR is to jump to a subroutine, then go back to the next instruction
in the assembly after the jump completes, the next instruction is located at current PC + 4, this is not our use case since we want to perform an unconditional jump).
LDR X3, .+8 <--- load into the global register X3 the address of the current PC + an offset of 8 bytes
BR X3 <---- perform the jump into the address of the new function (swap_function) located 8 bytes after the previous two assembly instructions.

memcpy(orig_func, "\x43\x00\x00\x58\x60\x00\x1F\xD6", 8);
memcpy(orig_func + 8, &new_func, 8);

How is the assembly transformed into hex code:
- You can use the manual instruction to work out the instruction value based on the opcodes.
- You can also write the assembly then cross-compile to access the hexcode after disassembling it
Example:
create a file `hook.s` with the following assembly content
```
.section .text
.global _start

_start:
ldr x3, .+8
br x3
````
you can now cross compile it to AARCH64 on mac using the NDK toolchain
$NDK_HOME/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/aarch64-linux-android/bin/as hook.s -o hook.o

you can link it using
$NDK_HOME/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/aarch64-linux-android/bin/ld hook.o -o hook

now inspect the ARM64 executable using
objdump -d hook
```
hook: file format ELF64-aarch64-little

Disassembly of section .text:
_start:
4000b0: 43 00 00 58 ldr x3, #8
4000b4: 60 00 1f d6 br x3
```
This is the method used for AMR 64bit

- You can also use this online tool to convert hex to assembly & assembly to hex http://armconverter.com


The ARM_FUNCTION_HOOK_SIZE is the number of bytes the hook need to rewrite to install the jump code.

Example for AARCH64:
It's simply two ARM64 instructions, 4 bytes each (the first memcpy is 8) and the actual function address will also be 8 bytes (second memcpy)
so the total is 16 bytes.
*/
#if __aarch64__
#define HOOK_SIZE 16
#define ARM_FUNCTION_HOOK "\x43\x00\x00\x58\x60\x00\x1F\xD6"
#define ARM_FUNCTION_HOOK_SIZE 8
#elif defined(__arm__)
#define HOOK_SIZE 8
#define ARM_FUNCTION_HOOK "\x00\xf0\x9f\xe5"
#define ARM_FUNCTION_HOOK_SIZE 4
#else
#define HOOK_SIZE 5
#endif
Expand Down Expand Up @@ -65,8 +149,9 @@ static void swap_function()
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;
bool orig_thumb = false;
#if __arm__ && !defined(__aarch64__)
orig_thumb = (uintptr_t)orig_func % 4 != 0;
if (orig_thumb) {
orig_func--;
}
Expand All @@ -86,15 +171,14 @@ static void swap_function()
// Store the original code before replacing it.
memcpy(s_orig_code, orig_func, HOOK_SIZE);

#if __arm__
#if __arm__ || __aarch64__
if (orig_thumb) {
// LDR R3, [PC, #0]; BX R3;
memcpy(orig_func, "\x00\x4b\x18\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);
memcpy(orig_func, "\x00\x4b\x18\x47", ARM_FUNCTION_HOOK_SIZE);
memcpy(orig_func + ARM_FUNCTION_HOOK_SIZE, &new_func, ARM_FUNCTION_HOOK_SIZE);
} else {
memcpy(orig_func, ARM_FUNCTION_HOOK, ARM_FUNCTION_HOOK_SIZE);
memcpy(orig_func + ARM_FUNCTION_HOOK_SIZE, &new_func, ARM_FUNCTION_HOOK_SIZE);
}
#else
int32_t jmp_offset = (int64_t)new_func - (int64_t)orig_func - HOOK_SIZE;
Expand All @@ -107,20 +191,8 @@ static void swap_function()

s_swapped = !s_swapped;

#if __arm__
// Clear ARM instruction cache.
{
register unsigned long begin __asm("a1") = (unsigned long)orig_func;
register unsigned long end __asm("a2") = (unsigned long)code_end;
register unsigned long flag __asm("a3") = 0;
register unsigned long scno __asm("r7") = 0xf0002;
__asm __volatile (
"swi 0 @ sys_cacheflush"
: "=r" (begin)
: "0" (begin), "r" (end), "r" (flag), "r" (scno)
);
};
#endif
__builtin___clear_cache((void *)page_start, (void *)(code_end - page_start));

// Return this region to no longer being writable.
mprotect((void*)page_start, code_end - page_start, PROT_READ | PROT_EXEC);
}