TDD the C++ Layer of an Android app

@giorgionatili

$ whoami

  • Engineering manager at Amazon
  • Organizer of Droidcon Boston
  • Organizer of SwiftFest Boston
  • Meetups and community enthusiast

@giorgionatili

Agenda

  • Native code in Android
  • The JNI wrapper
  • Testing and architecture boundaries
  • Flavors of testing the native layer

Disclaimer

  • My opinions are my own
  • I intensively use animated gifs :)

Be advised...

Warmup

Java or Kotlin

I would say Kotlin :p 

  • It's null-safe
  • It's concise and elegant
  • It's shipped with a powerful standard library
  • It works with functional and/or OOP programming
  • It supports lambdas and high-order functions
  • It supports delegation out of the box 
  • It supports asynchronous programming

Interoperable 

C++ looks scary

Complexity

  • Compilation time and configuration
  • Multiple CPU architectures
  • Pointers, memory leaks, and other cool stuff :)

Additional Layer

Hello C++

Why

  • The native code is compiled to binary code (faster)
  • A lot of multimedia libraries are written in C++
  • You can write cross platform features
  • The amount of existing code in C++ is unbelievable

How

  • Using the Android NDK
  • Using the Java Native Interface (JNI)

What is the Android NDK

  • It's a toolset to use C and C++ with Android
  • Provides access to physical devices components
  • It is a tool to debug and test the C++ layer

What is the JNI

  • It is an interfaces between Java and other components written with other programming languages
  • It is a bidirectional facade between native and and platform code

Android Studio

Options

  • Exceptions Support, -fexceptions flag to cppFlags in the module-level build.gradle file
  • Runtime Type Information Support, enables code reflection features in C++ 

Simplified

If you are using Android Studio and need to integrate native libraries in your app, you may have had to use some complex methods before, involving maven and .aar/.jar packages… the good news is you don’t need these anymore 

Gradle and C++

Configuration

externalNativeBuild {
            cmake {
                cppFlags "-std=c++11"
                arguments '-DANDROID_STL=c++_static'
            }
        }

externalNativeBuild {
        cmake {
            path 'src/main/cpp/CMakeLists.txt'
        }
    }

CMake

What is CMake

  • A tool to control and manage the compilation process
  • It's platform independent and therefore reusable

What should I know?

(KISS)

Add files to the  library

add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/includes/HelloMario.cpp
             src/main/cpp/native-lib.cpp )

Find a library

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

Finding Where?

Compiling your C/C++ source code from Android Studio

Use cmake

(You already know how to do it)

Use ndk-build

  • Application.mk
    • Platform and toolchain information
  • Android.mk
    • Target platform
    • Module name
    • Which c++ files to include

JNI Wrapper

JNI General Tips

  • Minimize data marshaling  
  • Avoid direct asynchronous communication (use multiple JVM threads)
  • Keep the JNI interface small and modular

