Handling C++ callbacks, Logging and exceptions with Java Native Access (JNA)

in #utopian-io7 years ago (edited)

Overview

In this tutorial we will learn logging messages in C++, throwing exceptions in C++ and handling in Java and handling C++ callbacks in Java using Java Native Access(JNA) library.

For more information on Java Native Access(JNA) library please see previous tutorials series

Requirements

  • Android Studio 3.0 or higher
  • Android NDK

Tutorial covers

  • Creating Android Studio project
  • Configuring JNA AAR library
  • Loading C++ shared library in Java
  • Logging messages in C++
  • Handling C++ exceptions
  • Synchronous callbacks
  • Asynchronous callbacks

Difficulty

  • Intermediate

Guide

1. Creating Android Studio project

Create new android project and change Application name you can also change Company domain and Package name according to requirements, select Include C++ support and click next

p1.png

Select minimum SDK version and click next

p2.png

Select Empty Activity and click next

p3.png

Change Activity Name and Layout Name according to requirements and click Next

p4.png

Select C++ Standard as Toolchain Default and click Finish to create project

p5.png

If you get NDK not configured error in Messages window then click on File menu and select Project Structure and set Android NDK location.

p6.png

2. Configuring JNA AAR library

Download jna.aar and create New Module and select Import JAR/AAR Package and click Next

p7.png

Select jna.aar file from file system and click Finish

p8.png

jna module added to project, open build.gradle file and add jna module under dependencies

dependencies {
    implementation project(':jna')
    ....
    ....
}

p9.png

3. Loading C++ shared library in Java
static {
    Native.register(MainActivity.class, "native-lib");
}

First argument to Native.register() method is the class in which we defined our native methods and 2nd argument is the name of native shared library. Name of shared library is native-lib we can change this default name in CMakeLists.txt file which is available in app folder. Loading should be done in static block which will load shared library during class loading time.

4. Logging messages in C++

Log library is already configured in our project. In order to log messages in C++ and see those messages in android studio's logcat window we can use __android_log_print() method some examples are

__android_log_print(ANDROID_LOG_DEBUG , "tag", "Debug message");
__android_log_print(ANDROID_LOG_INFO , "tag", "Info message");
__android_log_print(ANDROID_LOG_ERROR , "tag", "Error message");

First argument to __android_log_print() is log type, 2nd argument is tag it can be anything it is used to filter messages inside android studio's logcat window. 3rd argument is message string.

If you want to add some number in log message then we can use format specifier to achieve this, to log integer

int age = 100;
__android_log_print(ANDROID_LOG_DEBUG , "tag", "Your age: %d", age);

If there are many numbers to be included in log message then we can add those number to end of __android_log_print() method

    int a = 3;
    int b = 5;
    int sum = a + b;
__android_log_print(ANDROID_LOG_DEBUG , "tag", "Sum of %d and %d is : %d", a, b, sum);

As we can see in above example length of debug method becomes very large and it is difficult to read and maintain if there are lots of debug statements. We can simplify those method calls using C macros

#define LOGD(MSG) __android_log_print(ANDROID_LOG_DEBUG , "tag", MSG);
#define LOGI(MSG)   __android_log_print(ANDROID_LOG_INFO , "tag", MSG);
#define LOGE(MSG) __android_log_print(ANDROID_LOG_ERROR , "tag", MSG);

In order to use these macros we can simply write

LOGD("Log message in C++");
LOGI("Log Info in C++");
LOGE("Log error in C++");

We can also rewrite macros for integers log examples

#define LOGD1(MSG, VALUE) __android_log_print(ANDROID_LOG_DEBUG , "tag", MSG, VALUE);
#define LOGD2(MSG, VALUE1, VALUE2, SUM) __android_log_print(ANDROID_LOG_DEBUG , "tag", MSG, VALUE1, VALUE2, SUM);

In order to use these macros we can simply write

int age = 100;
LOGD1("Log integer : %d", age);

int a = 3;
int b = 5;
int sum = a + b;
LOGD2("Sum of %d and %d is : %d", a, b, sum);

Use of macros allows us to write less code and we can manage code easily and code is also readable. Open native-lib.cpp file under src/main/cpp folder and remove everything here is complete code for log example

#include <jni.h>
#include <pthread.h>
#include <unistd.h>
#include <android/log.h>

#define LOGD(MSG) __android_log_print(ANDROID_LOG_DEBUG , "System.out", MSG);
#define LOGD1(MSG, VALUE) __android_log_print(ANDROID_LOG_DEBUG , "System.out", MSG, VALUE);
#define LOGD2(MSG, VALUE1, VALUE2, SUM) __android_log_print(ANDROID_LOG_DEBUG , "System.out", MSG, VALUE1, VALUE2, SUM);

