Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Return an array of java objects using SWIG

Tags:

java

c++

swig

I am writing a JNI wrapper for a C++ library using SWIG. One of the methods in a library returns an array of structs in an allocated memory:

typedef struct {
    int id;
    double x;
    double y;
} DataStruct;

int get_all_data ( long ref, DataStruct **ppdata, size_t *psize ) {
    // ... prepare the data by the ref
    *ppdata = (DataStruct*) malloc(sizeof(DataStruct) * size);
    *psize = size;
    return 0;
}

Method signature in java should look something like this:

 native DataStruct[] get_all_data(long ref);

Thus I am expecting SWIG to generate java DataStruct in java and wrapper that would call the library method, then create an jarray of DataStruct in JVM and fill it with DataStruct objects initialized from DataStruct structs returned by the library and finally free the allocated memory pointed by ppdata.

I've been trying to wrap my head around SWIG typemaps for some time and the only solution I see for now is to use %native directive and create JNI implementaion fully manually. Can I get at least some help from SWIG in this case?

like image 220
senyacap Avatar asked Oct 19 '22 10:10

senyacap


1 Answers

The main thing you're looking for here are two features of typemaps. Firstly you want multi argument typemaps to be able to work with arguments that are logically grouped. Secondly you want argout typemaps to have a chance at converting arguments into outputs.

There are a few different ways to write such interfaces inside SWIG, with the main trade-offs covering quantity of C (JNI) vs actual Java that needs to be written. I've put together one complete example:

%module test

%{
#include <assert.h>
%}

%typemap(in,numinputs=0) (DataStruct **ppdata, size_t *psize) (size_t tempsize, DataStruct *tempdata) %{
  $2 = &tempsize;
  $1 = &tempdata;
%}

%typemap(jtype) int get_all_data "long[]";
%typemap(jstype) int get_all_data "DataStruct[]";
%typemap(javaout) int get_all_data {
    final long[] arr = $jnicall;
    DataStruct[] ret = new DataStruct[arr.length];
    for (int i = 0; i < arr.length; ++i) {
      ret[i] = new DataStruct(arr[i], false);
    }
    return ret;
  }

%typemap(jni) int get_all_data "jlongArray";

%typemap(out) int get_all_data %{
  // Handle errors in the int return value ($1)
  assert(!$1);
%}

%typemap(argout) (DataStruct **ppdata, size_t *psize) {
  $result = JCALL1(NewLongArray, jenv, *$2);
  jlong temparr[*$2];
  for (size_t i = 0; i < *$2; ++i) {
    *(DataStruct**)&temparr[i] = &((*$1)[i]);
  }
  JCALL4(SetLongArrayRegion, jenv, $result, 0, *$2, temparr);
}

%inline %{
typedef struct {
    int id;
    double x;
    double y;
} DataStruct;

int get_all_data ( long ref, DataStruct **ppdata, size_t *psize ) {
    static const size_t size = 4;
    *ppdata = (DataStruct*) malloc(sizeof(DataStruct) * size);
    *psize = size;
    for (size_t  i = 0; i < size; ++i) {
      DataStruct val = {i,i,i};
      (*ppdata)[i] = val;
    }
    return 0;
}
%}

This does a few things:

  1. Converts the return type of get_all_data to be an array. Internally it's an array of longs that represent pointers to DataStruct instances but the visible part of it is an array of DataStruct objects that get constructed.
  2. The input typemap creates temporaries to be used as placeholders for the output arguments.
  3. Once the call actually happens we loop over the array and store the addresses of the members into the Java array. This is important because the array is an array of objects, not an array of pointers. SWIG's generated proxy objects function in relation to pointers to individual objects so we need to get those from somewhere. We can't compute them inside Java either because exposing the sizeof information needed to do so correctly from just the raw output pointer is somewhat tricky so this is a good compromise. (It also means the same code could work if you switched to e.g. linked lists in the future with only small changes).

With that in place I can run my test:

public class run {
  public static void main(String[] argv) {
    System.loadLibrary("test");
    DataStruct[] result = test.get_all_data(0);
    for (int i = 0; i < result.length; ++i) {
      System.out.println(result[i].getId());
    }
  }
}

This runs as:

swig2.0 -Wall -java -c++ test.i
gcc -Wall -Wextra -shared -o libtest.so -I/usr/lib/jvm/default-java/include -I/usr/lib/jvm/default-java/include/linux test_wrap.cxx
javac run.java
LD_LIBRARY_PATH=. java run
0
1
2
3
like image 82
Flexo Avatar answered Oct 21 '22 23:10

Flexo