Sharing native C code for iOS and Android (NDK)

Sharing native C code for iOS and Android (NDK)

This article describes project setup and preparation required to use native C code that can be used on both platforms, iOS and Android. Special detail is given to Android Studio configuration including NDK, file structure and command line tool usage.

Download

Completed code sample can be downloaded here:

xCode

Project setup for iOS part actually does not involve any effort - C files can be complied directly without any changes. It is slightly more complex with C++ files - in that case all files importing C code must have .mm extension which is not desirable in large scale projects (i will cover packaging a library into CocoaTouch framework to be used in any project in next article). This section only covers using C code.

1. First let's create a simple C API (ex. Core.h / Core.c) to call test method returning library version.

const char* version()
{
    return "1.0";
}

2. Add files to xCode and import as any other file.

#import "Core.h"

3. Use methods as if they were static (C does not support class instances). Good practice is to create special wrapper class for calling native code (in this case method: version()).

const char *tmp = version();
return [NSString stringWithUTF8String:tmp];

Done! Now let's take a look at Android implementation...

Android Studio

This description is based on Android Studio 2.3.1 running on OSX (Sierra).

1. Before we can start integrating C code, we first need to download NDK. Open SDK manager and make sure NDK is installed. Tools -> Android -> SDK Manager.

2. Setup external tools:

While on the same window (SDK Manager), select Tools dropdown from list on the left, then go for External Tools. In bottom area of the screen you'll find a plus sign to add command line scripts. Configure as below:

  • javah
javah
$JDKPath$/bin/javah
-classpath "$Classpath$" -v -jni $FileClass$
$SourcepathEntry$/../jni
  • ndk-build
ndk-build
/Users/ /*YOUR_UESR NAME*/ /Library/Android/sdk/ndk-bundle/ndk-build 
$ProjectFileDir$/app/src/main

3. Switch from Android view to Project view on left top corner

Create new directory at:

app -> src -> main -> jni

Add three new files in 'jni' directory:

  • Android.mk
  • Application.mk
  • CLibrary.cpp

Open app -> src -> main and copy-paste Shared C folder (next to jni folder).


You should have something like this:








4. Next we have to create android wrapper class to call native C methods. Create new java file (in this case 'CLibraryWrapper.java' in app main package. This class has two responsibilites, 1) contains API for native library and 2) loads static C library that we will compile shortly.

public class CLibraryWrapper
{
     public native String version();

     static
     {
         System.loadLibrary("CLibrary");
     }
}
  • Before we can continue, we have to configure gradle scripts to allow to compile the project with C files. Switch back from Project view to Android view. Expand 'Gradle Scripts' dropdown -> build.gradle (module app)

add to defaultConfig:

ndk {
    moduleName = "CLibrary"
}

- add to android:

sourceSets {
    main {
        jni.srcDirs = []
        jniLibs.srcDir 'src/main/libs'
    }
}

Now we have to create a link between native C files and Java - a bridge. Interface that allows us to do this is JNI (Java Native Interface). First build project (Build -> Rebuild Project). Then switch back to 'Project' view, right click on newly created CLibrrayWrapper.java file, select ExternalTools-> javah.

As a result a new file should have been created:

/Users/pawelklapuch/Documents/Articles/NativeCSharing/SampleUsingNativeCInAndroid/app/src/main/jni/com_example_pawelklapuch_sampleusingnativecinandroid_CLibraryWrapper.h

If you look at the content of the file - there is only one line of code that is really of interest to us:

JNIEXPORT jstring JNICALL Java_com_example_pawelklapuch_sampleusingnativecinandroid_CLibraryWrapper_version
  (JNIEnv *, jobject);

This is an interface that will bridge between java method and native method. Now we have a header, we still need to provide implementation. Copy this header method and copy-paste it to CLibrary.cpp file (this is the place where native C code meets java code). If you look closely at the method signature, you will notice it only describes data types, but does not declare actual parameters. Let's quickly define these:

JNIEXPORT jstring JNICALL Java_com_example_pawelklapuch_sampleusingnativecinandroid_CLibraryWrapper_version
  (JNIEnv *env, jobject obj)

Also we have to include the header (javah generated file) and C header file:

