Pull to refresh

Android interop with SWIG (a guide). From simple to weird. Part 2 — weird

Reading time26 min
Views2.3K

This is Part 2. Part 1 is here.

Part 2 covers the use of typemaps, complex & weird cases, and debugging suggestions. This part requires an understanding of Part 1 - usage of SWIG in simple cases.

This is a practical guide, with Android Studio project you can play with.

Table of contents

Complex cases

This section covers the typemaps.

Shared pointer to std::string (C++ side typemaps)

Scheme

%shared_ptr(std::string);

just doesn't seem to be working ¯\_(ツ)_/¯ - the generated code will not compile. Because SWIG, when generating a wrapper for shared_ptr arguments, expects that they are SWIG-wrapped types and relies on SWIG-generated functions, which are not part of the java.lang.String class that we expect to use on the Java side. We have to create our own typemap to allow std::shared_ptr<std::string>. Typemap instructs SWIG on how to process a particular type. I will not dig into deep details, see Java typemaps chapter, especially C++ typemaps and Director-specific typemaps within the chapter.

Look to the following table for a simpler understanding of the typemaps (description is below the table).

Type in level

Type 1

Type 2

Type 3

Typemaps between levels

1

Java

String

Date

byte[][]

%typemap(jstype)

%typemap(javain↓|javaout↑|javadirectorin↑|javadirectorout↓]

2

Java intermediary

String

long

byte[][]

%typemap(jtype)

Automatically done by JNI (no typemap)

3

JNI (C++ intermediary)

jstring

jlong

jobjectArray

%typemap(jni)

%typemap(in↓|out↑|directorin↑|directorout↓)

4

C++

std::shared_ptr<std::string>

time_t (equals) long long

shared_ptr<vector<shared_ptr<vector<byte>>>>

To pass anything between C++ and JAVA, back or forth, you need to go through the four levels. Levels 1 and 4 are Java and C++ - the levels you are writing business logic at. To go from 1 to 4 or back you have to pass 3 levels of conversion. 2->3 and back is done by JNI. But 1->2 and 3->4 and back are done by the typemaps. Some typemaps are defining types to operate on, in the table, they are residing on the lines with numbers (levels). Some typemaps are defining level and direction, at which they will convert type (between lines with numbers); arrow explains in which direction the particular typemap is working. Note that regular and director typemaps directions are flipped, i.e. %typemap(in) doing the same operation as %typemap(directorout). For example, %typemap(in) std::shared_ptr<std::string> is converting level 3->4; level 4 C++ type is defined in this typemap; level 3 JNI type jstring is defined by another typemap: %typemap(jni) std::shared_ptr<std::string> "jstring". The table also covered the Date and byte[][] type, we will talk about them in the later chapters.

It might sound complicated, but you will eventually get how everything is working while practicing. Read the comments in the following snippet, they are explaining everything in place. So, to proceed, crate new file swig/std_shared_ptr_to_string.i and fill it with content.

%include <std_shared_ptr.i>
%include <std_string.i>

%{
#include <memory>
#include <string>
%}

namespace std
{
    // To properly recognize type defined in our Types.h
    %template(shared_ptr_to_string) std::shared_ptr<std::string>;
}

%{
namespace
{
    std::shared_ptr<std::string> convertStringFromJavaToCpp(JNIEnv *jenv, jstring javaString)
    {
        const char * psz_string = jenv->GetStringUTFChars(javaString, NULL);
        if (!psz_string)
        {
            return nullptr;
        }

        std::shared_ptr<std::string> cppString = std::make_shared<std::string>(psz_string);
        jenv->ReleaseStringUTFChars(javaString, psz_string);
        return cppString;
    }

    jstring convertStringFromCppToJava(JNIEnv *jenv, std::shared_ptr<std::string> cppString)
    {
        if (!cppString)
        {
            return nullptr;
        }

        return jenv->NewStringUTF(cppString->c_str());
    }
}
%}

//
// Type definitions for C type 'std::shared_ptr<std::string>'
//

%typemap(jstype) std::shared_ptr<std::string> "java.lang.String" // Java type
%typemap(jtype) std::shared_ptr<std::string> "java.lang.String" // Java intermediary type
%typemap(jni) std::shared_ptr<std::string> "jstring" // JNI C type (or intermediary C type)

//
// Java-side typemaps
//

// Conversion from 'jstype' (Java type) to 'jtype' (Java intermediary type)
%typemap(javain) std::shared_ptr<std::string> "$javainput"

// Conversion from 'jtype' (Java intermediary type) to 'jstype' (Java type)
%typemap(javaout) std::shared_ptr<std::string>
{
     return $jnicall;
}

// Conversion from 'jstype' (Java type) to 'jtype' (Java intermediary type) for director methods
%typemap(javadirectorout) std::shared_ptr<std::string> "$javacall"

// Conversion from 'jtype' (Java intermediary type) to 'jstype' (Java type) for director methods
%typemap(javadirectorin) std::shared_ptr<std::string> "$jniinput"

//
// C++ side typemaps
//

// Conversion from 'jni' (JNI C type) to C++ type
%typemap(in) std::shared_ptr<std::string>
{
    $1 = convertStringFromJavaToCpp(jenv, $input);
}

// Conversion from C++ type to 'jni' (JNI C type)
%typemap(out) std::shared_ptr<std::string>
{
    return convertStringFromCppToJava(jenv, $1);
}

// Conversion from 'jni' (JNI C type) to C++ type for director methods
%typemap(directorout) std::shared_ptr<std::string>
{
    $result = convertStringFromJavaToCpp(jenv, $input);
}

