Pull to refresh

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

Reading time 18 min
Views 4K

SWIG is a tool for generating cross-language interfaces - it provides interoperability of C++ and other languages (C++ and Java in our case). SWIG just simplifies and automizes cross-language interaction; otherwise, you may end up with thousands of lines of handwritten JNI code - but SWIG covers this for you.

This guide is for newbies (Part 1) and for those who experienced in SWIG (part 2). I'm starting from basic setup and usage and ending with complex & weird cases encountered in development. The latter cases are not so complex, rather usual for modern languages, which SWIG doesn't support yet (as lambdas).

This guide is practical. In opposition to overcomplicated huge-volume SWIG documentation, this guide is showing the cases practically. The bits developed by myself while working on the different projects or taken from StackOverflow. This guide allows you to quick-start an Android Studio project and giving practical examples of using SWIG. The link to the Android Studio project is here.

This guide is Android-first. The goal was to make it simple to onboard for Android developers. There are many articles about SWIG, but they are mainly for desktop Java applications, and it is quite an overhead to just try them on Android to check if the solution for the particular problem is working. While this guide includes an Android Studio project, with which you can play around instantly. Of course, all the information given here applies to any Java application.

Warning! I should warn you, that nowadays cross-platform development offers powerful tools. If you are developing a new application it is much more cost-efficient in practice to use ReactNative, Flutter of Kotlin-Native than the SWIG. While SWIG is more suitable to connect the C++ library or existing C++ application core.

Enjoy :)

Table of contents

Installation and Starting

Install the latest Android Studio (3.5.3), then the latest NDK (using Android Studio SDK manager), and the latest SWIG tool (4.0.1). Add SWIG to your PATH environment variable. The versions are given at the moment of writing this article.

Create new Android Studio project: File -> New -> New Project -> Select "Native C++" -> Chose project parameters and location, use Java* language -> Press "Finish". *You can use Kotlin, no difference using SWIG. This guide is just written in Java. Now you've got a simple project template, which includes C++ code. Run it on your device.

In short, except for the standard project structure for Java, your new project includes the:

  • file with C++ code (app/src/main/cpp/native-lib.cpp)

  • Rules to build your C++ code into a library which will be packaged into your APK (app/src/main/cpp/CMakeLists.txt)

* All paths here and later are relative to the Android Studio project root.

Then you load your library dynamically in Java code:

public class MainActivity extends AppCompatActivity
{
    static
    {
        System.loadLibrary("native-lib");
    }
}

And now you can use the C++ code through JNI. You can read more about the basic C++ usage in Android here. And about JNI specifically here. In any case, just check the article and note how many JNI code is required to use Java array or to call a simple method. JNI is a low-level tool and most of the JNI code is not checkable by the compiler - we will know about the error only in the runtime. That's why we are using SWIG, which automizes the process to generate JNI boilerplate code.

Simple cases

If you are known with SWIG, you can jump to the next section.

SWIG is a tool for generating a JNI layer between Java and C++ automatically, based on the set of rules. On the input, it takes a public section of C++ header files, and on the output generates C++/JNI and Java files which can be included into the project and interlanguage interaction becomes seamless. Not as seamless, as Obj-C/C++ boundary, and has some caveats, but much simpler than pure JNI. For simple situations and basic types, the rules are already existing in SWIG. For custom cases, you have to write your own rules. This is hard to understand at first, but once the rules are written, generating new interfaces become seamless - just run a script once more, when your header files have changed. However, SWIG has downsides - it is overcomplicated, bloated, and has very-very large documentation. Best to learn SWIG on practice.

We are using the SWIG tool of the latest version 4.0.1. It should be installed on your PC. It doesn't interact with IDE. It is called from the external script. The SWIG tool should be added to your PATH environment variable.

Calling C++ from Java. Writing the SWIG script.

To call C++ from Java. Imagine we have Example C++ class in 'ActivityModel.h'.

class ActivityModel
{
public:
    void setMultiplier(int multiplier);
    int multiply(int a);
    static int multiply(int a, int b);

private:

    int _multiplier;
};

And implementation in 'ActivityModel.cpp'.

#include "ActivityModel.h"

