Let's Build Chuck Norris! - Part 7: Android and JNA
Dimitri Merejkowsky
Posted on June 18, 2018
Originally published on my blog.
Note: This is part 7 of the Let’s Build Chuck Norris! series.
Last time we managed to cross-compile and run C++ code for Android.
It’s now time to write some Java code, but we need to take a detour on the desktop first.
Java bindings
Let’s create a new Java library project with gradle
:
$ cd chucknorris
$ mkdir java && cd java
$ gradle init --type java-library
Gradle created a bunch of files, but for now we just care about the sources and the tests.
Our goal is to write a test that demonstrates we can indeed get some ChuckNorris facts.
Let’s start by removing cruft from the generated build.gradle
:
dependencies {
- api 'org.apache.commons:commons-math3:3.6.1'
- implementation 'com.google.guava:guava:23.0'
}
Then let’s fix the source files that gradle created so that we have proper package:
$ tree java
├── build.gradle
├── gradle
└── src
├── main
│ └── java
│ └── com
│ └── chucknorris
│ └── ChuckNorris.java
└── test
└── java
└── com
└── chucknorris
└── ChuckNorrisTest.java
Now we can write a failing test:
/* In ChuckNorrisTest.java */
public class ChuckNorrisTest {
@Test
public void testGetFact() {
ChuckNorris ck = new ChuckNorris();
String fact = ck.getFact();
assertThat(fact, containsString("Chuck Norris"));
}
}
/* In ChuckNorris.java */
public class ChuckNorris {
String getFact() {
return "";
}
}
Let’s run the tests:
$ ./gradlew test
> Task :test FAILED
com.chucknorris.ChuckNorrisTest > testGetFact FAILED
java.lang.AssertionError at ChuckNorrisTest.java:14
1 test completed, 1 failed
# open build/reports/tests/test/index.html
java.lang.AssertionError:
Expected: a string containing "Chuck Norris"
but: was ""
OK, this fails for the good reason.
Now we can try and load our shared library using JNA.
First, we add the dependency in the build.gradle
file:
dependencies {
+ api 'net.java.dev.jna:jna:4.5.1'
testImplementation 'junit:junit:4.12'
}
Then we’re ready to use JNA:
- We use
Native.loadLibrary()
to load the shared library - We create a
CLibrary
interface that implements the C functions we want to call as methods. (justchuck_norris_init
for now). - We call
chuck_norris_init
in the constructor of our ChuckNorris class, storing the result into a JNAPointer
:
public class ChuckNorris {
private Pointer ckPointer;
private static CLibrary loadChuckNorrisLibrary() {
return (CLibrary) Native.loadLibrary("chucknorris", CLibrary.class);
}
public interface CLibrary extends Library {
CLibrary INSTANCE = loadChuckNorrisLibrary();
void chuck_norris_init();
}
public ChuckNorris() {
ckPointer = CLibrary.INSTANCE.chuck_norris_init();
}
public String getFact() {
return "";
}
}
And we run the tests:
$ ./gradlew test
java.lang.UnsatisfiedLinkError: Unable to load library 'chucknorris':
Native library (linux-x86-64/libchucknorris.so)
not found in resource path (...)
at com.sun.jna.NativeLibrary.loadLibrary(NativeLibrary.java:303)
at com.sun.jna.NativeLibrary.getInstance(NativeLibrary.java:427)
...
This is expected. We never told JNA where the libchucknorris.so
file is.
As a reminder, the file currently lives in the build/default
folder. Here’s how we built it:
$ cd build/default
$ conan install ../..
$ cmake -GNinja -DBUILD_SHARED_LIBS=ON ../..
$ ninja
There are several ways to tell JNA about the location of the shared library file. Here we’ll set a system property in the test
block of the Gradle script:
def thisFile = new File(project.file('build.gradle').absolutePath)
def projectPath = thisFile.getParentFile()
def topPath = projectPath.getParentFile()
def cppPath = new File(topPath, "cpp")
def cppBuildPath = new File(cppPath, "build/default/lib")
test {
systemProperty 'jna.library.path', cppBuildPath
}
Now if we re-run the tests we get back our first failure:
$ ./gradlew test
java.lang.AssertionError:
Expected: a string containing "Chuck Norris"
but: was ""
But we did manage to instantiate the ChuckNorris
class, so this is progress :)
Let’s implement getFact()
, and while we’re at it, add a .close()
method:
public interface CLibrary extends Library {
// ...
void chuck_norris_init();
String chuck_norris_get_fact(Pointer pointer);
void chuck_norris_deinit(Pointer pointer);
}
public ChuckNorris() {
ckPointer = CLibrary.INSTANCE.chuck_norris_init();
}
public String getFact() {
return CLibrary.INSTANCE.chuck_norris_get_fact(ckPointer);
}
public void close() {
CLibrary.INSTANCE.chuck_norris_deinit(ckPointer);
}
}
Re-run the tests:
$ ./gradlew test
BUILD SUCCESSFUL
Success!
Creating a new Android project
Now we know:
- How to compile the C++ code for Android.
- How to load some C++ in Java, but only for the desktop.
It’s time to glue things together.
To do so, the best thing is to use Android Studio to create the gradle project, starting with a with a basic activity so we don’t have to deal with all the Android boilerplate.
Adapting the GUI
Let’s pretend the ChuckNorris class already exists for now1.
We start by adding a text_view
ID for the text view in the content_main
layout.
Then we adapt the MainActivity.java
file to update the text view when clicking on the floating button action:
public class MainActivity extends AppCompatActivity {
private ChuckNorris chuckNorris;
@Override
protected void onCreate(Bundle savedInstanceState) {
chuckNorris = new ChuckNorris();
super.onCreate(savedInstanceState);
}
@Override
protected void onDestroy() {
chuckNorris.close();
super.onDestroy();
}
// ...
final TextView textView = (TextView) findViewById(R.id.text_view);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String fact = chuckNorris.getFact();
textView.setText(fact);
});
Adding ChuckNorris sources
One of Java’s slogan is “Write Once, Run Everywhere”2
So let’s:
- Add JNA in the dependencies
- Add the ChuckNorris.java file we wrote earlier
And everything should work, right?
Fun with jnidispatch.so
To check if our code works, let’s create an emulator, and click on play.
We’re faced with:
ChuckNorris has stopped
Open App Again
What? Chuck Norris can’t be stopped, this is unacceptable!
Time to look at the logs:
06-18 14:27:18.553 6890-6890/info.dmerej.chucknorris E/AndroidRuntime:
FATAL EXCEPTION: main
Process: info.dmerej.chucknorris, PID: 6890
java.lang.UnsatisfiedLinkError:
Native library (com/sun/jna/android-x86-64/libjnidispatch.so)
not found in resource path (.)
at com.sun.jna.Native.loadNativeDispatchLibraryFromClasspath
at com.sun.jna.Native.loadNativeDispatchLibrary
...
at info.dmerej.chucknorris.ChuckNorris.loadChuckNorrisLibrary
That’s a fun one. Turns out the name of dependency changes when compiling for Android, you need a @aar
prefix3:
dependencies {
// ...
implementation 'net.java.dev.jna:jna:4.5.1@aar'
}
Let’s try again!
We get the same error message, but this time it’s the chucknorris.so
library that is not found:
06-18 14:27:18.553 6890-6890/info.dmerej.chucknorris E/AndroidRuntime:
FATAL EXCEPTION: main
Process: info.dmerej.chucknorris, PID: 6890
java.lang.UnsatisfiedLinkError: Unable to load library 'chucknorris':
Native library (android-x86-64/libchucknorris.so) not found
Fortunately, there’s a more or less standard solution.
If you put a .so
file in a folder named src/main/jniLibs/<arch>
, it will be included inside the Java application, and the Java code will be able to load it without any configuration.
The shared option
For simplicity purposes, we built the ChuckNorris library as a static library, just to show that the C++ binary still needed libc++_shared.so
to run. But JNA needs a shared library to run.
Remember in part 4 we had to call CMake with -DBUILD_SHARED_LIBS=ON
to get a shared library.
We’ll do the same thing, but going through Conan this time.
First, let’s add the ChuckNorris:shared
option in the android
profile:
...
[options]
*:pic=True
ChuckNorris:shared=True
Then adapt the recipe:
class ChucknorrisConan(ConanFile):
name = "ChuckNorris"
...
options = {"shared": [True, False]}
default_options = "shared=False"
def build(self):
cmake = CMake(self)
cmake_definitions = {}
if self.options.shared:
cmake_definitions["BUILD_SHARED_LIBS"] = "ON"
cmake.configure(defs=cmake_definitions)
def package(self):
self.copy("lib/libchucknorris.so", dst="lib", keep_path=False)
self.copy("lib/libc++_shared.so", dst="lib", keep_path=False)
Then let’s re-create the Conan package:
$ conan create . dmerej/test --profile android --setting arch=x86_64
Exporting package recipe
...
package(): Copied 2 '.so' files: libchucknorris.so, libc++_shared.so
Package '<hash>' created
Finally let’s create symlinks to all .so
files from the package.
$ cd android/app
$ cd src/main
$ mkdir -p jniLibs/x86_64
$ cd jniLibs/x86_64
$ ln -s ~/.conan/data/ChuckNorris/0.1/dmerej/test/<hash>/libchucknorris.so .
$ ln -s ~/.conan/data/ChuckNorris/0.1/dmerej/test/<hash>/libc++_shared.so .
Let’s try again:
Victory \o/
That’s all for today. See you next time!
Thanks for reading this far :)
I'd love to hear what you have to say, so please feel free to leave a comment below, or read the feedback page for more ways to get in touch with me.
-
This is also known as wishful thinking programming ↩
-
As always, the Wikipedia page contains lots of interesting stuff about this topic. ↩
-
You can find a note about this in JNA’s FAQ, but as far as I know, not anywhere else in the documentation. ↩
Posted on June 18, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.