JNIEnv (it's a pointer)

  • It exposes most of the JNI functions  
  • It's one of the arguments received by the native functions
  • It cannot be shared between threads

Show me the code!

class MainActivity : AppCompatActivity() {

    companion object {
        fun loadLibrary() {
           System.loadLibrary("native-lib")
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        loadLibrary()
    }
}

Load the library

class NativeWrapper {

    external fun sayHelloToJNI(): String
    external fun sayHelloFromMario(name: String): String
}

Expose the methods

extern "C" JNIEXPORT jstring
JNICALL
Java_io_a2xe_experiments_simplejnitest_NativeWrapper_sayHelloFromMario(
                                                                        JNIEnv *env, 
                                                                        jclass type,
                                                                        jstring name) {

    const char *toSayHello = env->GetStringUTFChars(name, 0);
    env->ReleaseStringUTFChars(name, toSayHello);

    return env->NewStringUTF(HelloMario::greet(toSayHello).c_str());
}

Define a method

Naming Convention

  • The prefix Java_
  • A mangled fully-qualified class name
  • An underscore (_) separator
  • A mangled method name
  • For overloaded native methods, two underscores (__) followed by the mangled argument signature

Use the method

val wrapper = NativeWrapper()

sample_text.text = wrapper.sayHelloFromMario("Mary")

Straightforward

TDD 

TDD Cycles

Three Principles

  • No code should be written before a failing test
  • Write just enough code to make the test pass
  • Refactor the code and implement the business logic described in the test

Specific Tests

  • Add more specific tests
  • Decouple the code to support the specificity of the test
  • Repeat until you don't get blocked 

Tests and Architecture

  • Pause to consider if the architecture is evolving or not
  • Make the changes to improve the architecture
  • Refactor to keep your tests green

 Architectural Boundaries

Android

JNI

Native code

Scenarios

  • A JNI wrapper that uses native code
  • JNI wrapper that includes multiple libraries
  • Complex Android and C++ integration

JNI Wrapper

A single wrapper

  • Limited in scope
  • Limited in complexity

Write a simple test

@Test
fun useTheCLuke() {

    val wrapper = NativeWrapper()
    assertNotEquals("whatever", wrapper.sayHelloFromMario("Mary"))
}

Bummer!!!

/home/giorgio/apps/android-studio/jre/bin/java -...
com.intellij.rt.execution.application.AppMainV2 com.intellij.rt.execution.junit.JUnitStarter 
-ideVersion5 io.a2xe.experiments.simplejnitest.ExampleUnitTest,injectTheCLuke

java.lang.UnsatisfiedLinkError: io.a2xe.experiments.simplejnitest.NativeWrapper
.sayHelloToJNI()Ljava/lang/String;

	at io.a2xe.experiments.simplejnitest.NativeWrapper.sayHelloToJNI(Native Method)
	at io.a2xe.experiments.simplejnitest.ExampleUnitTest.injectTheCLuke(ExampleUnitTest.kt:22)

Process finished with exit code 255

Alternatives

  • Conditional execution of the code
  • Mocking the native interface

Using an interface

interface NativeSayHello {
    fun sayHelloFromMario(name: String): String
}
class NativeWrapperMock : NativeSayHello {
    override fun sayHelloFromMario(name: String): String {
        return "Hello $name!"
    }
}

Move loading in the wrapper

class NativeWrapper : NativeSayHello {

    init {
        System.loadLibrary("native-lib")
    }

    external override fun sayHelloFromMario(name: String): String
}

Run-time conditional

class NativeWrapperSolver : NativeSayHello {

    override fun sayHelloFromMario(name: String): String {
        return implementation.sayHelloFromMario(name)
    }

    companion object {

        var implementation: NativeSayHello = try {
            NativeWrapper()
        } catch (e: Throwable) {
            NativeWrapperMock()
        }
    }
}

Test are green

WRONG!

Instruments Tests

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {

    @Test
    fun itShouldContainTheNameOfThePersonToSayHello() {

        val wrapper = NativeWrapper()
        assertTrue(wrapper.sayHelloFromMario("Mary")
            .contains("Mary"))
    }
}

Test the wrapper

Quite there...

It's slow!!!

It's terribly slow!

Alternatives

  • Mock the JNI layer and run unit tests
  • Don't write unit test

Mock the JNI layer

No Unit Tests around the C++ layer

Robolectric

In a nutshell

  • It is a framework that allows you to write unit tests and run them on a desktop JVM
  • It enables you to run your Android tests in your continuous integration environment

The advantages

  • It provides an abstraction layer of the Android API
  • It's executed in memory and it's fast
  • It provides a JVM compliant version of android.jar 

The gotchas

  • It's executed on your local machine (not on a device!) 
  • It mocks types that are not owned by the app
  • Only some portions of Roboletric use the Android code
  • The test environment could be error prone
  • Native libraries don't work out of the box

Android NDK

Copy the libraries

The Application class

open class SimpleJNITestApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        initializeNativeWrappers()
    }

    open fun initializeNativeWrappers() {
        NativeLibsManager()
    }
}

The Roboletric Application

class RobolectricApplication : SimpleJNITestApplication() {

    init {
        ShadowLog.stream = System.out //Android logcat output.
    }

    override fun initializeNativeWrappers() {
        // Load external libraries
    }
}

Loading the libs

 val libsBasePath = File(File("").absolutePath + "/src/test/libs")
    .absolutePath
 var os = System.getProperty("os.name")
 os = if (!TextUtils.isEmpty(os)) os else ""
 
val soFileList = arrayListOf<File>()
  if (os.contains("Linux")) {
     val linuxSysSoBasePath = "$libsBasePath/arch_x86-64/"
     soFileList.addAll(addLibs(linuxSysSoBasePath))
}

 for (soFie in soFileList) {
       System.load(soFie.absolutePath)
}

Test the native code integration

@Test
fun `it should contain some cheering text`() {

    val textView = mainActivity
        .findViewById<TextView>(R.id.sample_text)
    val helloMessage = textView.text.toString()
        
    assert.that(helloMessage, startsWith("Hello"))
}

Way better!

But there is more...

Googletest

In a nutshell

  • It's a testing library built for C++
  • It can be used for unit, integration and acceptance tests (you can also assign a size to the tests!)
  • It's open source and used by projects like Chromium, OpenCV, llvm, and so on

A simple test

#include "gtest/gtest.h"
#include "includes/HelloMario.h"

TEST(SalutationTestCase, test_hello_with_name) {
    EXPECT_EQ(string("Hello World!"), 
                          HelloMario::greet("World"));
}

More dependencies?

fakeIt!

Here's fakeIt

#include <fakeit.hpp>
using namespace fakeit;

struct SomeInterface {
   virtual int foo(int) = 0;
   virtual int bar(int,int) = 0;
};

Mock<SomeInterface> mock;
// Stub a method to return a value once
When(Method(mock,foo)).Return(1);

Load the tests

$ANDROID_NDK_ROOT/ndk-build -C $NDK_PROJECT_PATH APP_BUILD_SCRIPT
=Android.mk NDK_APPLICATION_MK=Application_test.mk NDK_LIBS_OUT
=../../../libs V=1 $1 > /dev/null

adb push libs/x86/module_under_test.so /data/local/tmp/
adb push libs/x86/your_unit_test_module /data/local/tmp/
adb shell chmod 775 /data/local/tmp/your_unit_test_module

Run the tests

 adb shell "LD_LIBRARY_PATH=/data/local/tmp 
            /data/local/tmp/your_unit_test_module"

Grab the reports

https://github.com/harshvs/android-gtest-driver

Done!

Wrap-up

Not an easy process

Reusable

Powerful

Q&A

The fart app

Thanks!

@giorgionatili

TDD the C++ Layer of an Android app

By Giorgio Natili

TDD the C++ Layer of an Android app

  • 804