void ActivityModel::setMultiplier(int multiplier)
{
    _multiplier = multiplier;
}

int ActivityModel::multiply(int a)
{
    return _multiplier * a;
}

int ActivityModel::multiply(int a, int b)
{
    return a * b;
}

Now create the swig directory inside your project. We need to include the header into a special SWIG file which will be handled by our SWIG script. Add into the swig directory file SwigAndroidGuide.i with content:

// Module name (required)

%module SwigAndroidGuide

// Anything in the following section is added verbatim to the .cxx wrapper file

%{
#include "ActivityModel.h"
%}

// Process our C++ file (only the public section)

%include "ActivityModel.h"

and add the script to run SWIG (swig/run_shig.sh). This is done for bash and will run on Linux/Mac. For Windows, you have to re-write the script for Windows terminal or try to using Cygwin.

#!/bin/bash

# cd to directory of this script.
# Note: don't need to go back - by exit this script, terminal will remain in the directory it currently is.

cd "$(dirname "$0")"

# Define various files and directories for script to work with.

generated_src_base_dir="../app/src/main"
generated_cpp_dir="${generated_src_base_dir}/cpp/swig-generated"
android_src_dir="${generated_src_base_dir}/cpp"

# Generated C++/JNI code goes into this file.
# This file is included into 'CMakeLists.txt'
generated_cpp_file="${generated_cpp_dir}/SwigAndroidGuide_wrap.cpp"

# Generated Java code goes into this directory.
generated_java_dir="${generated_src_base_dir}/java/com/goldberg/swigandroidguide/swiggenerated"

# Swig required already existing output directories.
# Delete all existing generated code and re-create directories.

rm -rf ${generated_cpp_dir}
rm -rf ${generated_java_dir}

mkdir -p ${generated_cpp_dir}
mkdir -p ${generated_java_dir}

# Run 'swig' tool.
#
# '-I' = include directory.
# '-c++ -java' - languages to create interface.
# '-package' defines Java package for generated code 'com.goldberg.swigandroidguide.swiggenerated'.
# '-o' - location of generated C++/JNI file.
# '-outdir' - location of generated Java code directory.
# 'SaveAndSpend.i' - script input file.
swig -I${android_src_dir} -c++ -java -package com.goldberg.swigandroidguide.swiggenerated -o ${generated_cpp_file} -outdir ${generated_java_dir} SwigAndroidGuide.i

Run this script. If using git, you can see the new files (git diff):

# Our C++ code
AM app/src/main/cpp/ActivityModel.cpp
AM app/src/main/cpp/ActivityModel.h

# Our SWIG file and script
A  swig/SwigAndroidGuide.i
A  swig/run_swig.sh

# SWIG-generated Java code
A  app/src/main/java/com/goldberg/swigandroidguide/swiggenerated/ActivityModel.java

# SWIG-generated helper code
A  app/src/main/cpp/swig-generated/SwigAndroidGuide_wrap.cpp
A  app/src/main/java/com/goldberg/swigandroidguide/swiggenerated/SwigAndroidGuide.java
A  app/src/main/java/com/goldberg/swigandroidguide/swiggenerated/SwigAndroidGuideJNI.java

Java files are going to app/src/main/java/com/goldberg/swigandroidguide/swiggenerated directory. There are two helper Java files SwigAndroidGuide.java and SwigAndroidGuideJNI.java; and one Java file per processed C++ class, currently only the ActivityModel.java. Generated C++ helper code is going to app/src/main/cpp/swig-generated directory. Currently only one file generated - SwigAndroidGuide_wrap.cpp. In the next chapter you will get one more file there - SwigAndroidGuide_wrap.h. So usually there are two files. After running the SWIG script, we got the following Java code in ActivityModel.java. Note the generated Java class has SWIG internal helper code, but you can find our functions inside the class.

package com.goldberg.swigandroidguide.swiggenerated;

public class ActivityModel {

  //
  // This is SWIG helper code
  //
  
  private transient long swigCPtr;
  protected transient boolean swigCMemOwn;

  protected ActivityModel(long cPtr, boolean cMemoryOwn) {
    swigCMemOwn = cMemoryOwn;
    swigCPtr = cPtr;
  }

