SWIG get returntype from String as String array in java

一个人想着一个人 提交于 2020-02-04 06:36:26

问题


For a small Java project I needed to interact with existing code written in C, so to make things easy (I'm not a C/C++ programmer unfortunately..) I decided to use swig.

The generated wrapper code seems to work; however, when I call a function that is supposed to give me a NULL-delimited list of strings (That's what the C function is supposed to return, if I'm not mistaken) the wrapped code only returns the first String value of the expected list of values. I assume the correct return datatype in Java would be a String Array instead of a String? Is this assumption correct and can this be handled by specifying a typemap in the swig interface file? Or, am I on the wrong track?

The function in the C header file states:

DllImport char *GetProjects dsproto((void));

The resulting JNI java file:

public final static native String GetProjects();

Any help/pointers would be greatly appreciated!


回答1:


Solution 1 - Java

There are a bunch of different ways you can solve this problem in SWIG. I've started out with a solution that just requires you to write a little more Java (inside the SWIG interface) and that automatically gets applied to make your function return String[] with the semantics you desire.

First up I wrote a small test.h file that lets us exercise the typemaps we're working towards:

static const char *GetThings(void) {
  return "Hello\0World\0This\0Is\0A\0Lot\0Of Strings\0";
}

Nothing special just a single function that splits multiple strings into one and terminates with a double \0 (the last one is implicit in string constants in C).

I then wrote the following SWIG interface to wrap it:

%module test
%{
#include "test.h"
%}

%include <carrays.i>

%array_functions(signed char, ByteArray);

%apply SWIGTYPE* { const char *GetThings };

%pragma(java) moduleimports=%{
import java.util.ArrayList;
import java.io.ByteArrayOutputStream;
%}

%pragma(java) modulecode=%{
static private String[] pptr2array(long in, boolean owner) {
  SWIGTYPE_p_signed_char raw=null;
  try {
    raw = new SWIGTYPE_p_signed_char(in, owner);
    ArrayList<String> tmp = new ArrayList<String>();
    int pos = 0;
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    while (ByteArray_getitem(raw, pos) != 0) {
      byte c;
      while ((c = ByteArray_getitem(raw, pos++)) != 0) {
        bos.write(c);
      }
      tmp.add(bos.toString());
      bos.reset();
    }
    return tmp.toArray(new String[tmp.size()]);
  }
  finally {
    if (owner && null != raw) {
      delete_ByteArray(raw);
    }
  }
}
%}

%typemap(jstype) const char *GetThings "String[]";
%typemap(javaout) const char *GetThings {
  return pptr2array($jnicall, $owner);
}

%include "test.h"

Essentially what that does is use the carrays.i SWIG library file to expose a few functions that let us get, set and delete arrays exactly like a raw pointer in C would. Since SWIG special cases char * by default though we have to break that in the case of the function we're looking at with %apply since we don't want that happening. Using signed char for the array functions gets us what we want: a mapping to Byte in Java and not String.

The jstype typemap simply changes the resulting function return type to what we want it to be: String[]. The javaout typemap is explaining how we're doing a conversion from what the JNI call returns (a long since we deliberately stopped it getting wrapped as a normal null terminated string) and instead uses a bit of extra Java we wrote inside the module (pptr2array) to do that work for us.

Inside pptr2array we're essentially building up our output array byte by byte into each String. I used an ArrayList because I'd rather grow it dynamically than make two passes over the output. Using a ByteArrayOutputStream is a neat way to build a Byte array byte by byte, which has two main advantages:

  1. Multibyte unicode can work correctly like this. This is in contrast to casting each byte to char and appending to a String(Builder) individually.
  2. We can re-use the same ByteArrayOutputStream for each string, which lets the buffer get reused. Not really a deal breaker at this scale, but no harm done by doing it from day 1.

One more point to note: in order for $owner to be set correctly and indicate if we're expected to free() the memory returned from the C function you'll need to use %newobject. See discussion of $owner in docs.


Solution 2 - JNI

If you prefer you can write almost the same solution, but entirely in typemaps making a few JNI calls instead:

%module test
%{
#include "test.h"
#include <assert.h>
%}

%typemap(jni) const char *GetThings "jobjectArray";
%typemap(jtype) const char *GetThings "String[]";
%typemap(jstype) const char *GetThings "String[]";
%typemap(javaout) const char *GetThings {
  return $jnicall;
}
%typemap(out) const char *GetThings {
  size_t count = 0;
  const char *pos = $1;
  while (*pos) {
    while (*pos++); // SKIP
    ++count;
  }
  $result = JCALL3(NewObjectArray, jenv, count, JCALL1(FindClass, jenv, "java/lang/String"), NULL);
  pos = $1;
  size_t idx = 0;
  while (*pos) {
    jobject str = JCALL1(NewStringUTF, jenv, pos);
    assert(idx<count);
    JCALL3(SetObjectArrayElement, jenv, $result, idx++, str);
    while (*pos++); // SKIP
  }
  //free($1); // Iff you need to free the C function's return value
}

%include "test.h"

Here we've done essentially the same thing, but added 3 more typemaps. The jtype and jnitype typemaps tell SWIG what return types the generated JNI code and corresponding native function is going to return, as Java and C (JNI) type respectively. The javaout typemap get simpler, all it does is pass a String[] straight through as a String[].

The in typemap however is where the work happens. We allocate a Java array of String[] in the native code. This is done by making a first pass to simply count how many elements there are. (There's no neat way of doing this in one pass in C). Then in a second pass we call NewStringUTF and store that into the right place in the output array object we created previously. All of the JNI calls use the SWIG specific JCALLx macros which allow them to work in both C and C++ compilers. There's no actual need to use them here, but it's not a bad habit to get into.

All that remains to be done then is free the result the function returned if required. (In my example it's a const char* string literal so we don't free it).


Solution 3 - C

Of course if you prefer to just write C you can also get a solution. I've outlined one such possibility here:

%module test
%{
#include "test.h"
%}

%rename(GetThings) GetThings_Wrapper;
%immutable;
%inline %{
  typedef struct {
    const char *str;
  } StrArrHandle;

  StrArrHandle GetThings_Wrapper() {
    const StrArrHandle ret = {GetThings()};
    return ret;
  }
%}
%extend StrArrHandle {
  const char *next() {
    const char *ret = $self->str;
    if (*ret)
      $self->str += strlen(ret)+1;
    else
      ret = NULL;
    return ret;
  }
}

%ignore GetThings;
%include "test.h"

Note that in this instance the solution changes the return type of GetThings() as exposed from your wrapped code. It now returns an intermediate type, that only exists in the wrapper, StrArrHandle.

The purpose of this new type is to expose the extra functionality you need to work with all the answer given from your real function. I did this by declaring and defining using %inline an extra function that wraps the real call to GetThings() and an extra type that holds the pointer it returned for us to work with later.

I used %ignore and %rename to still claim that my wrapped function was called GetThings (even though it's not to avoid name clashes inside the generated C code). I could have skipped the %ignore and simply not added %include at the bottom of the file, but based on the assumption that in the real world there are likely more things inside the header file that you also wanted to wrap this example is probably more useful.

Using %extend we can then add a method to the wrapper type we created, which returns the current string (if not at the end) and advances the cursor. If it's your responsibility to free the original function's return value you'd want to keep a copy of that too and use %extend to add a 'destructor' for SWIG to call when the object gets garbage collected.

I told SWIG not to allow users to construct the StrArrHandle object with %nodefaultctor. SWIG will generate a getter for the str member of StrArrHandle. The %immutable prevents it from generating a setter, which would make no sense here at all. You could have just ignored it with %ignore or splitting StrArrHandle out instead of using %inline and simply not telling SWIG about that member.

Now with this you can call it from Java using something like:

StrArrHandle ret = test.GetThings();
for (String s = ret.next(); s != null; s = ret.next()) {
  System.out.println(s);
}

If you wanted to though you could combine this with parts of solution #1 in order to return a Java array. You'd want to add two typemaps for that, near the top:

%typemap(jstype) StrArrHandle "String[]";
%typemap(javaout) StrArrHandle {
  $javaclassname tmp = new $javaclassname($jnicall, $owner);
  // You could use the moduleimports pragma here too, this is just for example
  java.util.ArrayList<String> out = new java.util.ArrayList<String>();
  for (String s = tmp.next(); s != null; s = tmp.next()) {
    out.add(s);
  }
  return out.toArray(new String[out.size()]);
}

Which has pretty much the same result as solution 1, but in a very different way.



来源:https://stackoverflow.com/questions/37253529/swig-get-returntype-from-string-as-string-array-in-java

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!