#include "com_example_pawelklapuch_sampleusingnativecinandroid_CLibraryWrapper.h"
#include "Core.h"

Final method implementation should look like this:

JNIEXPORT jstring JNICALL Java_com_example_pawelklapuch_sampleusingnativecinandroid_CLibraryWrapper_version
  (JNIEnv *env, jobject obj)
  {
        return env->NewStringUTF(version());
  }

Last thing to do is to configure NDK build script. For that we have to go back to 2x other files we had created before.

  • Android.mk
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := CLibrary
LOCAL_SRC_FILES := CLibrary.cpp

LOCAL_SRC_FILES += ../Shared/Core.c
LOCAL_C_INCLUDES := ../Shared/Core.h

include $(BUILD_SHARED_LIBRARY)
  • Application.mk
APP_MODULES := CLibrary
APP_ABI := all

Difficult part is now done. Now we can build the library with NDK. Right click on 'jni' folder and choose external tool -> ndk-build. This should produce the following output.

/Users/pawelklapuch/Library/Android/sdk/ndk-bundle/ndk-build
[arm64-v8a] Compile++      : CLibrary <= CLibrary.cpp
[arm64-v8a] Compile        : CLibrary <= Core.c
[arm64-v8a] StaticLibrary  : libstdc++.a
[arm64-v8a] SharedLibrary  : libCLibrary.so
[arm64-v8a] Install        : libCLibrary.so => libs/arm64-v8a/libCLibrary.so
[x86_64] Compile++      : CLibrary <= CLibrary.cpp
[x86_64] Compile        : CLibrary <= Core.c
[x86_64] StaticLibrary  : libstdc++.a
[x86_64] SharedLibrary  : libCLibrary.so
[x86_64] Install        : libCLibrary.so => libs/x86_64/libCLibrary.so
[mips64] Compile++      : CLibrary <= CLibrary.cpp
[mips64] Compile        : CLibrary <= Core.c
[mips64] StaticLibrary  : libstdc++.a
[mips64] SharedLibrary  : libCLibrary.so
[mips64] Install        : libCLibrary.so => libs/mips64/libCLibrary.so
[armeabi-v7a] Compile++ thumb: CLibrary <= CLibrary.cpp
[armeabi-v7a] Compile thumb  : CLibrary <= Core.c
[armeabi-v7a] StaticLibrary  : libstdc++.a
[armeabi-v7a] SharedLibrary  : libCLibrary.so
[armeabi-v7a] Install        : libCLibrary.so => libs/armeabi-v7a/libCLibrary.so
[armeabi] Compile++ thumb: CLibrary <= CLibrary.cpp
[armeabi] Compile thumb  : CLibrary <= Core.c
[armeabi] StaticLibrary  : libstdc++.a
[armeabi] SharedLibrary  : libCLibrary.so
[armeabi] Install        : libCLibrary.so => libs/armeabi/libCLibrary.so
[x86] Compile++      : CLibrary <= CLibrary.cpp
[x86] Compile        : CLibrary <= Core.c
[x86] StaticLibrary  : libstdc++.a
[x86] SharedLibrary  : libCLibrary.so
[x86] Install        : libCLibrary.so => libs/x86/libCLibrary.so
[mips] Compile++      : CLibrary <= CLibrary.cpp
[mips] Compile        : CLibrary <= Core.c
[mips] StaticLibrary  : libstdc++.a
[mips] SharedLibrary  : libCLibrary.so
[mips] Install        : libCLibrary.so => libs/mips/libCLibrary.so

5. Now we can simply use the wrapper class in Android app.

CLibraryWrapper library = new CLibraryWrapper();
Log.d("OUTPUT", library.version());

This should produce the following output:

05-26 03:19:18.279 2604-2604/com.example.pawelklapuch.sampleusingnativecinandroid D/OUTPUT: 1.0

Above setup is downloadable at the top of the page. Feel free to comment / ask questions! Article on passing complex data structures / arrays back and forth / callbacks from native code to ObjC and Java to follow shortly!

To view or add a comment, sign in

More articles by Pawel Klapuch

  • Xcode 12.6 (Beta) / Carthage

    Quick look at the the Xcode beta - first thing that strikes - Carthage fails to build any of my dependencies (..

Others also viewed

Explore content categories