  protected static long getCPtr(ActivityModel obj) {
    return (obj == null) ? 0 : obj.swigCPtr;
  }

  @SuppressWarnings("deprecation")
  protected void finalize() {
    delete();
  }

  public synchronized void delete() {
    if (swigCPtr != 0) {
      if (swigCMemOwn) {
        swigCMemOwn = false;
        SwigAndroidGuideJNI.delete_ActivityModel(swigCPtr);
      }
      swigCPtr = 0;
    }
  }

  //
  // Here is our C++ functions!
  //
  
  public void setMultiplier(int multiplier) {
    SwigAndroidGuideJNI.ActivityModel_setMultiplier(swigCPtr, this, multiplier);
  }

  public int multiply(int a) {
    return SwigAndroidGuideJNI.ActivityModel_multiply__SWIG_0(swigCPtr, this, a);
  }

  public static int multiply(int a, int b) {
    return SwigAndroidGuideJNI.ActivityModel_multiply__SWIG_1(a, b);
  }

  public ActivityModel() {
    this(SwigAndroidGuideJNI.new_ActivityModel(), true);
  }
}

You can call this from Java as:

ActivityModel activityModel = new ActivityModel();
Log.d(TAG, "mathExample() ActivityModel.multiply(5, 42): " + ActivityModel.multiply(5, 42));
activityModel.setMultiplier(3);
Log.d(TAG, "mathExample() activityModel.multiply(5): " + activityModel.multiply(6));

If you run your project now, it will crash with an error:

java.lang.UnsatisfiedLinkError: No implementation found for long
com.goldberg.swigandroidguide.swiggenerated.SwigAndroidGuideJNI.new_ActivityModel()
(tried Java_com_goldberg_swigandroidguide_swiggenerated_SwigAndroidGuideJNI_new_1ActivityModel
and Java_com_goldberg_swigandroidguide_swiggenerated_SwigAndroidGuideJNI_new_1ActivityModel__)

That's because your C++ files were not added to the project. Go to CMakeLists.txt and add your ActivityModel.cpp file into the existing shared library:

# We placed siwg-generated C++ code into the different directory,
# than the other code. We have to include the directory where our "ActivityModel.h" is located.
# In my case it is the directory on "CMakeLists.txt" file, i.e. 'this' directory.

include_directories(./) # < Add this line

add_library(
        native-lib
        SHARED
        native-lib.cpp

        # Add lines below

        ActivityModel.cpp
        swig-generated/SwigAndroidGuide_wrap.cpp)

After sync (run Gradle Sync) you will be able to run your project. Log output is now correct:

D/MainActivity: simpleExample() ActivityModel.multiply(5, 42): 210
D/MainActivity: simpleExample() ActivityModel.multiply(5): 18

Hooray! The C++ code invocation is working. Now we've done basic SWIG integration.

After this is done, no need to add SWIG-generated files into the project manually. They are already added to the project - the single C++ file SwigAndroidGuide_wrap.cpp, which only modified by re-generation; and Java as a directory, so all newly generated Java files are included automatically.

If error is issued by SWIG, you will find SwigAndroidGuide_wrap.cpp file deleted and app/src/main/java/com/goldberg/swigandroidguide/swiggenerated/ directory empty. This is because our script run_swig.sh deletes all the generated files, and then SWIG generated them again. You have to fix the error and run SWIG again.

Calling Java from C++ (Directors)

To call Java from C++ we have to use SWIG directors. SWIG director is a feature allowing cross-language polymorphism. In simpler language, with directors, Java classes can be 'inherited' from the C++ classes. Under the hood, SWIG creates a Java class, that has the same methods as a C++ class, and generates code to bind them; then you inherit your Java implementation class from the SWIG-generated Java class.

Let's create IAndroidActivity class in app/src/main/cpp/IAndroidActivity.h to allow C++ to control Android Activity.

class IAndroidActivity
{
public:
    // Required. Otherwise SWIG will issue warning.
    virtual ~IAndroidActivity() = default;

    virtual void showToast(std::string value) = 0;
};

To call this from C++, let's add a new function into ActivityModel class in app/src/main/cpp/ActivityModel.h (the same way as in previous chapter):