#define LOGI(MSG) __android_log_print(ANDROID_LOG_INFO , "System.out", MSG);
#define LOGE(MSG) __android_log_print(ANDROID_LOG_ERROR , "System.out", MSG);

extern "C"
void logExample() {
    LOGD("Log message in C++");

    int age = 100;
    LOGD1("Log integer : %d", age);

    int a = 3;
    int b = 5;
    int sum = a + b;
    LOGD2("Sum of %d and %d is : %d", a, b, sum);

    LOGI("Log Info in C++");
    LOGE("Log error in C++");
}

Method logExample() is marked with extern "C" to avoid name mangling. Mapping C++ logExample() method in java is similar, we need to add native keyword which tells compiler that method is implemented in native code

public native void logExample();

Calling native methods in java is same as calling other java methods here is complete code for calling logExample()

public class MainActivity extends AppCompatActivity {
    static {
        Native.register(MainActivity.class, "native-lib");
    }

    public native void logExample();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        logExample();

    }
}
Output

Output of C++ log statements are in logcat window

p10.png

5. Throwing exceptions in C++
extern "C"
void exceptionExample(JNIEnv *env, int age) {
    if (age <= 0) {
        jclass cl = env->FindClass("java/lang/Exception");
        env->ThrowNew(cl, "Age must be greater than 0");
    }
}

exceptionExample() method accepts two arguments JNIEnv and age which we will pass from java side. Using this JNIEnv pointer variable we can creates java objects, calls method on java objects, access java class fields and many more. In above example we finds Exception class and then throws it if age less than or equal to 0.

Mapping C++ exceptionExample(JNIEnv *env, int age) method in java is similar, we need to add native keyword which tells compiler that method is implemented in native code

public native void exceptionExample(JNIEnv env, int age);

Here is complete code for calling exceptionExample() we will pass negative value for age so that C++ can throw exception and then we can handle it on java side

public class MainActivity extends AppCompatActivity {
    static {
        Native.register(MainActivity.class, "native-lib");
    }

    public native void exceptionExample(JNIEnv env, int age);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        try {
            exceptionExample(JNIEnv.CURRENT, -1);
        } catch (Exception e) {
            System.out.println("C++ Exception: " + e.getMessage());
        }
    }
}
Output

Output is in logcat window, exception was thrown in C++ and caught in Java Code

p11.png

6. Synchronous callbacks

Using callbacks we can send function pointer of java method to C++ and in C++ we can do long running task and when task gets completed we can returned back the result to Java using that function pointer. Using callbacks we can do computation asynchronously without blocking the android UI thread.

In this example we will cover synchronous callback which is a blocking call, java program will waits for this C++ method call to completes and then it executes next statements.

Here is C++ example

typedef void (*Callback)(int);

extern "C"
void syncCallbackExample(Callback call) {
    //simulating long running task
    sleep(5);
    call(1000);
}

First we defined type alias for function which we will pass from java. typedef allows us to give simple names to complex function types. In syncCallbackExample() has one argument of type Callback which we defined in typedef, it is a function pointer to method which we will pass from java. In this method we added sleep() to simulate long running task and then returns 1000 value back to java method.

Mapping C++ syncCallbackExample() method in java is similar, we need to add native keyword which tells compiler that method is implemented in native code

public native void syncCallbackExample(Callback callback);

Here is complete java code for this example

public class MainActivity extends AppCompatActivity {
    static {
        Native.register(MainActivity.class, "native-lib");
    }

    public native void syncCallbackExample(Callback callback);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        System.out.println("before");
        syncCallbackExample(new Callback() {
            public void callback(int num) {
                System.out.println("Number: " + num + ", Thread: " + Thread.currentThread().getName());
            }
        });
        System.out.println("after");
    }
}
Output

As we can see from logcat output its blocking call first it prints before and then it blocks for 5 seconds which is sleep time we added in C++ and then it prints after message. Its all happening on android UI main thread which we can see in thread name in log message which is not good.

p12.png

7. Asynchronous callbacks

In this example we will make callback as non blocking call which is more efficient, it doesn't block android UI thread it will just pass the result when it gets completed on background thread.

We executed long running task in C++ thread and when it gets completed it will return back the result. On java side code remains the same as previous example. Here is example of C++ code

typedef void (*Callback)(int);

void *task(void *arg) {
    Callback call = (Callback) arg;

    //simulating long running task
    sleep(5);
    call(1000);
    pthread_exit(NULL);
}

extern "C"
void asyncCallbackExample(Callback call) {
    pthread_t t1;
    pthread_create(&t1, NULL, task, (void *) call);
}

