React Native with JNI & C
React Native has quickly become our preferred way to write mobile apps. The ability to write code once and have it run well on both Android and iOS really helps development. But what about when we need to write platform specific, low level code? React Native lets us do that too! Via its Java bridge and JNI, we can run C code on Android (iOS will be covered in a later article) and get the results back in JavaScript.
Motivation
Before doing this, make sure it's actually something you need to do. We chose to do this early on in the planning phase for BallisticsARC. The app relies on the JBM Ballistics Library, all of which is written in C. If you're into competitive shooting or just interested in bullet trajectories and ballistics, you've likely heard of JBM. For the many who haven't, it's a library that takes a bunch of parameters about current atmosphere, rifle, and bullet characteristics and calculates how much a target shooter must compensate for bullet drop and deviation from wind. We had two options in integrating this into our React Native app, rewrite all of it in JavaScript, or dispatch the already written C code from JavaScript. We chose the latter.
If you're trying to work with a big C library, using JNI on Android is probably your best bet. If you just want to run some C code for performance or because you're more comfortable writing C, we would heavily advise against doing so. Getting to C from JavaScript is not computationally cheap. So unless you're doing enormous computations in C (why are you doing this on a phone?), you're going to lose any C performance boost with the time it takes to pass data around. However, you may have some tasks that just have to be done in C, in which case your only choice is to run C code. So now that you've figured out why you're executing C, let's get started.
Setup
This article assumes that you are at least familiar with React Native, Java, and C and have a working development environment for React Native on Android set up, as explained in the React Native docs. In addition to having RN set up, you'll also need to get the Android NDK (Native Development Kit). While we have not had an issue using later versions, React Native only supports version 10e, which you may download here.
- Mac OS X - http://dl.google.com/android/repository/android-ndk-r10e-darwin-x86_64.zip
- Linux 64-bit - http://dl.google.com/android/repository/android-ndk-r10e-linux-x86_64.zip
- Windows 64-bit - http://dl.google.com/android/repository/android-ndk-r10e-windows-x86_64.zip
- Windows 32-bit - http://dl.google.com/android/repository/android-ndk-r10e-windows-x86.zip
In the event RN changes what version of the NDK it relies on, links should be available on their Building from Source page.
Once you've downloaded and unzipped the NDK, you need to either set the environment variable ANDROID_NDK
to point to the directory it was unzipped to (the one containing the toolchains
directory), or add the line ndk.dir=<directory>
(replacing <directory>
with your NDK's directory) to android/local.properties
(create it if it doesn't exist) in your React Native project.
JavaScript to Java
The first step in running C code from React Native is to run Java code from React Native. If you need further explanation or details on the JS-Java bridge beyond this article, check out React Native's guide to Native Modules on Android.
We're going to start simple and write our own Hello World. In the Java src directory of your React Native project android/app/src/main/java/com/thebhwgroup/demo
(replacing com/thebhwgroup/demo
with your package), create a new file called HelloWorldModule.java
.
//HelloWorldModule.java
package com.thebhwgroup.demo; //replace com.thebhwgroup.demo with the package name in MainApplication.java
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
public class HelloWorldModule extends ReactContextBaseJavaModule {
public HelloWorldModule(ReactApplicationContext reactContext) {
super(reactContext); //required by React Native
}
@Override
public String getName() {
return "HelloWorld"; //HelloWorld is how this module will be referred to from React Native
}
@ReactMethod
public void helloWorld(Promise promise) { //this method will be called from JS by React Native
promise.resolve("Hello World!");
}
}
As explained by the comments, the getName()
method is used by React Native to identify this module, it will later be referenced as React.NativeModules.HelloWorld
in JavaScript. As for helloWorld(Promise)
, there are a couple ways to return data from Java to JavaScript. We prefer to use promises as it generally results in easier to read code than callbacks. Promise.resolve()
is used on a successful result, Promise.reject()
is used in the event of an error or exception. Details can be found in React Native docs.
Before this module can be registered with React Native, it first must be added to a package. Create another file in the same directory as HelloWorldModule.java
, and name it MyReactPackage.java
.
//MyReactPackage.java
package com.thebhwgroup.demo; //replace com.thebhwgroup.demo with the package name in MainApplication.java
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class MyReactPackage implements ReactPackage {
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new HelloWorldModule(reactContext)); //this is where we register our module, and any others we may later add
return modules;
}
}
createJSModules()
and createViewManagers()
are used for other native code aspects in React Native, but have to be implemented here. createNativeModules()
is where HelloWorldModule
is registered along with any more modules that are created later. Now this package can be registered with React Native. Open MainApplication.java
and find the getPackages()
method.
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
Add your package to the list so that it looks like so
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new MyReactPackage()
);
}
That's it for Java! (for now) Back to where you've put your JavaScript code, make a new file HelloWorld.js
.
//HelloWorld.js
'use strict'
import { NativeModules } from 'react-native';
module.exports = NativeModules.HelloWorld;
This file is now ready for import. If you wanted to change NativeModules.HelloWorld
to something else, you would change the string returned from getName()
in HelloWorldModule.java
. Now, in a file where you want to use this, add the following.
import HelloWorld from './HelloWorld'; //HelloWorld.js file we just made
...
async helloWorld() {
try {
let helloWorldStr = await HelloWorld.helloWorld();
console.log(helloWorldStr);
} catch (e) {
console.error(e);
}
}
You can put this helloWorld()
function anywhere you would put another function, like inside the component in which it will be used. Also if you're using lambda functions, the first line can instead be written as helloWorld = async () => {
Note that since we're using promises (and not callbacks) HelloWorld.helloWorld()
must either be called in an async function with await, or called in a normal function with the resulting variable calling Promise.then()
or Promise.all()
. If this is a new syntax to you there are a couple guides overviewing async/await, and MDN has documentation on Promises.
Add a call to this function somewhere in your code and run your app on an Android emulator or phone. You should see Hello World!
printed out in the Android logcat (run adb logcat -s ReactNativeJS
to filter only React Native logs).
Now that we're successfully calling Java code from JavaScript, it's time to bring our Hello World down to C.
Java to C
C
Calling C code from Java is enabled with the Java Native Interface (JNI). To start, make a new directory in your project at android/app/src/main/jni
and add the following files to it.
Android.mk
LOCAL_SRC_FILES := hello_world.c
LOCAL_MODULE := hello_world_jni
Application.mk
APP_ABI := all
hello_world.c
//hello_world.c
#include <jni.h>
jstring Java_com_thebhwgroup_demo_HelloWorldModule_helloWorldJNI(JNIEnv* env, jobject thiz) {
return (*env)->NewStringUTF(env, "Hello World!");
}
Android.mk
tells Android what source files to compile, for now only hello_world.c
, and what to name our module, hello_world_jni
. Application.mk
instructs Android to build against all supported architectures. For more explanation on these, see the Android documentation for Android.mk and Application.mk.
hello_world.c
might look a little odd at first, but this is how JNI looks. C functions called by JNI return JNI types, an explanation of which can be found in the JNI Spec. Generally, putting "j" before a C/Java type will get you the JNI type, and in some cases they're nothing more than a typedef. A jdouble
for example is defined by typedef double jdouble;
The name of the function is very specific, it must start with Java, followed by the package name, the class, and the function name within the class. So there is a Java function, in our com.thebhwgroup.demo package, in the HelloWorldModule class, called helloWorldJNI. All functions called via JNI must take a JNIEnv*
and jobject
as their first two parameters, the JNIEnv*
is a "pointer to a structure storing all JNI function pointers" and is how JNI functions are called in C.
Our C function itself is simple, it's just calling a JNI function to make a jstring
that can be converted to a java.lang.String
by JNI. Note that it is both called from env
and passed env
as its first parameter, every JNI function is called this way. Details on JNI functions can also be found in the JNI Spec.
Java
This module now needs to be loaded and called from Java code. Open the app build.gradle
file, found at android/app/build.gradle
. Under the ndk
section, add the line moduleName "hello_world_jni"
(as named in Android.mk) along with the ldLibs
and cFlags
lines so that it looks like so.
ndk {
abiFilters "armeabi-v7a", "x86"
moduleName "hello_world_jni"
ldLibs "log"
cFlags "-std=c99"
}
This instructs gradle to build and include the module with your app. The ldLibs
line links the Android logging library allowing us to make logcat calls from C (something we'll do later), and the cFlags
enables C99 features. Next open HelloWorldModule.java
and add the following static
block right after the class definition.
public class HelloWorldModule extends ReactContextBaseJavaModule {
static {
System.loadLibrary("hello_world_jni"); //this loads the library when the class is loaded
}
This is a static initializer, it loads the JNI module only once when the class is first loaded, not every time an instance is made. Then, change the helloWorld()
function as shown and add the new helloWorldJNI()
declaration below it.
@ReactMethod
public void helloWorld(Promise promise) {
try {
String hello = helloWorldJNI();
promise.resolve(hello);
} catch (Exception e) {
promise.reject("ERR", e);
}
}
public native String helloWorldJNI();
We've now changed the helloWorld()
function to get a string by calling helloWorldJNI()
. This references the declaration below it, public native String helloWorldJNI()
. The native
keyword indicates to Java that this is a JNI function, and it will find the function in C, not Java. Note that if you're using Android Studio, it will warn you if it can't find an appropriately named function in C (Java_com_thebhwgroup_demo_HelloWorldModule_helloWorldJNI
in our case) and show you what the name of the function it's looking for is.
At this point, you should be able to recompile and run your app, and it will again log "Hello World!" into the Android logcat. You shouldn't really notice any difference as it's still just getting a string and logging it in React Native, but this time it's getting it from C, not Java.
Real World JNI
Now that you've got a very basic, working example of JNI, it's time to write some practical code. Chances are your C function is going to take more than 0 parameters and return more than just a string.
The C library we were writing code for had quite a few structs we would have to represent in JavaScript. The most complicated of which had both other structs and an array of structs as some of its members. The solution we came up with does require a decent chunk of boilerplate and somewhat repetitive code at first, but it makes things easier later on.
Java to C and Back Again
We initially wrote our code by breaking up JavaScript objects into their respective members in Java and passing every member in as its own parameter to JNI functions. Not only is this incredibly tedious and messy, it straight up doesn't work for arrays. We then instead chose to make a Java class that was a counterpart to the C struct that we were working with. So let's say a C library has the following structs.
//defs.h
#ifndef DEFS_H
#define DEFS_H
typedef struct
{
double x;
double y;
} MyVector, *pMyVector;
typedef struct
{
int vector_count;
MyVector* vectors;
MyVector position;
} MyStruct, *pMyStruct;
#endif
This struct is much simpler than the JBM library structs we were using, but covers the unique cases we came across.
Given this, you now want to make analogous classes in Java. Add the following files to the same directory as HelloWorldModule.java
and the other class files.
//MyVector.java
package com.thebhwgroup.demo;
public class MyVector {
double x;
double y;
public MyVector() {}
public MyVector(double x, double y) {
this.x = x;
this.y = y;
}
}
//MyStruct.java
package com.thebhwgroup.demo;
public class MyStruct {
int vector_count;
MyVector[] vectors;
MyVector pos;
public MyStruct() {}
public MyStruct(int vector_count, MyVector[] vectors, MyVector pos) {
this.vector_count = vector_count;
this.vectors = vectors;
this.pos = pos;
}
}
These Java classes can be passed as is to the native functions and will be cast to jobject
in C. For an example, let's define another native function in HelloWorldModule.java
. At the bottom of the class definition, add the line public native MyStruct helloStructJNI(MyStruct struct, double d);
. We're passing both a Java class and primitive to give examples of how you handle both complex and primitive types in JNI.
The equivalent C function will be defined as jobject Java_com_thebhwgroup_demo_HelloWorldModule_helloStructJNI(JNIEnv* env, jobject thiz, jobject jstruct, jdouble jd)
. The first 2 parameters are the JNIEnv
and jobject
that are part of every JNI invoked function, the third parameter is our MyStruct
(now a jobject
), and the fourth is the double
(now a jdouble
, which as mentioned earlier is still just a double
).
Since the jdouble
is just a typedef, it can be used as is and passed to any C functions that need it. However, a C MyStruct
needs to be made out of the jobject
before it can be used with any C function expecting a MyStruct
. This is done via a couple JNI functions, which we've set up and added to hello_world.c
. Since there are a lot of steps in this, we've gone and finished the hello_world.c file, which you can view on GitHub.
There's a lot of new stuff going on here, so let's go through and explain the details of the less obvious parts. First we import Android logging functions, the JNI header, stdlib for malloc, and our defs.h
file defining MyVector
and MyStruct
. Next we set up some helpful #defines for Android logging. Calling LOGD()
later in code will have the same behavior as calling Log.d()
in Java and the log with jni_hello_world
Lines 13 to 26 declare a couple JNI types. These variables are used by JNI to associate a jobject
with a Java class and work with the data it holds. For each struct there is a jclass
, used to refer to the Java class, a jmethodID
which refers to the constructor method of the class, and a couple jfieldID
s which are used to refer to the fields of the Java class. Finally, there is a jclass
which will be set up to refer to a generic Java exception in the event we need to throw one.
JNI_OnLoad
All of these variables are setup in the JNI_OnLoad()
function. JNI_OnLoad
is called when the library is loaded (by our System.loadLibrary()
call made in Java), and is expected to return the version needed by the library. Android docs says to use JNI_VERSION_1_6
. We've commented the setup calls for MyStruct
in the loading function as they're more complicated than the setup calls for MyVector
, and if you understand these you'll understand the easier ones. The first one sets jclass MY_STRUCT
. It makes a call to FindClass()
passing it env
(all JNI functions need this), and the fully qualified name of MyStruct
in Java. The fully qualified name of a class is the package name (com.thebhwgroup.demo
in our case) followed by the class name (MyStruct
), with slashes replacing periods. So while we've named our Java class MyStruct to make its C pairing clear, you could name it whatever you want as long as you refer to it correctly. Finally the return from FindClass
is wrapped in a call to NewGlobalRef()
(also passing env
again). This is done as the reference returned from FindClass()
is only local and will only persist through this function call. We however will need to use MY_VECTOR
continuously in other function calls, so we hold onto it with a global reference.
Next we get the jmethodID
of the MyStruct(int vector_count, MyVector[] vectors, MyVector position)
constructor. In C, both jmethodID
and jfieldID
are pointers to opaque structures. In the call to GetMethodID()
, we pass the following parameters
env
as is required to all JNI functionsMY_STRUCT
which now refers to theMyStruct
Java class and is sort of tellingGetMethodID()
where to "look"- the function name, which since it's a constructor is always
"<init>"
- the type signature of the constructor method,
"(I[Lcom/thebhwgroup/demo/MyVector;Lcom/thebhwgroup/demo/MyVector;)V"
The type signature is probably the oddest part of this call. There are two ways to get this, write it yourself, or use javap
to get it for you, the latter of which is less error prone.
javap
requires that you have built the APK at least once with the Java class you need to get the signature of. If you've done so, you should be able to run javap -classpath android/app/build/intermediates/classes/debug -s com.thebhwgroup.demo.MyStruct
from your React Native project directory. If you're running from somewhere else, replace the -classpath
argument with the equivalent relative directory, and the -s
argument with the name of your package and class in the event they are different. Assuming this works, you should see some output in the console. You're most interested in the following lines.
public com.thebhwgroup.demo.MyStruct(int, com.thebhwgroup.demo.MyVector[], com.thebhwgroup.demo.MyVector);
descriptor: (I[Lcom/thebhwgroup/demo/MyVector;Lcom/thebhwgroup/demo/MyVector;)V
Specifically the descriptor:
line. Everything after the colon and leading space is the JNI method signature for the constructor method you want. In the event you can't get this to work, or are more interested in the signature, we'll also go over what exactly it means.
The MyStruct
constructor takes an int
, an array of MyVector
, and a lone MyVector
as its three parameters and returns void
. The return value is the last part of the signature and is the V
at the end of the string. The part of the string in parentheses is the signature of the parameters. The signature of a primitive type is easy, just a single letter, and in the case of an int
it's the letter I
. The signature for an array of a Java class is a little more. It's a [
indicating an array and then the letter L
followed by the fully qualified name of the Java class terminated with a semicolon. So in all, [Lcom/thebhwgroup/demo/MyVector;
is the signature of an array of MyVector
in the package com.thebhwgroup.demo
. The signature of the final method parameter is the same as the previous one without the [
as it's not an array, Lcom/thebhwgroup/demo/MyVector;
. And there you have it, the signature of a method with its parameters and return type. This is explained more in the Type Signatures section of the JNI Types spec.
Now that we have the jmethodID
, we'll get the jfieldID
s for the class fields. We first get the simplest field in MyStruct
, int vector_count
. GetFieldID()
takes parameters similar to GetMethodID()
, taking a field name instead of a method name and a field type signature instead of a method type signature. The field name here is "vector_count"
and the type signature is just "I"
since it's an int. Next we get the jfieldID
s for vectors
and position
. You should recognize these type signatures as they were also used in getting the constructor method ID.
Finally, we get the class for a Java exception, which can later be used to throw an exception.
Just so you're aware, you don't have to do it like this. You could instead make calls to GetMethodID()
and GetFieldID()
anytime you need the methods and fields. If you're only using them once, it's probably better to do just that. When we were writing BallisticsARC we knew we were going to have to use these a couple times, so we went ahead and saved it all to variables for two reasons, cleaner code and because these lookups are slow. FindClass()
for example, "searches the directories and zip files specified by the CLASSPATH environment variable for the class with the specified name." IBM benchmarked GetFieldID()
and found that in six calls to GetIntField()
, using a cached field ID instead of calling GetFieldID()
every time was 24 times faster. So in this space-time tradeoff, a little more space saves a lot more time.
Also, be aware that every one of these JNI calls could be silently creating an exception. In general you should watch for these with if ((*env)->ExceptionCheck(env))
, we however have chosen not to. The code we needed to run in C was so integral to our app, that if it failed to initialize our app might as well crash as there is no saving it at that point. However, if an ungraceful crash is not your intention, you should be handling exceptions.
Helper Functions
Now that all the needed JNI variables are initializaed it's time to put them to use. For each C struct there is a make_native_...
function which will take a jobject
and return a C struct that can then be passed to third party library functions. Just as before we're only going to walk through the more complicated of the two under the assumption that if you understand it you'll understand the simpler one. make_native_my_struct()
takes a JNIEnv
and a jobject
that represents a Java MyStruct
. First we declare a C MyStruct
which will be set with all the data from the Java one. Next we set the value for vector_count
which is pretty easy as it's just an int. We make a call to GetIntField()
and pass it the following
env
, the JNIEnv pointer as alwaysjstruct
, the jobject it is getting the int fromMY_STRUCT_VECTOR_COUNT
, the jfieldID of the int field, which was set earlier inJNI_OnLoad
There are similar functions for other primitive types. If you check make_native_my_vector
, you'll notice it's simply setting the x
and y
fields with two calls to GetDoubleField()
with the corresponding jfieldID
.
Next we set position
, which is a little more complicated since it's a struct. However, we've made this step easier by using make_native_my_vector()
, the process of which should make sense if you understood the use of GetIntField()
. We use GetObjectField()
to get another jobject
from our first jobject jstruct
. The call to this is just like GetIntField()
except for its return. We pass it
env
jstruct
, thejobject
we're getting the field fromMY_STRUCT_POSITION
, thejfieldID
forposition
that was set inJNI_OnLoad
We then immediately pass the resulting jobject
to make_native_my_vector()
(along with env
), to get a MyVector
that can be assigned to position
.
The final field, vectors
, is the most complicated one as it's an array of Java classes. First the array in the C struct needs to be initialized with malloc()
using my_struct.vector_count
as the size. Next we get the jobject
field from jstruct
, only this time we assign it to a jobjectArray
. We then iterate through the array, setting each element of vectors
by getting a jobject
from the array and passing it to make_native_my_vector()
. GetObjectArrayElement()
works similar to GetObjectField()
, but instead of taking a field ID, it takes an index in the array. Remember when doing this to call DeleteLocalRef()
after you're done with the jobject
. Each call to GetObjectArrayElement()
makes a local reference and the JNI implementation only guarantees 16 of those per function call. You could find yourself in some trouble if you never delete those references.
With these functions a jobject
passed in from JNI can now be turned into a corresponding C struct that can then be used like any other struct. But more functions are still needed to make a jobject
out of a C struct so that it can be returned to JNI.
Making a jobject
from the struct MyVector
is actually a single line since it only consists of primitive types. However we still chose to make a function (make_jni_my_vector
) for it for consistency and because it's cleaner and easier to remember than a NewObject()
call. This also becomes more useful the more fields a struct has. Making a jobject
from a MyStruct
(done in make_jni_my_struct
) is made more complicated by the array of structs. First a new jobjectArray
is allocated; we pass NewObjectArray
the following
env
vector_count
, the size of the arrayMY_VECTOR
, the object type of the arrayNULL
, an element used for initialization
Since MyVector
s are immediately going to be copied into the array, it's initialized with NULL
for now. Next the array elements are copied into the jobjectArray
. We iterate through every MyVector
in vectors
, and for each one call SetObjectArrayElement()
, passing the following
env
ja_vectors
, the destinationjobjectArray
we're copying intoi
, the index in the array that we're setting- the data to be set at the index, which we get by making a call to
make_jni_my_vector()
, which returns ajobject
of the vector
We've made an assumption in our code that if you're making a jobject
out of a struct you're done with the struct, so at that point we call free()
on the array previously allocated with malloc()
to return that memory. Last, we create a jobject
of MyStrcut
with a call to NewObject()
and return it. To NewObject()
we pass
env
MY_STRUCT
the jobject "type" were makingMY_STRUCT_CONS
a reference to the Java method used to construct the classmy_struct.vector_count
, the first parameter to theMY_STRUCT_CONS
constructor, which is passed as is since JNI can handle the primitive conversionja_vectors
thejobjectArray
which will get converted to a[]MyVector
by JNI and passed as the second parameter to the constructor- the third paramater to the constructor, a
jobject
ofposition
which is made withmy_struct.position
passed tomake_jni_my_vector
And that's it, all needed helper/utility functions are now finished and ready for use. If you look at Java_com_thebhwgroup_demo_HelloWorldModule_helloWorldJNI()
(the C function for the native helloWorldJNI()
method in HelloWorldModule
), you'll see why we set all this up. Instead of having all those JNI calls in our function, there's just the single line MyStruct my_struct = make_native_my_struct(env, jstruct);
. This struct can be used in a couple calls to LOGD to log its contents to the Android logcat. We've commented out what a call to create an exception would look like (as that's not intended right now), but if one of these calls could fail, you would want to check it and call ThrowNew()
(passing it the JAVA_EXCEPTION
jclass) to allow Java to deal with the failure. Our function finishes with a call to make_jni_my_struct
to return the MyStruct
back to Java as a jobject
.
Clearly this function doesn't actually do anything yet. But using the convenience functions for making structs and jobjects as needed, it's much easier to prepare data for manipulation in C. When writing BallisticsARC, we went through the JBM library and found all entry points we would be calling. We then wrote wrappers to move data to and from Java and C for all of the needed JBM functions. This made the make_jni...
and make_native...
functions very useful to us as we had multiple Java classes with 10+ fields, and 5-6 C function that we'd need to call with them. With this setup we were able to easily make JBM C structs from Java classes and pass them as needed to JBM functions. By doing so we didn't have to look at or change any JBM code to make it able to interact with all the JNI types we had.
It's time to to actually test our code. Back in Java, add the following snippet to the try block in the helloWorld()
function that gets called by React Native.
MyVector[] vectors = {new MyVector(1, 1), new MyVector(2, 2), new MyVector(3, 3)};
MyStruct myStruct = new MyStruct(
vectors.length,
vectors,
new MyVector(10, 15));
MyStruct newStruct = helloStructJNI(myStruct, 47);
(There's no reason to any of the chosen numbers) Rebuild, run your app, and check the Android logs (adb logcat -s jni_hello_world
to filter only the JNI logs, assuming you have #define LOG_TAG "jni_hello_world"
in hello_world.c). If everything is going well you should see the following in the logs.
jni_hello_world: double 47.000000
jni_hello_world: struct vector_count 3
jni_hello_world: struct vectors
jni_hello_world: vector 0 (1.000000, 1.000000)
jni_hello_world: vector 1 (2.000000, 2.000000)
jni_hello_world: vector 2 (3.000000, 3.000000)
jni_hello_world: struct pos (10.000000, 15.000000)
If not, either you aren't calling helloWorldJNI()
correctly or your app has probably just crashed. If it has crashed, go through the Android logs (you'll need to run adb logcat
again if you ran it with the -s
flag previously) and look for JNI DETECTED ERROR IN APPLICATION
(it'll likely be right before an enormous stack trace). Chances are one of the many JNI calls you're making failed, made an exception, and you made another call afterwards without checking for an exception. Our most common error was a class, method, or field not being found with the fully qualified name and type signature we had given. You'll probably see something along the lines of JNI DETECTED ERROR IN APPLICATION: JNI <method> called with pending exception <exception>
. Look at what the exception is, and hopefully it's a clear enough description to fix. If it is JNI being unable to find a method, class, or field as you've described it, double check your fully qualified name and type signature, and try comparing it to javap
output as was mentioned before. A note for when you rebuild, we occasionally ran into an issue where it looked like Android Studio didn't notice C code changes, meaning we would have to clean the project, uninstall the APK, and then rebuild and run. This is generally accompanied with No local changes, not deploying APK
in the Android Studio logs.
JavaScript to Java and Back Again
Our React Native code is now calling C code, but no data is being passed to and from it. Since we have Java<->C data conversion set up for our types, we just need to make a JavaScript<->Java conversion. This process is easier than Java to C as React Native handles the JavaScript side of conversion. Conversion of primitive types is straight forward. For example, a number in JS can be an int, double, or float in Java, and vice versa. Java classes and arrays are a little more complicated. React Native added two Java classes, ReadableMap and ReadableArray, both of which also have writable versions. The ReadableMap is just that, a map. It has methods to check for the existence of a key, along with getting a value from a key. The ReadableArray has the same thing, but instead of getting values from keys, they're gotten by indices. Setting up the conversion to and from JavaScript in Java requires the addition of two new methods to MyVector and three new methods to MyStruct, the complete versions of which are linked below.
Just like with the JNI section, we'll explain the MyStruct conversion process and assume the MyVector conversion is understandable from that. The first new addition to MyStruct.java is the constructor MyStruct(ReadableMap map)
. Since a ReadableMap is passed to Java from React Native, it needs to be converted a MyStruct so that it can freely be used in Java. This constructor simply calls the original constructor that takes an int
, MyVector[]
, and a MyVector
. Getting the int from the map is done via a call to getInt
with the key for vector_count
. Note that these keys map to the object's properties in JavaScript, so if the properties of your JavaScript object are not the same as your Java class variable names you'll need to put whatever the property is in JavaScript for the key. Next we get a ReadableArray from the map with getArray()
and pass this array to another helper function we've made, which will be explained shortly. Last we get a ReadableMap for position
from map
and pass it to the MyVector constructor that takes a ReadableMap, the function of which should now be apparent based on the function of this MyStruct
constructor.
Onto MyVectorArrayFromReadableArray()
at the bottom. It starts by allocating a new MyVector[] based on the size of the ReadableArray. It then iterates through the ReadableArray and passes each ReadableMap to the associated MyVector constructor. Pretty simple.
The third function is used to get a WritableMap from MyStruct, that way React Native can convert it to a JavaScript object. We start by creating a new WritableMap. We then set the key "vector_count"
to be the int vector_count
. Next we create a new WritableArray for vectors
. We iterate through vectors
, convert each one to its own WritableMap, and push the map to the WritableArray. Last we convert position
to a WritableMap and set it for the key "position"
in map
. And with that we have a WritableMap that's ready to be given to React Native.
To test this, change the helloWorld function in HelloWorldModule.java to accept a new parameter instead of using its own MyStruct
.
HelloWorldModule.java
@ReactMethod
public void helloWorld(ReadableMap structMap, double num, Promise promise) {
try {
MyStruct myStruct = new MyStruct(structMap);
MyStruct newStruct = helloStructJNI(myStruct, num);
promise.resolve(newStruct.toWritableMap());
} catch (Exception e) {
promise.reject("ERR", e);
}
}
This function now expects an object and a number from its invocation in JavaScript. Note that the Promise
used to return data to JavaScript is always the last parameter to the Java function. The new MyStruct returned from helloStructJNI()
is converted to a WritableMap
and used to resolve the promise, which returns the data to JavaScript. To test this, the call to helloWorld()
in React Native has to be changed.
let struct = {
vector_count: 3,
vectors: [
{
x: 10,
y: 10,
},
{
x: 20,
y: 20,
},
{
x: 30,
y: 30,
},
],
position: {
x: 12,
y: 34,
}
};
let helloWorldStr = await HelloWorld.helloWorld(struct, -3.14);
console.log(helloWorldStr);
Just make an object, pass it and a number to helloWorld.helloWorld()
, and log it. Now rebuild and run the app and check the logs (run adb logcat -s ReactNativeJS,jni_hello_world
to filter only React Native console.log()
and C LOGD()
calls). If all goes well you should see the following in the logs.
jni_hello_world: double -3.140000
jni_hello_world: struct vector_count 3
jni_hello_world: struct vectors
jni_hello_world: vector 0 (10.000000, 10.000000)
jni_hello_world: vector 1 (20.000000, 20.000000)
jni_hello_world: vector 2 (30.000000, 30.000000)
jni_hello_world: struct pos (12.000000, 34.000000)
ReactNativeJS: { position: { y: 34, x: 12 },
ReactNativeJS: vectors: [ { y: 10, x: 10 }, { y: 20, x: 20 }, { y: 30, x: 30 } ],
ReactNativeJS: vector_count: 3 }
And there you have it! You're finally passing data from JavaScript down to C through Java and getting it back up in JavaScript. Go ahead and try actually manipulating it in C. Maybe double the vector values, or add another MyVector to the array (make sure to increment vector_count
). You should see changes you make reflected in the returned JavaScript object.
Wrap Up
Hopefully this article has given you enough of a base to use C, JNI, Java, and React Native with your own libraries and needs. This was very new ground for us when we started as none of us had ever used JNI before. We're glad we were able to run third party C library code from our React Native app. You'll probably run into situations different than the ones we've set up, so remember the JNI Functions and JNI Types specs, along with React Native's Native Modules guide and their source code. The Android docs also have a couple extra tips for writing JNI, along with sample projects of their own. We consulted all of these and more when writing BallisticsARC, and we hope we've saved you some time and effort bringing it to one place.
What's next? You're probably writing your app in React Native so that you can also run it on iOS, but everything done here is Android specific. We'll soon be covering how to do the same thing and run C code from React Native on iOS. Lucky for you it's a lot easier. Objective C is a superset of C meaning you can take the NSDictionary
React Native passes you and convert it directly to a C struct for your C libraries, skipping the JNI step.