class ActivityModel
{
public:
    void onCreate(IAndroidActivity* androidActivity);
    // ...

And then call this function in app/src/main/cpp/ActivityModel.cpp:

void ActivityModel::onCreate(IAndroidActivity* androidActivity)
{
    androidActivity->showToast("Toast from C++");
}

In the SWIG interface file we should enable directors to allow Java inheritance of C++ classes. SwigAndroidGuide.i:

// Enabling directors:
// %module SwigAndroidGuide // Delete this line
%module(directors="1") SwigAndroidGuide // Add this line

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

%include "ActivityModel.h"

%feature("director") IAndroidActivity; // Add this line
%include "IAndroidActivity.h" // Add this line

Re-run SWIG script now (swig/run_swig.sh). If you run git diff after this, you will notice the new file with the strange name: app/src/main/java/com/goldberg/swigandroidguide/swiggenerated/SWIGTYPE_p_std__string.java Prefix SWIGTYPE_p_ means SWIG has not enough information to process a particular type. _std__string.java means std::string was not processed properly. SWIG can process only types integral to C++ by default, like int, char*, etc. But SWIG instructions for most of the std types and containers SWIG are already written. They reside in the SWIG include directory (where the SWIG program is installed). To properly process std::string include std_string.i for processing. Modify your SWIG file SwigAndroidGuide.i:

%module(directors="1") SwigAndroidGuide

%include <std_string.i> // < Add this line

%{
#include "ActivityModel.h"
// ...

Re-run SWIG script swig/run_swig.sh. Notice IAndroidActivity class is added in your Java folder. Let's create a Java implementation for our C++ interface. We can't extend our MainActivity from IAndroidActivity, as it already extends an AppCompatActivity. Unfortunately, it's impossible to create a pure Java interface with SWIG. Let's create an inner class, and implement the showToast(String text) method in it.

public class MainActivity extends AppCompatActivity
{
    // ...
    private class AndroidActivity extends IAndroidActivity
    {
        @Override
        public void showToast(String text)
        {
            Log.d(TAG, "showToast() Will show toast with text: " + text);
            Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT).show();
        }
    }

Now we can call C++ code that will callback to showToast(String text).

public class MainActivity extends AppCompatActivity
{
    private final ActivityModel activityModel = new ActivityModel();
    private final AndroidActivity androidActivity = new AndroidActivity();

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        activityModel.onCreate(androidActivity);
        // ...

Voilà! Now, when you run the application, you will see the toast with text sent from C++. And log: D/MainActivity: showToast() Will show toast with text: Toast from C++

Shared pointer to an object

In many cases, you may want to use std::shared_ptr while developing in C++. With SWIG it is possible and very simple to work with. Let's create C++ class Message in app/src/main/cpp/Message.h.

class Message
{
public:
    virtual ~Message() = default;

    int getId() { return _id; }
    void setId(int id) { _id = id; }

    std::string getText() { return _text; }
    void setText(std::string text) { _text = text; }

private:
    int _id;
    std::string _text;
};

And use shared_ptr<message> inIAndroidActivity.h`:

class IAndroidActivity
{
public:
    virtual ~IAndroidActivity() = default; // Required. Otherwise SWIG will issue warning.
    virtual void showToast(std::string text) = 0;

    // Add this line
    virtual void sendMessage(std::shared_ptr<Message> message) = 0;

And call it from C++, in ActivityModel.cpp:

void ActivityModel::onCreate(IAndroidActivity *androidActivity)
{
    // ...
    shared_ptr<Message> message = make_shared<Message>();
    message->setId(22);
    message->setText("Greetings!");
    androidActivity->sendMessage(message);
}

Then, in SWIG file SwigAndroidGuide.i:

%{
#include "ActivityModel.h"
#include "Message.h" // << Add this line
#include "IAndroidActivity.h"
%}

%include "ActivityModel.h"

// Add these lines
%shared_ptr(SwigAndroidGuide::Message); // Tell SWIG to process shared_ptr of this type
%include "Message.h" // Process file

%feature("director") IAndroidActivity;
%include "IAndroidActivity.h"

Now, when you re-run SWIG, it will generate proper interfaces for the shared_ptr argument - shared_ptr<Message>. Let's implement it in Java, in MainActivity.java:

public class MainActivity extends AppCompatActivity
{
    // ...
    private class AndroidActivity extends IAndroidActivity
    {
        // ...
        @Override
        public void sendMessage(Message message)
        {
            Log.d(TAG, String.format(Locale.US, "Message sent: '%d %s'",
                                     message.getId(), message.getText()));
        }

Run the application, and you'll get proper log output: D/MainActivity: Message sent: '22 Greetings!'

C++ std:: containers

As we were done wrapping of std::string in the previous section, we can do with most (if not any) of C++ standard library containers. Let's try on an example of std::vector. This applies to all other containers.

Create new file app/src/main/cpp/Types.h:

typedef unsigned char byte;

Create new file app/src/main/cpp/PhotoMessage.h:

#include "Types.h"

class PhotoMessage : public Message
{
public:
    std::vector<byte> getPhotoData() { return _photoData; }
    void setPhotoData(std::vector<byte> photoData) { _photoData = photoData; }

private:
    std::vector<byte> _photoData;
};

Create a function in IAndroidActivity.h to use our new class:

class IAndroidActivity
{
public:
    virtual ~IAndroidActivity() = default;
    virtual void showToast(std::string text) = 0;
    virtual void sendMessage(std::shared_ptr<Message> message) = 0;

    // Add this line
    virtual void sendPhotoMessage(std::shared_ptr<PhotoMessage> message) = 0;

And use it from C++, in ActivityModel.cpp.

void sendPhotoMessage(IAndroidActivity *androidActivity)
{
    shared_ptr<PhotoMessage> photoMessage = make_shared<PhotoMessage>();
    photoMessage->setId(27);
    photoMessage->setText("PhotoMessage!");

    vector<byte> photoData;
    photoData.push_back((byte)78);
    photoData.push_back((byte)64);
    photoData.push_back((byte)35);
    photoMessage->setPhotoData(photoData);

    androidActivity->sendPhotoMessage(photoMessage);
}

void ActivityModel::onCreate(IAndroidActivity *androidActivity)
{
    // ...
    sendPhotoMessage(androidActivity);
}

For std::vector we must specify the template we want SWIG to generate wrapper to. In our case %template(VectorByte) std::vector<byte>. The unique template name should be passed in a %template(NAME) argument. Modify SWIG file SwigAndroidGuide.i:

%module(directors="1") SwigAndroidGuide

// Process our Types.h first, so SWIG will be able to understand our types.
%include "Types.h" // << Add this line

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

%template(VectorByte) std::vector<byte>; // << Add this line

%{
#include "Types.h"
#include "Message.h"
#include "PhotoMessage.h" // << Add this line
#include "ActivityModel.h"
#include "IAndroidActivity.h"
%}

%shared_ptr(SwigAndroidGuide::Message);
%include "Message.h"

// Add these lines >>
%shared_ptr(SwigAndroidGuide::PhotoMessage);
%include "PhotoMessage.h"

%include "ActivityModel.h"

%feature("director") IAndroidActivity;
%include "IAndroidActivity.h"

Now run SWIG script swig/run_swig.sh, note it will generate Java class VectorByte (as we specified in %template(NAME)). See, it mirrors the std::vector functions. They now can be called from Java. Also note, the generated Java class is templated for type short. This is just because our C++ byte equals unsigned char, and the latter maps to Java type short. For other types, you will not get such a problem. The problem with this 'default' implementation of containers with SWIG - it allows only per-element access. While this is ok when you having vector<Message>; but this is a bottleneck, if you have to pass big amounts of data through vector<byte>, as accessing each byte will be a dedicated JNI call. We will address both problems in later chapter (Part 2 : byte[][]). Generated VectorByte.java:

public class VectorByte extends java.util.AbstractList<short> implements java.util.RandomAccess {
// ...
  public VectorByte(short[] initialElements) {
  //...
  }

  public VectorByte(Iterable<short> initialElements) {
  //...
  }

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

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

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

  public void add(int index, Short e) {
    modCount++;
    doAdd(index, e);
  }

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

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

  public long capacity() {
    return SwigAndroidGuideJNI.VectorByte_capacity(swigCPtr, this);
  }

  public void reserve(long n) {
    SwigAndroidGuideJNI.VectorByte_reserve(swigCPtr, this, n);
  }

  public void clear() {
    SwigAndroidGuideJNI.VectorByte_clear(swigCPtr, this);
  }
// ...
}

Now let's make Java implementation in MainActivity.java:

public class MainActivity extends AppCompatActivity
{
    // ...
    private class AndroidActivity extends IAndroidActivity
    {
        // ...
        @Override
        public void sendPhotoMessage(PhotoMessage message)
        {
            VectorByte data = message.getPhotoData();
            String dataString = "";
            for (int i = 0; i < data.size(); ++i)
            {
                dataString += data.get(i).toString() + " ";
            }

            Log.d(TAG, String.format(Locale.US,
                    "Photo message sent: '%d %s' with bytes: '%s'",
                    message.getId(), message.getText(), dataString));
        }

Run an application, and you will see the proper log output: D/MainActivity: Photo message sent: '27 PhotoMessage!' with bytes: '78 64 35 '

Type downcast

In the previous document section, we have created a class PhotoMessage inherited from Message And we created a separate function for PhotoMessage to be passed to Java:

class IAndroidActivity
{
public: // ...
    virtual void sendMessage(std::shared_ptr<Message> message) = 0;
    virtual void sendPhotoMessage(std::shared_ptr<PhotoMessage> message) = 0;

We did this because downcasting by the inheritance tree of C++ doesn't work in Java. See example. Let's pass child object PhotoMessage to a function expecting base type Message:

void sendPhotoMessage(IAndroidActivity *androidActivity)
{
    shared_ptr<PhotoMessage> photoMessage = make_shared<PhotoMessage>();
    // ... initializing photoMessage ...

    // Pass child object to the function expecting the base type
    // sendMessage(shared_ptr<message>)
    androidActivity->sendMessage(photoMessage);
}

In Java, you will get the object of type Message, which is legit, but you cannot downcast it, even if the underlying C++ object is of child type PhotoMessage.

@Override
public void sendMessage(Message message)
{
    if (message instanceof PhotoMessage)
    {
        // Always false
    }

    // Always will throw ClassCastException
    PhotoMessage photoMessage = (PhotoMessage) message;
}

So, how to we can enable downcasts in Java? SWIG documentation tells us how to support type downcasts - by extending our classes with function, that we can call from Java, and which will implement C++ dynamic_cast functionality. In SWIG code this looks very simple:

%extend SwigAndroidGuide::PhotoMessage {
    static std::shared_ptr<SwigAndroidGuide::PhotoMessage> dynamic_cast(std::shared_ptr<SwigAndroidGuide::Message> message) {
        return std::dynamic_pointer_cast<SwigAndroidGuide::PhotoMessage>(message);
    }
};

%extend means your class will get an extra function. %extend unfolds to a member function in Java, which internally calls static C++ function (non-member of your C++ class) with code we put there in %extend definition. Add the code from the above snippet to the bottom of your SWIG file swig/SwigAndroidGuide.i. Then run SWIG generation swig/run_swig.sh. See that PhotoMessage got and extra function in PhotoMessage.java:

public class PhotoMessage extends Message {
  // ...
  public static PhotoMessage dynamic_cast(Message message) {
    long cPtr = SwigAndroidGuideJNI.PhotoMessage_dynamic_cast(Message.getCPtr(message), message);
    return (cPtr == 0) ? null : new PhotoMessage(cPtr, true);
  }

And we are looking the first time into app/src/main/cpp/swig-generated/SwigAndroidGuide_wrap.cpp. Just to get an idea of how the SWIG extension %extend interacts with our code.

// This function is generated as we specified in %extend

SWIGINTERN std::shared_ptr&lt; SwigAndroidGuide::PhotoMessage > SwigAndroidGuide_PhotoMessage_dynamic_cast(std::shared_ptr&lt; SwigAndroidGuide::Message > arg){
        return std::dynamic_pointer_cast<swigandroidguide::photomessage>(arg);
    }

// ...

// SWIG connects our new function to Java

SWIGEXPORT jlong JNICALL Java_com_goldberg_swigandroidguide_swiggenerated_SwigAndroidGuideJNI_PhotoMessage_1dynamic_1cast(JNIEnv *jenv, jclass jcls, jlong jarg1, jobject jarg1_) {
  jlong jresult = 0 ;
  std::shared_ptr&lt; SwigAndroidGuide::Message > arg1 ;
  std::shared_ptr&lt; SwigAndroidGuide::Message > *argp1 ;
  std::shared_ptr&lt; SwigAndroidGuide::PhotoMessage > result;

  (void)jenv;
  (void)jcls;
  (void)jarg1_;
  argp1 = *(std::shared_ptr&lt; SwigAndroidGuide::Message > **)&amp;jarg1;
  if (argp1) arg1 = *argp1; 
  
  // SWIG connects our new function to Java in this line
  result = SwigAndroidGuide_PhotoMessage_dynamic_cast(arg1);
  
  *(std::shared_ptr&lt; SwigAndroidGuide::PhotoMessage > **)&amp;jresult = result ? new std::shared_ptr&lt; SwigAndroidGuide::PhotoMessage >(result) : 0; 
  return jresult;
}

Now, remove the specific function IAndroidActivity::sendPhotoMessage(shared_ptr<photomessage>) from C++ and its implementation in Java and re-run SWIG script swig/run_swig.sh again. Alter implementation of IAndroidActivity.sendMessage in Java, as now it supports polymorphic arguments. But the typecasting logic works as in C++ (not like 'standard' Java) - you doing dynamic_cast, and then checking the argument for null, as in the following code from MainActivity.java.

public class MainActivity extends AppCompatActivity
{
    // ...
    private class AndroidActivity extends IAndroidActivity
    {
        // ...
        @Override
        public void sendMessage(Message message)
        {
            PhotoMessage photoMessage = PhotoMessage.dynamic_cast(message);
            if (photoMessage != null)
            {
                Log.d(TAG, String.format(Locale.US, "Photo message sent: '%d %s, %d bytes'",
                        photoMessage.getId(), photoMessage.getText(), photoMessage.getPhotoData().size()));

                return;
            }

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

And in ActivityModel.cpp:

void sendPhotoMessage(IAndroidActivity *androidActivity)
{
    // ...
    // androidActivity->sendPhotoMessage(photoMessage); // Delete this line
    androidActivity->sendMessage(photoMessage); // Add this line
}

Run the application and check that log is valid:

D/MainActivity: Message sent: '22 Greetings!'
D/MainActivity: Photo message sent: '27 PhotoMessage!' with bytes: '78 64 35 '

The problem with this chunk of SWIG code - we must copy it for all types we want to support downcast. The solution to the problem is SWIG macro %define. It unfolds just like C++ macro.

%define %dynamic_cast_extension(CppNamespace, DescendantClass, BaseClass)
%extend CppNamespace::DescendantClass
{
    static std::shared_ptr<CppNamespace::DescendantClass>
    dynamic_cast(std::shared_ptr<CppNamespace::BaseClass> arg)
    {
        return std::dynamic_pointer_cast<CppNamespace::DescendantClass>(arg);
    }
};
%enddef

Put this into the new file swig/dynamic_cast_extension.i. We still need to unfold this macro for each type, but now it is a one-liner. Do changes in your SwigAndroidGuide.i:

// ...
%include <std_string.i>
%include <std_vector.i>
%include "dynamic_cast_extension.i" // Add this line
// ...

// Remove this chunk
/*
%extend SwigAndroidGuide::PhotoMessage {
    static std::shared_ptr<swigandroidguide::photomessage> dynamic_cast(std::shared_ptr<swigandroidguide::message> message) {
        return std::dynamic_pointer_cast<swigandroidguide::photomessage>(message);
    }
};
*/

%dynamic_cast_extension(SwigAndroidGuide, PhotoMessage, Message) // Add this line

Now much better. Just re-run SWIG run_swig.sh and verify the application is working properly.

Conclusion

Now we dismantled the SWIG basics in Android and simple cases. It looks very tangled and confusing at the beginning, but after small practice, everything falls into place. I invite you to Part 2 of the guide, where we talk about peculiar/weird cases, typemaps, and debugging.

Resources

Tags:
Hubs:
+3
Comments 0
Comments Leave a comment

Articles