// Conversion from C++ type to 'jni' (JNI C type) for director methods
// Note: Ljava/lang/String; is the JNI field descriptor for the Java String type
%typemap(directorin,descriptor="Ljava/lang/String;") std::shared_ptr<std::string>
{
    $input = convertStringFromCppToJava(jenv, $1);
}

Include this file into your SWIG interface file:

%module(directors="1") SwigAndroidGuide

%include <std_shared_ptr.i>
%include <std_string.i>
%include "std_shared_ptr_to_string.i" // << Add this line

Re-run SWIG script run_swig.sh and you will be able to run your application.

Note that I've created separate conversion functions, which are used in the typemaps. This allows:

  • Writing clear C++ code; note, you have to use $input and similar constructions in typemap body;

  • You know what exactly C++ function is doing by giving it a proper name;

  • Typemaps are looking clearer (and you know what they are doing by reading C++ function names);

  • Re-use these functions, as regular and director typemaps are the same in most of the cases;

  • Decrease in code size. Each time you use the argument particular typemap is working with, the typemap code will be copy-pasted into the SWIG-generated file. This increases code size, which is not great for mobile devices.

Java Date and C++ time_t (Java-side typemaps)

For java.util.Date it is much simpler to write such typemaps on the Java side, to avoid unnecessary JNI calls in C++. See Table 1 to see how we map Date type to C++. In C++ it will be the time_t, which is long long.

Let's add the creation date to our message, in Message.h:

class Message
{
public:
    // Add these lines
    time_t getCreationDate() const { return _creationDate; }
    void setCreationDate(time_t creationDate) { _creationDate = creationDate; }
private:

    time_t _creationDate; // < Add this line

If you run SWIG script run_swig.sh now, you will notice the new file:

/app/src/main/java/com/goldberg/swigandroidguide/swiggenerated/SWIGTYPE_p_time_t.java

and new functions in WSIG-generated Message.java:

public SWIGTYPE_p_time_t getCreationDate() {
  return new SWIGTYPE_p_time_t(SwigAndroidGuideJNI.Message_getCreationDate(swigCPtr, this), true);
}

public void setCreationDate(SWIGTYPE_p_time_t creationDate) {
  SwigAndroidGuideJNI.Message_setCreationDate(swigCPtr, this, SWIGTYPE_p_time_t.getCPtr(creationDate));
}

This means, as you guess, that the type time_t can't be handled properly.

Let's add new SWIG file swig/time_t.i. We do only assignments for the C++ portion of the typemaps. But java typemaps are including the actual Java code. Note, I didn't create separate function as I did in C++, because code is very simple. One more reason - one Java file is generated for each SWIG-wrapped class, so it is just harder to do than in C++, where everything is in one large *.cpp file.

//
// Type definitions for C type 'time_t'
//

%typemap(jstype) time_t "java.util.Date" // Java type
%typemap(jtype) time_t "long" // Java intermediary type
%typemap(jni) time_t "jlong" // JNI C type (or intermediary C type)

//
// Java-side typemaps
//

// Conversion from 'jstype' (Java type) to 'jtype' (Java intermediary type)
%typemap(javain) time_t "$javainput.getTime() / 1000"

// Conversion from 'jtype' (Java intermediary type) to 'jstype' (Java type)
%typemap(javaout) time_t
{
    return new java.util.Date($jnicall * 1000);
}

// Conversion from 'jstype' (Java type) to 'jtype' (Java intermediary type) for director methods
%typemap(javadirectorin) time_t "new java.util.Date($jniinput * 1000)"

// Conversion from 'jtype' (Java intermediary type) to 'jstype' (Java type) for director methods
%typemap(javadirectorout) time_t "$javacall.getTime() / 1000"

//
// C++ side typemaps
// All the typemaps are just an assignments.
//

%typemap(in) time_t "$1 = $input;" // Conversion from 'jni' (JNI C type) to C++ type
%typemap(out) time_t "$result = $1;" // Conversion from C++ type to 'jni' (JNI C type)

// Conversion from 'jni' (JNI C type) to C++ type for director methods
%typemap(directorout) time_t "$result = $input;"

// Conversion from C++ type to 'jni' (JNI C type) for director methods
// Note: Ljava/lang/String; is the JNI field descriptor for the Java String type
%typemap(directorin, descriptor = "Ljava/util/Date;") time_t "$input = $1;"

Include this file into SWIG instructions file SwigAndroidGuide.i:

%module(directors="1") SwigAndroidGuide

// ...

%include <std_shared_ptr.i>
%include <std_string.i>
// ...
%include "time_t.i" // << Add this line

Now run SWIG script run_swig.sh and see that Message.java has a proper implementation of our new functions.

public java.util.Date getCreationDate() {
    return new java.util.Date(SwigAndroidGuideJNI.Message_getCreationDate(swigCPtr, this) * 1000);
}

public void setCreationDate(java.util.Date creationDate) {
    SwigAndroidGuideJNI.Message_setCreationDate(swigCPtr, this, creationDate.getTime() / 1000);
}

Here the javain/out typemaps were unfolded:

  • $javainput => creationDate argument in setCreationDate(java.util.Date creationDate).

  • $jnicall => SwigAndroidGuideJNI.Message_getCreationDate(swigCPtr, this) in getCreationDate().

Let's write some code to use our new functions.

ActivityModel.cpp:

    void ActivityModel::onCreate(IAndroidActivity *androidActivity)
    {
        // ...
        message->setText(make_shared<string>("Greetings!"));

        // Add these lines

        time_t now = time(nullptr);
        __android_log_print(ANDROID_LOG_DEBUG, TAG, "%s", asctime(localtime(&now)));
        message->setCreationDate(now);

        // ...

MainActivity.java (git dif):

- Log.d(TAG, String.format(Locale.US, "Message sent: '%d %s'", message.getId(), message.getText()));
+ Log.d(TAG, String.format(Locale.US, "Message sent: '%d %s %s'", message.getId(), message.getText(), message.getCreationDate()));

Run the application and verify that log output is correct - the same date and time are printed in C++ and Java:

D/ActivityModel: Thu Oct 29 18:46:08 2020
D/MainActivity: Message sent: '22 Greetings! Thu Oct 29 18:46:08 GMT+02:00 2020'

More complexity (all the weirdness)

Shared pointer to vector of shared pointers to vectors of bytes

shared_ptr<vector<shared_ptr<vector<unsigned char>>>>

At first, wrapping std containers with SWIG is easy. Just include this into your SWIG file:

%include <std_vector.i> // Or another C++ container
%template(Std_Vector_Int) std::vector<int>; // Define a template and set a name for your type
%template(Std_Vector_Message) std::vector<message>; // Define a template and set a name for your type

Run SWIG, and you will get the working code:

public class Std_Vector_Int extends java.util.AbstractList<Integer> implements java.util.RandomAccess {
  // ...

  public Integer get(int index) {
    return doGet(index);
  }

  public Integer set(int index, Integer e) {
    return doSet(index, e);
  }

  public boolean add(Integer e) {
    modCount++;
    doAdd(e);
    return true;
  }

  public Integer remove(int index) {
    modCount++;
    return doRemove(index);
  }

  public int size() {
    return doSize();
  }

  public Std_Vector_Int() {
    this(SwigAndroidGuideJNI.new_Std_Vector_Int__SWIG_0(), true);
  }

  // ...
}

If you'll write C++ code like this:

public int decryptMessages(std::vector<message> messages) {
    return // ...
}

Generated Java method to call this C++ method will be like:

public int decryptMessages(Std_Vector_Message messages) {
    // ...
}

The same applies to %template(Std_Vector_Int) std::vector<int>. Quite simple. However, the downsides of this:

  1. Non-native Java containers. The generated types are just simple wrappers for real C++ objects.

  2. Per-element access, i.e. each access to element is C++ call - lower performance.

  3. Need to explicitly define a SWIG template for each use-case.

Despite the downsides, I think the above should be the default way. Unless you have special requirements, i.e. for performance-critical tasks. To overcome the first problem, you can create converter functions, that will convert this to Java containers. This is easy. Well, issue #2 is not an issue for most cases, but it was an issue in our case. We wanted to pass byte arrays between Java and C++ without the performance penalty. Java has a ByteBuffer type, which storage can be allocated in C++ heap (called direct ByteBuffer); but there are numerous problems with the ByteBuffer type associated with the inability to manage its memory. Also, we wanted Java to interact with our existing code without actually changing it or having any additional conversion layer. Using the approach given below data is still copied between C++ and Java (opposite to what direct ByteBuffer offers), but the data access and memory management are efficient, and we don't experience any problems.

So, shared pointer to vector of shared pointers to vectors of bytes

shared_ptr<vector<shared_ptr<vector<unsigned char>>>> Well, you can think of this is too much. But this is just an equivalent of Java byte[][]. C++ syntax is unfriendly. In the application I've worked on, this type was holding parts of encrypted data. We don't want this (huge amounts of data) to be copied when passing around, and we wanted quick access from Java. Before proceeding, see our Types.h, types from it are used in C++ and SWIG:

#include <memory>
#include <vector>

typedef unsigned char byte;
typedef std::shared_ptr <std::string> shared_ptr_to_string;
typedef std::shared_ptr<std::vector<std::shared_ptr<std::vector<byte>>>>
shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes;

Let's write our C++ code first.

class ActivityModel
{
public:
    // ...

    // Add this function
    int decryptMessages(shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes messages);
};

As usual, the implementation doesn't do anything real. We are covering only cross-language interaction here.

int ActivityModel::decryptMessages(shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes messages)
{
    int bytesDecrypted = 0;
    for (int i = 0; i < messages->size(); ++i)
    {
        bytesDecrypted += messages->at(i)->size();
    }

    return bytesDecrypted;
}

SWIG code is given below. Place it in std_shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes.i. The code is well-commented and self-explanatory. Read it carefully if you want to dig into it. The approach used is the same as for shared_ptr<string>. See the conversion map in the Table 1.

%include <std_shared_ptr.i>
%include <std_vector.i>

namespace std
{
   // To properly recognize type defined in our Types.h
   %template(shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes) std::shared_ptr<std::vector<std::shared_ptr<std::vector<unsigned char="">>>>;
}

// Use as Swig type alias
%define %shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes std::shared_ptr<std::vector<std::shared_ptr<std::vector<unsigned char="">>>> %enddef

%{
#include "Types.h"

namespace
{
    /*
     * C++ type 'std::shared_ptr<std::vector<std::shared_ptr<std::vector<unsigned char="">>>>' with alias 'shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes'
     * is equivalent to Java type 'byte[][]',
     * which is equivalent to JNI type: 'jobjectArray' with 'jbyteArray' elements.
     * JNI descriptors are 'jobjectArray' = '[[B' (treat as byte[][]), 'jbyteArray' = '[B'.
     * Note: more logical will be to pass two-dimensional array, but JNI does support only 1-dimentional arrays.
     */

    shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes convertArrayOfArraysOfByteFromJavaToCpp(JNIEnv *jenv, jobjectArray javaArrayOfArraysOfBytes)
    {
        if(javaArrayOfArraysOfBytes == nullptr) return nullptr;

        // Create outer vector

        jsize sizeOfArrayOfArrays = jenv->GetArrayLength(javaArrayOfArraysOfBytes);
        std::shared_ptr<std::vector<shared_ptr_to_vector_of_bytes>> vectorOfVectorsOfBytes = std::make_shared<std::vector<shared_ptr_to_vector_of_bytes>>();
        vectorOfVectorsOfBytes->reserve((size_t) sizeOfArrayOfArrays);

        // Push all inner vectors into outer vector

        for(jsize i = 0; i < sizeOfArrayOfArrays; ++i)
        {
            // Null array

            jbyteArray arrayOfBytes = (jbyteArray) (jenv->GetObjectArrayElement(javaArrayOfArraysOfBytes, i));
            if (arrayOfBytes == nullptr)
            {
                vectorOfVectorsOfBytes->push_back(nullptr);
                continue;
            }

            // Empty array

            jsize sizeOfBuffer = jenv->GetArrayLength(arrayOfBytes);
            if(sizeOfBuffer == 0)
            {
                vectorOfVectorsOfBytes->push_back(std::make_shared<vector_of_bytes>());
                continue;
            }

            // Array with elements

            jbyte *bufferOfBytes = jenv->GetByteArrayElements(arrayOfBytes, nullptr);
            shared_ptr_to_vector_of_bytes vector = std::make_shared<vector_of_bytes>(bufferOfBytes, bufferOfBytes + sizeOfBuffer);
            vectorOfVectorsOfBytes->push_back(vector);
            jenv->ReleaseByteArrayElements(arrayOfBytes, bufferOfBytes, 0);
        }

        return vectorOfVectorsOfBytes;
    }

    jobjectArray convertArrayOfArraysOfByteFromCppToJava(JNIEnv *jenv, shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes vectorOfVectorsOfBytes)
    {
        if (!vectorOfVectorsOfBytes) return nullptr;

        // Allocate array of objects, where each object is 'jbyteArray'

        jsize sizeOfArrayOfArrays = (jsize) vectorOfVectorsOfBytes->size();
        jclass classOfJByteArray = jenv->FindClass("[B"); // '[B' is the JNI field descriptor for the Java byte[] type
        jobjectArray arrayOfArraysOfBytes = jenv->NewObjectArray(sizeOfArrayOfArrays, classOfJByteArray, nullptr);

        // Set inner arrays to it

        for(jsize i = 0; i < sizeOfArrayOfArrays; ++i)
        {
            // Null array

            shared_ptr_to_vector_of_bytes vectorOfBytes = vectorOfVectorsOfBytes->at(i);
            if (!vectorOfBytes)
            {
                continue;
            }

            // Empty array

            jsize sizeOfArray = (jsize) vectorOfBytes->size();
            jbyteArray javaArrayOfBytes = jenv->NewByteArray(sizeOfArray);
            jenv->SetObjectArrayElement(arrayOfArraysOfBytes, (jsize) i, javaArrayOfBytes);
            if (sizeOfArray == 0)
            {
                continue;
            }

            // Array with elements

            jenv->SetByteArrayRegion(javaArrayOfBytes, 0, sizeOfArray, (jbyte *) &amp;(vectorOfBytes->at(0)));
        }

        return arrayOfArraysOfBytes;
    }
}
%}

%typemap(jstype) %shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes "byte[][]"
%typemap(jtype) %shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes "byte[][]"
%typemap(javain) %shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes "$javainput"
%typemap(jni) %shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes "jobjectArray"
%typemap(in) %shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes
{
    $1 = convertArrayOfArraysOfByteFromJavaToCpp(jenv, $input);
}
%typemap(javadirectorout) %shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes "$javacall"
%typemap(directorout) %shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes
{
    $result = convertArrayOfArraysOfByteFromJavaToCpp(jenv, $input);
}
%typemap(argout) %shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes;  // Just override the default and do nothing
%typemap(freearg) %shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes; // Just override the default and do nothing
%typemap(javaout) %shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes
{
     return $jnicall;
}
%typemap(out) %shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes
{
    $result = convertArrayOfArraysOfByteFromCppToJava(jenv, $1);
}
%typemap(javadirectorin) %shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes "$jniinput"
// '[[B' is the JNI field descriptor for the Java 'byte[][]' type
%typemap(directorin,descriptor="[[B") %shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes
{    
    $input = convertArrayOfArraysOfByteFromCppToJava(jenv, $1);
}

Include this into the main SWIG file SwigAndroidGuide.i.

// ...
%include <std_string.i>
%include "std_shared_ptr_to_string.i"
%include "std_shared_ptr_to_vector_of_shared_ptr_to_vector_of_bytes.i" // Add this line

%{
#include "Types.h" // Add this line
#include "ActivityModel.h"
// ...
%}

// Add this line.
// Important! This should be included before any usage of types typedefed inside.
// Otherwise SWIG can't process them properly.
%include "Types.h"

%include "ActivityModel.h"
// ...

Generate SWIG wrappers running run_swig.sh. Generated Java function in ActivityModel.java looks like this - with real Java type byte[][].

public int decryptMessages(byte[][] messages) {
    return SwigAndroidGuideJNI.ActivityModel_decryptMessages(swigCPtr, this, messages);
}

Usage in MainActivity.java:

public class MainActivity extends AppCompatActivity
{
    // ...
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        // ...
        decrypt();
    }
    // ...
    private void decrypt()
    {
        byte[][] messages = new byte[5][];
        for (int i = 0; i < messages.length; ++i)
        {
            messages[i] = new byte[(i + 1) * 385];
        }

        int result = activityModel.decryptMessages(messages);
        Log.d(TAG, "decrypt() Message bytes decrypted: " + result);
    }

And the result is as expected, so it was called correctly: D/MainActivity: decrypt() Message bytes decrypted: 5775

P.S. The easier case of shared_ptr<vector<byte>> is not covered in this article, but you can find the typemap in the file /swig/std_shared_ptr_to_vector_of_bytes.i

std::function

Or passing functions to Java. The simple approach here is to create a class similar to Java Runnable

class Runnable
{
public:
    virtual void run() = 0;
};

Subclass it where needed and be fine. But we wanted to make the experience more 'integrated'. Especially regarding cool feature of std::function - capturing variables. The class we are going to create now will allow us to run our std::function from C++ in Java. We don't want to use POSIX threads, as a lot of effort required to do so, instead of using native Android/iOS background task management. Let's start creating a C++ interface to be subclassed in Java - ITaskScheduler.h

class ITaskScheduler
{
public:
    virtual ~ITaskScheduler() = default;
    virtual bool isMainThread() = 0;
    virtual void executeOnBackgroundThread(std::function<void()> function) = 0;
    virtual void executeOnUIThread(std::function<void()> function) = 0;
};

After including this file for processing into the SWIG file SwigAndroidGuide.i as usual, and running SWIG notice in git status that SWIG can't process std::function properly, generating mock file:

app/src/main/java/com/goldberg/swigandroidguide/swiggenerated/SWIGTYPE_p_std__functionT_void_fF_t.java

We have to write a special typemap for this case. I didn't write it. It is taken from Stackoverflow, written by the great author Flexo, who knows how to deal with SWIG. If I don't found this answer, we'd stick with the Runnable approach mentioned above, as it is much simpler. I will not insert code here, it's quite complex and large. See the code directly in file /swig/std_function.i from the repository and read the original answer. In this file, there are two definitions for std::function

// Ret = Return type
%define %std_function(Name, Ret, ...)
%define %std_function_void(Name, Ret)

I have to add %std_function_void, which implementation is 97% matches the %std_function because for some reason %define statement was not unfolding properly when no variadic arguments were passed .... When you pass no variadic arguments, the script thinks that one empty argument is passed, while it should think zero arguments passed. %std_function_void eliminates variadic argument. I don't know what's the reason, maybe it is due to OS type/version or SWIG version.

So this code in the SWIG file SwigAndroidGuide.i:

%std_function(FunctorVoid, void);

was giving the error when running SWIG: SwigAndroidGuide.i:13: Error: Syntax error in: $typemap(jstype,)

Ok, let's use it. The usage is simple. Just define in your main SWIG file SwigAndroidGuide.i which functions you want to pass through the language boundaries. The only downside of this approach in comparison to Runnable, is that we have to define each type used.

%include "std_function.i"

// ...

// Name is FunctorVoid; void return type and void arguments.
%std_function_void(FunctorVoid, void);

// Name is FunctorVoidMessage; void return type and shared_ptr<message> argument.
%std_function(FunctorVoidMessage, void, std::shared_ptr<swigandroidguide::message>);

When re-generating SWIG (run_swig.sh), your curious type file SWIGTYPE_p_std__functionT_void_fF_t.java transforms to app/src/main/java/com/goldberg/swigandroidguide/swiggenerated/SWIGTYPE_p_f___void.java

Probably the file name is assembled in the included script std_function.i, but I can't understand where :) Let's implement our class in Java:

final class TaskScheduler extends ITaskScheduler
{
    private static final Handler mainThreadHandler = new Handler();
    private static final ExecutorService backgroundTaskExecutor = Executors.newSingleThreadExecutor();

    @Override
    public boolean isMainThread()
    {
        return Looper.getMainLooper().getThread() == Thread.currentThread();
    }
    @Override
    public void executeOnBackgroundThread(FunctorVoid function)
    {
        backgroundTaskExecutor.execute(function::call);
    }

    @Override
    public void executeOnUIThread(FunctorVoid function)
    {
        mainThreadHandler.post(function::call);
    }
}

To use function references, add this to your app-level app/build.gradle:

android {
    // ...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

Let's also add functions to IAndroidActivity to allow us to use our newly created class:

class IAndroidActivity
{
public:
    virtual ITaskScheduler * getTaskScheduler() = 0;
    // ...
};

When you re-run SWIG once more, it will issue a warning

app/src/main/cpp/IAndroidActivity.h:23:
Warning 473:
Returning a pointer or reference in a director method is not recommended.

The warning is issued because, as we are returning a pointer, the Java object bound to this pointer may become invalid at any point of time - after the next Garbage Collector run. We have to make sure pointed Java Object will not be garbage collected by keeping its reference, as here, in implementation. We will talk about Garbage Collector in the next section.

public class MainActivity extends AppCompatActivity
{
    // ...

    private final ITaskScheduler taskScheduler = new TaskScheduler();

    private class AndroidActivity extends IAndroidActivity
    {
        @Override
        public ITaskScheduler getTaskScheduler()
        {
            return taskScheduler;
        }

        // ...
    }

    // ...

To overcome the warning, we can pass pointers directly to the functions, as with void onCreate(IAndroidActivity* androidActivity);. Even if you pass a not-referenced object to this function, it will definitely live until the function exits. Because such an object is the so-called JNI Local Reference. Ok, let's run some async tasks in C++ (ActivityModel.cpp):

void doAsyncTasks(IAndroidActivity *androidActivity)
{
    ITaskScheduler *taskScheduler = androidActivity->getTaskScheduler();
    __android_log_print(ANDROID_LOG_DEBUG, TAG, "Is main thread: %d", taskScheduler->isMainThread());
    taskScheduler->executeOnBackgroundThread(
            [=]()
            {
                __android_log_print(ANDROID_LOG_DEBUG, TAG, "Is main thread: %d", taskScheduler->isMainThread());
                taskScheduler->executeOnUIThread(
                        [=]()
                        {
                            __android_log_print(ANDROID_LOG_DEBUG, TAG, "Is main thread: %d", taskScheduler->isMainThread());
                        });
            });
}

void ActivityModel::onCreate(IAndroidActivity *androidActivity)
{
    androidActivity->showToast(make_shared<string>("Toast from C++"));
    androidActivity->sendMessage(make_shared<message>(22, make_shared<string>("Greetings!")));
    doAsyncTasks(androidActivity);
}

Our C++ code doesn't do anything except writing logs. From logs, we can see on which thread which lambda was executed. You can also see this from thread ids in logs 21342-21402. The left is process id, the right is thread id; for the main thread they are equal.

2020-08-16 17:56:22.580 21342-21342/? D/ActivityModel: Is main thread: 1
2020-08-16 17:56:22.584 21342-21402/? D/ActivityModel: Is main thread: 0
2020-08-16 17:56:22.634 21342-21342/? D/ActivityModel: Is main thread: 1

GC and Reference counting (or how to break the code)

Java GC and C++ shared_ptr doesn't know about one another. Fin.

With SWIG, when you pass your C++ object to Java, a reference to it will be kept. So, if you abandon your object in C++, Java is still able to use it. It's not good to abandon normal pointers, of course:) But with smart_ptr or regular objects (non-pointers), you can just create them, send to Java, and forget. The object will be kept alive until Java is using it. You can even pass your Java object back to C++ and it will be fine.

It gets complicated when you pass Java object inherited from C++ interface (director) to C++, you have to make sure Java object is valid when you using it. Java object is valid until it is Garbage Collected.

1. Your Java object is safe if you are using it in the scope of a function.

Even if you call it from Java as this:

activityModel.onCreate(new AndroidActivity());

Until onCreate() exits, your object will not be Garbage Collected. Your new AndroidActivity() object here is handled as a so-called JNI Local Reference. Usage in a scope of a function:

void ActivityModel::onCreate(IAndroidActivity *androidActivity)
{
    // Any usage of 'androidActivity' here is safe
    androidActivity->showToast(make_shared<string>("Toast from C++"));
    androidActivity->sendMessage(make_shared<message>(22, make_shared<string>("Greetings!")));
    doAsyncTasks(androidActivity);
    sendPolymorphicMessage(androidActivity);
}

If we store a pointer to IAndroidActivity *androidActivity anywhere, it will be eventually collected by GC. And we don't know when it will happen. You will just get a crash or undefined behavior.

2. Your Java object is safe when you keeping a reference to it.

This is what we currently do with our TaskScheduler - we keeping reference.

public class MainActivity extends AppCompatActivity
{
    // ...
    private final ITaskScheduler taskScheduler = new TaskScheduler();

But, TaskScheduler by its functionality (executing tasks) should have a longer lifecycle than MainActivity; if we have more activities, or on simple screen rotation event - activity will be destroyed, and reference to the old TaskScheduler lost. It eventually will be GC'd. Better to store TaskScheduler in the Application class, as it lives until the application will be destroyed. Or store it in the static variable.

public class App extends Application
{
    // ...
    private final ITaskScheduler taskScheduler = new TaskScheduler();

Such an object as ActivityModel, holding the activity data and business logic can be stored in the scope of the ViewModel of particular Activity. In our case, it would be like.

public class MainViewModel extends ViewModel
{
    // ...
    public final ActivityModel activityModel = new ActivityModel();

2.1. Breaking the code.

To illustrate what happens when you lose the reference to the Java director object, let's recall our C++ code from ActivityModel.cpp. We have a chain of lambda functions here. They are executed out of the scope of the doAsyncTasks() function.

void doAsyncTasks(IAndroidActivity *androidActivity)
{
    ITaskScheduler *taskScheduler = androidActivity->getTaskScheduler();
    __android_log_print(ANDROID_LOG_DEBUG, TAG, "Is main thread: %d", taskScheduler->isMainThread());
    taskScheduler->executeOnBackgroundThread(
            [=]()
            {
                __android_log_print(ANDROID_LOG_DEBUG, TAG, "Is main thread: %d", taskScheduler->isMainThread());
                taskScheduler->executeOnUIThread(
                        [=]()
                        {
                            __android_log_print(ANDROID_LOG_DEBUG, TAG, "Is main thread: %d", taskScheduler->isMainThread());
                        });
            });
}
// ...
void ActivityModel::onCreate(IAndroidActivity *androidActivity)
{
    doAsyncTasks(androidActivity);
    // ...
}

And let's alter our Java code a bit, to lose a reference, in MainActivity.java:

public class MainActivity extends AppCompatActivity
{
    // ...
    private class AndroidActivity extends IAndroidActivity
    {
        // ...
        @Override
        public ITaskScheduler getTaskScheduler()
        {
            // return taskScheduler; // Delete this line

            // Add this line
            // Creates new object and not stores a reference
            return new TaskScheduler();
        }

And to delay the lambda execution to give enough time for GC to run, in TaskScheduler.java

final class TaskScheduler extends ITaskScheduler
{
    private static final Handler mainThreadHandler = new Handler();
    private static final ExecutorService backgroundTaskExecutor = Executors.newSingleThreadExecutor();
    // ...
    @Override
    public void executeOnBackgroundThread(FunctorVoid function)
    {
        mainThreadHandler.postDelayed(() -> {
            backgroundTaskExecutor.execute(function::call);
        }, 10_000); // Delay task execution by 10 seconds
    }

Now run the application, and exactly in 10 seconds, you will get a crash. You may actually need a few tries - GC is not guaranteed to run at any particular time. If still not got a crash - please increase time significantly. This shortened log of a crash points to our std::function wrapper.

08-17 21:15:09.634 15891 15924 F libc    : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x6c6f675f6d6f73 in tid 15924 (pool-1-thread-1), pid 15891 (wigandroidguide)
08-17 21:15:09.778 15927 15927 F DEBUG   :     #03 pc 00000000000284dc  /data/app/com.goldberg.swigandroidguide-Hs_uprJLKN1eGn1jdTm0eQ==/lib/arm64/libnative-lib.so (std::__ndk1::function<void ()="">::operator()() const+160)
08-17 21:15:09.778 15927 15927 F DEBUG   :     #04 pc 000000000002842c  /data/app/com.goldberg.swigandroidguide-Hs_uprJLKN1eGn1jdTm0eQ==/lib/arm64/libnative-lib.so (Java_com_goldberg_swigandroidguide_swiggenerated_SwigAndroidGuideJNI_FunctorVoid_1call+48)

3. Keep global reference using NewGlobalRef

You can call JNI function NewGlobalRef in C++ on Java object, this will keep a reference to it, even if you abandon the reference in Java. Then, when you are done with the object, release the reference calling DeleteGlobalRef. With SWIG it can be done via typemaps or another way. I didn't do this, because this solution is unsafe, as this is manual memory management; much safer just to keep references. Also, there is a limited count of these references. Don't know for now, but a few years ago it was about 512. It probably depends on the device's VM settings.

Debugging SWIG

Debugging SWIG script

For debugging SWIG script run_swig.sh and code generation process.

  • Use -v (Verbose) argument for verbose output.

  • See other debug options by running swig -help in the terminal.

The line where the SWIG program is executed in run_swig.sh is the last line. The options you will have to add depends on your specific case.

swig -I${android_src_dir} -c++ -java -package com.goldberg.swigandroidguide.swiggenerated -o ${generated_cpp_file} -outdir ${generated_java_dir} SwigAndroidGuide.i

Debugging generated code and runtime errors

Observe SWIG-generated files

As mentioned in the chapter Calling C++ from Java, observe the files generated with SWIG (it is explained in the linked section).

As mentioned here, you may get a file generated with the naming pattern SWIGTYPE_: app/src/main/java/com/goldberg/swigandroidguide/swiggenerated/SWIGTYPE_p_std__string.java Such a file means SWIG didn't find a way to process your type. It does the basic pointer wrapping, and it will go through the language boundaries, but you will not be able to use your object.

Exceptionally, for our std::function SWIG-generated file name patterns are:

  • incorrect: SWIGTYPE_p_std__functionT_void_fF_t.java

  • correct: SWIGTYPE_p_f___void.java

Check generated code

This usually doesn't need for SWIG-generated code for arguments of simple types. But if you have added your custom typemaps, you may want to check what code was actually generated. Let's walk through the process. At first, let's write simple test for our function setText(shared_ptr<string>) in Message.h. Edit app/build.gradle (git diff shown):

     implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+    androidTestImplementation 'androidx.test:runner:1.3.0'
+    androidTestImplementation 'androidx.test.ext:truth:1.3.0'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
 }

Create app/src/androidTest/java/com/goldberg/swigandroidguide/MessageTest.java:

package com.goldberg.swigandroidguide;

import androidx.test.ext.junit.runners.AndroidJUnit4;

import com.goldberg.swigandroidguide.swiggenerated.Message;

import org.junit.Test;
import org.junit.runner.RunWith;

import static com.google.common.truth.Truth.assertThat;

@RunWith(AndroidJUnit4.class)
public class MessageTest
{
    static
    {
        // Load C++ library.
        // Alternatively it can be loaded in Application class,
        // as an Android Application is created when executing Android tests.
        System.loadLibrary("native-lib");
    }

    @Test
    public void test()
    {
        Message message = new Message();
        message.setText("Have a nice day");
        assertThat(message.getText()).isEqualTo("Have a nice day");
    }
}

Run this test, it will pass. But actually our shared_ptr<string> typemap is incomplete. Now let's break it.

    @Test
    public void test()
    {
        Message message = new Message();
        message.setText(null);
        assertThat(message.getText()).isNull();

When executing this test, we will get a crash (shortened and comments added; see Run window on Android Studio or logcat to see this output). The call stack is read bottom-to-top:

// Reason of the crash
A/wigandroidguid: java_vm_ext.cc:542] JNI DETECTED ERROR IN APPLICATION: GetStringUTFChars received NULL jstring
java_vm_ext.cc:542]     in call to GetStringUTFChars
java_vm_ext.cc:542]     from void com.goldberg.swigandroidguide.swiggenerated.SwigAndroidGuideJNI.Message_setText(long, com.goldberg.swigandroidguide.swiggenerated.Message, java.lang.String)
// ...
// Place of crash
runtime.cc:558]   native: #14 pc 0000000000031e18  /data/app/com.goldberg.swigandroidguide-O2baaPk-eJTHZyu474aE9Q==/lib/arm64/libnative-lib.so (_JNIEnv::GetStringUTFChars(_jstring*, unsigned char*)+64)
// `???` seems to be our function `convertStringFromJavaToCpp` from the typemap
runtime.cc:558]   native: #15 pc 000000000002ed7c  /data/app/com.goldberg.swigandroidguide-O2baaPk-eJTHZyu474aE9Q==/lib/arm64/libnative-lib.so (???)
// Function in `swig-generated/SwigAndroidGuide_wrap.cpp`
runtime.cc:558]   native: #16 pc 000000000002eab8  /data/app/com.goldberg.swigandroidguide-O2baaPk-eJTHZyu474aE9Q==/lib/arm64/libnative-lib.so (Java_com_goldberg_swigandroidguide_swiggenerated_SwigAndroidGuideJNI_Message_1setText+164)
// ...
runtime.cc:558]   at com.goldberg.swigandroidguide.swiggenerated.SwigAndroidGuideJNI.Message_setText(Native method)
// ...
// Call to our function in Java
runtime.cc:558]   at com.goldberg.swigandroidguide.swiggenerated.Message.setText(Message.java:56)
runtime.cc:558]   at com.goldberg.swigandroidguide.MessageTest.test(MessageTest.java:27)

From this crash report we can clearly see that broken function is setText(shared_ptr<string>) in Message.h. How we inverstigate this? At first, we need to know which typemap is failed. Typemap description tells us it should be the %typemap(in) std::shared_ptr<std::string> in std_shared_ptr_to_string.i typemap file we have used earlier:

// Conversion from 'jni' (JNI C type) to C++ type
%typemap(in) std::shared_ptr<std::string>
{
    $1 = convertStringFromJavaToCpp(jenv, $input);
}

To know exactly, the simplest way is to modify the typemap and check if the generated code has changed.

// Conversion from 'jni' (JNI C type) to C++ type
%typemap(in) std::shared_ptr<std::string>
{
    // TYPEMAP START
    $1 = convertStringFromJavaToCpp(jenv, $input);
    // TYPEMAP END
}

Run SWIG script run_swig.sh to re-generate the code. Check the generated code for this function in SwigAndroidGuide_wrap.cpp:

SWIGEXPORT void JNICALL
Java_com_goldberg_swigandroidguide_swiggenerated_SwigAndroidGuideJNI_Message_1setText(
JNIEnv *jenv, jclass jcls, jlong jarg1, jobject jarg1_, jstring jarg2) {
  SwigAndroidGuide::Message *arg1 = (SwigAndroidGuide::Message *) 0 ;
  std::shared_ptr< std::string > arg2 ;
  std::shared_ptr< SwigAndroidGuide::Message > *smartarg1 = 0 ;

  (void)jenv;
  (void)jcls;
  (void)jarg1_;

  smartarg1 = *(std::shared_ptr<  SwigAndroidGuide::Message > **)&amp;jarg1;
  arg1 = (SwigAndroidGuide::Message *)(smartarg1 ? smartarg1->get() : 0); 
  {
    // TYPEMAP START
    arg2 = convertStringFromJavaToCpp(jenv, jarg2);
    // TYPEMAP END
  }
  (arg1)->setText(arg2);
}

Our newly added comments are here, it means we picked the right typemap. We know GetStringUTFChars with null argument caused a crash. It is in our conversion function in std_shared_ptr_to_string.i.

std::shared_ptr<std::string> convertStringFromJavaToCpp(JNIEnv *jenv, jstring javaString)
{
    const char * psz_string = jenv->GetStringUTFChars(javaString, NULL); // Crash happened at this line
    if (!psz_string)
    {
        return nullptr;
    }

    std::shared_ptr<std::string> cppString = std::make_shared<std::string>(psz_string);
    jenv->ReleaseStringUTFChars(javaString, psz_string);
    return cppString;
}

Let's fix it by handling the null argument.

std::shared_ptr<std::string> convertStringFromJavaToCpp(JNIEnv *jenv, jstring javaString)
{
    if (!javaString) return nullptr; // < Add this line

    const char * psz_string = jenv->GetStringUTFChars(javaString, NULL);

Run SWIG script run_swig.sh to re-generate the code. Now run an Android test. It will pass. We were making changes to the typemap and re-generated the code because the problem was very simple. But for the more complex problem, you can edit the SWIG-generated code directly to find the solution faster, without the need to re-generate wrapper, and then implement it in the typemap.

Congratulations! Now you know how to debug SWIG-generated code. We have handled only the case on the C++ side. But when Java-side broke, the approach is exactly the same; the only difference is that you have to debug code generated on the Java side.

You can also run a project in Debug mode when needed to see variable values, go step-by-step, etc.

Misc

If you don't want to process some of your C++ code with SWIG, use #ifndef SWIG in the C++ code. The case may be, that you don't need or don't want to reveal some functions to Java, while they may be used on the C++ side and have to be in the public: section.

Conclusion

That's all.

In this part 2, I covered complex situations when using SWIG, and cases someone may call "weird". I've made hints on how to break the code (and how to not) and how to debug SWIG.

The use-case of SWIG is quite narrow, as there are many simpler ways for cross-platform development, but I hope someone will found this tutorial and especially the practical bits & project useful.

Best regards, and thank you for reading!

Resources

Tags:
Hubs:
0
Comments0

Articles