Mapping C++ asyncCallbackExample() method in java is similar, we need to add native keyword which tells compiler that method is implemented in native code

public native void asyncCallbackExample(Callback callback);

Here is complete java code for this example

public class MainActivity extends AppCompatActivity {
    static {
        Native.register(MainActivity.class, "native-lib");
    }

    public native void asyncCallbackExample(Callback callback);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        System.out.println("before");
        asyncCallbackExample(new Callback() {
            public void callback(int num) {
                System.out.println("Number = " + num + " : " + Thread.currentThread().getName());
            }
        });
        System.out.println("after");
    }
}
Output

From logcat output the logs messages around asyncCallbackExample() executed first and then after 5 seconds we see log message of callback. In this log message we can see thread name which is Thread-5 which is background thread created in C++.

p13.png

Complete Code

C++ code
#include <iostream>
#include <jni.h>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <android/log.h>

#define LOGD(MSG) __android_log_print(ANDROID_LOG_DEBUG , "System.out", MSG);
#define LOGD1(MSG, VALUE) __android_log_print(ANDROID_LOG_DEBUG , "System.out", MSG, VALUE);
#define LOGD2(MSG, VALUE1, VALUE2, SUM) __android_log_print(ANDROID_LOG_DEBUG , "System.out", MSG, VALUE1, VALUE2, SUM);

#define LOGI(MSG) __android_log_print(ANDROID_LOG_INFO , "System.out", MSG);
#define LOGE(MSG) __android_log_print(ANDROID_LOG_ERROR , "System.out", MSG);

extern "C"
void logExample() {
    LOGD("Log message in C++");
    LOGI("Log Info in C++");
    LOGE("Log error in C++");

    int age = 100;
    LOGD1("Log integer : %d", age);

    int a = 3;
    int b = 5;
    int sum = a + b;
    LOGD2("Sum of %d and %d is : %d", a, b, sum);

}

extern "C"
void exceptionExample(JNIEnv *env, int age) {
    if (age <= 0) {
        jclass cl = env->FindClass("java/lang/Exception");
        env->ThrowNew(cl, "Age must be greater than 0");
    }
}

typedef void (*Callback)(int);

extern "C"
void syncCallbackExample(Callback call) {
    //simulating long running task
    sleep(5);
    call(1000);
}

void *task(void *arg) {
    Callback call = (Callback) arg;

    //simulating long running task
    sleep(5);
    call(1000);
    pthread_exit(NULL);
}

extern "C"
void asyncCallbackExample(Callback call) {
    pthread_t t1;
    pthread_create(&t1, NULL, task, (void *) call);
}
Java code
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

import com.sun.jna.Callback;
import com.sun.jna.JNIEnv;
import com.sun.jna.Native;

public class MainActivity extends AppCompatActivity {
    static {
        Native.register(MainActivity.class, "native-lib");
    }

    public native void logExample();

    public native void exceptionExample(JNIEnv env, int age);

    public native void syncCallbackExample(Callback callback);

    public native void asyncCallbackExample(Callback callback);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        logExample();

        try {
            exceptionExample(JNIEnv.CURRENT, -1);
        } catch (Exception e) {
            System.out.println("C++ Exception: " + e.getMessage());
        }

        System.out.println("before synCallbackExample()");
        syncCallbackExample(new Callback() {
            public void callback(int num) {
                System.out.println("Number = " + num + " : " + Thread.currentThread().getName());
            }
        });
        System.out.println("after synCallbackExample()");

        System.out.println("before asyncCallbackExample()");
        asyncCallbackExample(new Callback() {
            public void callback(int num) {
                System.out.println("Number = " + num + " : " + Thread.currentThread().getName());
            }
        });
        System.out.println("after asyncCallbackExample()");
    }
}

Github

Complete project available on github. Clone repo and open project name T4.



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Thank you for the contribution It has been approved.


Need help? Write a ticket on https://support.utopian.io.
Chat with us on Discord.

[utopian-moderator]

Hey @portugalcoin, I just gave you a tip for your hard work on moderation. Upvote this comment to support the utopian moderators and increase your future rewards!

Hey @kabooom! Thank you for the great work you've done!

We're already looking forward to your next contribution!

Fully Decentralized Rewards

We hope you will take the time to share your expertise and knowledge by rating contributions made by others on Utopian.io to help us reward the best contributions together.

Utopian Witness!

Vote for Utopian Witness! We are made of developers, system administrators, entrepreneurs, artists, content creators, thinkers. We embrace every nationality, mindset and belief.

Want to chat? Join us on Discord https://discord.me/utopian-io

Loading...