Let's Build Chuck Norris! - Part 7: Android and JNA

dmerejkowsky

Dimitri Merejkowsky

Posted on June 18, 2018

Let's Build Chuck Norris! - Part 7: Android and JNA

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

Enter fullscreen mode Exit fullscreen mode

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'
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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"));
    }
}
Enter fullscreen mode Exit fullscreen mode
/* In ChuckNorris.java */

public class ChuckNorris {
    String getFact() {
        return "";
    }
}
Enter fullscreen mode Exit fullscreen mode

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 ""

Enter fullscreen mode Exit fullscreen mode

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'
}

Enter fullscreen mode Exit fullscreen mode

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. (just chuck_norris_init for now).
  • We call chuck_norris_init in the constructor of our ChuckNorris class, storing the result into a JNA Pointer:
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 "";
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
  ...

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 ""

Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

Re-run the tests:

$ ./gradlew test
BUILD SUCCESSFUL

Enter fullscreen mode Exit fullscreen mode

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);
  });
Enter fullscreen mode Exit fullscreen mode

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!

Chuck Norris punching the screen

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

Enter fullscreen mode Exit fullscreen mode

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'
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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 .

Enter fullscreen mode Exit fullscreen mode

Let’s try again:

Chuck Norris app running

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.


  1. This is also known as wishful thinking programming 

  2. As always, the Wikipedia page contains lots of interesting stuff about this topic. 

  3. You can find a note about this in JNA’s FAQ, but as far as I know, not anywhere else in the documentation. 

💖 💪 🙅 🚩
dmerejkowsky
Dimitri Merejkowsky

Posted on June 18, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related