Compiling C code as Android shared libraries

Arik Sosman
Arik’s Blog
Published in
5 min readFeb 9, 2021

--

This guide will take you through the process of taking C files that depend on the Android JNI, compiling them for all four prevailing Android architectures, and integrating them in an Android project using Android Studio.

Let’s start with the first step, a C file containing a function we wish to expose to Android. Let’s call this file medium_example.c.

From the example above, it’s easy to tell that this function, when called, will return the string Hello from C! However, the name may cause a bit more confusion.

Firstly, all C methods exposed to the JNI need to start with Java_.

In our case, we want this method exposed in a class called NativeLibrary, which is contained in a package called medium_example whose domain name prefix is io.arik for arik.io. That means that the class’s fully resolved identifier would be

io.arik.medium_example.NativeLibrary

For naming the C functions, all periods (.) are replaced with underscores (_), and underscores are replaced with _1, hence the function name prefix

Java_io_arik_medium_1example_NativeLibrary

Lastly, we wanna name the actual function greeting, hence the greeting suffix.

Step 2: Installing the Android NDK (Native Development Kit)

To compile libraries for Android, we need the actual compilers that are capable of doing that. Thankfully, Google has pre-built binaries that let us do just that. Just head over to the NDK downloads page here, and download a package compatible with your operating system (you probably want the latest stable version). The NDK downloads page link is as follows:

https://developer.android.com/ndk/downloads

The compressed folder you downloaded, let’s call its location <NDK>, when extracted, will have the following structure:

<NDK>/toolchains/llvm/prebuilt/<host_architecture>

Assuming you have a typical Linux machine, the value of <host_architecture> will be linux-x86_64, and on Mac OS it is darwin-x86_64.

Inside that folder, there should be two folders of relevance to us: bin and sysroot. The former contains the various compiler binaries we talked about in the first section.

Step 3: Compiling the C file using the NDK’s Clang binaries

There are four prevailing Android architectures: arm64, armv7, x86, and x86_64. We need to compile the C file for each one of them. This will generate four different shared library files (extension .so), and then we will have to tell Android which is which.

To that end, let’s use a little script to simplify changes a little bit; we’ll call the file compile_medium_example.sh.

In the file above, there are two variables, highlighted in <italic>, that you, the reader, will need to adjust depending on the NDK download location and host architecture.

You will note that we are creating an output folder, tutorial_out, inside of which we are auto-generating folder names for each of the generated shared library files, which are all named libmediumexample.so. Once you run the script, your folder structure should look something like this:

Step 4: Putting it all together with Android Studio

We have our libraries compiled, so let’s create an Android module to integrate them. The starting point for this is that you already have an existing project. In my case, that project’s name is “AndroidLab” because I’ve been doing some other experimenting with it.

The first thing we wanna do is create a new module, and we’re gonna pick the module type “Android Library.” As discussed in Step 1, we have decided that we’re naming this module medium_example, with the package name, in my case, therefore being io.arik.medium_example.

Also, as discussed in Step 1, we will create a Java class inside our library called NativeLibrary, and we add the following code:

You will note the loadLibrary call, whose argument is simply mediumexample. The JNI automatically prepends the lib prefix and the .so suffix, so the file name we have for the shared libraries, libmediumexample.so, will work automatically.

However, while we do have Java expecting the library, it doesn’t actually know where to look yet. To that end, go to the location of the Android module (right-click in Android Studio’s project explorer view, and click “Reveal in Finder” or your operating system’s equivalent). Then, navigate to the subdirectory src/main, and create a folder named jniLibs. Copy the folders inside tutorial_out from Step 3 to the jniLibs folder you just created, such that the module’s folder structure looks as follows:

Step 5: Let’s try it out!

Finally, let’s put everything we just built to the test. There are a couple things that need to be done to verify that everything works. First of all, the library module we just created needs to be declared as a dependency of the main app that is typically run in the simulator.

To do that, open project structure settings in Android Studio (⌘ + ; on a Mac), go to the Dependencies tab, highlight the main app, press the + button, and pick Module Dependency.

Then, in one of the app’s files, call NativeLibrary.greeting() and print it to the console or add a debugger breakpoint. In my case, I have an application with a tab bar, so in ui.home.HomeViewModel.kt, I added the following lines (in bold):

package io.arik.android_lab.ui.homeimport androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

import io.arik.medium_example.NativeLibrary;
class HomeViewModel : ViewModel() { private val _text = MutableLiveData<String>().apply {
val nativeGreeting = NativeLibrary.greeting();
value = "Native response: " + nativeGreeting

}
val text: LiveData<String> = _text
}

The result speaks for itself:

I hope this walkthrough was helpful. Thanks for reading, and good luck!

--

--