SWIG从java中的String as String数组中获取返回类型

时间:2016-05-16 12:04:35

标签: java c arrays string swig

对于一个小型Java项目,我需要与用C编写的现有代码进行交互,所以为了简单起见(不幸的是我不是C / C ++程序员......)我决定使用swig。

生成的包装器代码似乎有效;但是,当我调用一个应该给我一个以NULL分隔的字符串列表的函数时(这就是C函数应该返回的内容,如果我没有弄错的话),包装的代码只返回预期的第一个String值价值清单。我假设Java中正确的返回数据类型是String Array而不是String?这个假设是否正确,可以通过在swig接口文件中指定typemap来处理吗?或者,我走错了路?

C头文件中的函数声明:

DllImport char *GetProjects dsproto((void));

生成的JNI java文件:

public final static native String GetProjects();

非常感谢任何帮助/指示!

1 个答案:

答案 0 :(得分:3)

解决方案1 ​​ - Java

您可以通过多种方式在SWIG中解决此问题。我已经开始使用一个解决方案,只需要你编写一些Java(在SWIG界面内)并自动应用,使你的函数返回String[]所需的语义。

首先,我编写了一个小的test.h文件,让我们可以使用我们正在努力的字体图:

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

没有什么特别的,只有一个函数将多个字符串拆分为一个并以double \0结尾(最后一个隐含在C中的字符串常量中)。

然后我写了下面的SWIG接口来包装它:

%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"

基本上它的作用是使用carrays.i SWIG库文件来公开一些函数,这些函数可以让我们像C中的原始指针一样获取,设置和删除数组。由于SWIG特殊情况char *默认情况下虽然我们必须打破这个功能我们正在查看%apply,因为我们不希望发生这种情况。使用signed char获取数组函数可以获得我们想要的结果:在Java中映射到Byte而不是String

jstype typemap只是将生成的函数返回类型更改为我们想要的类型:String[]。 javaout类型映射解释了我们如何从JNI调用返回的转换(long,因为我们故意阻止它被包装为普通的空终止字符串),而是使用了一些额外的Java我们在模块(pptr2array)内写道,为我们做这项工作。

pptr2array内部,我们基本上逐字节地将每个输出数组构建到每个String中。我使用了ArrayList,因为我宁可动态地增长它,而不是在输出上进行两次传递。使用ByteArrayOutputStream是一种逐字节构建字节数组的简洁方法,它有两个主要优点:

  1. 多字节unicode可以像这样正常工作。这与将每个字节转换为char并单独附加到String(Builder)相反。
  2. 我们可以为每个字符串重复使用相同的ByteArrayOutputStream,这样可以重用缓冲区。在这种规模上并不是真正的交易破坏者,但从第1天开始就没有造成任何伤害。
  3. 还需要注意的一点是:为了正确设置$owner,并指出我们是否期望从{C}函数返回的内存free(),您需要使用%newobject。请参阅discussion of $owner in docs

    解决方案2 - JNI

    如果您愿意,您可以编写几乎相同的解决方案,但完全使用类型映射来代替进行一些JNI调用:

    %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"
    

    我们在这里完成了同样的事情,但又添加了3个类型图。 jtype和jnitype类型映射告诉SWIG生成的JNI代码和相应的native函数将返回的返回类型分别为Java和C(JNI)类型。 javaout类型映射变得更简单,只需将String[]直接传递为String[]

    然而,in typemap是工作发生的地方。我们在本机代码中分配String[]的Java数组。这是通过进行第一遍来简单计算有多少元素来完成的。 (在C中的一次传球中没有简洁的方法)。然后在第二遍中,我们调用NewStringUTF并将其存储在我们之前创建的输出数组对象中的正确位置。所有JNI调用都使用SWIG特定的JCALLx macros,这使它们可以在C和C ++编译器中工作。这里没有实际需要使用它们,但这并不是一个坏习惯。

    如果需要,所有剩下要做的事情都是免费的。 (在我的示例中,它是一个const char*字符串文字,所以我们不会释放它。)

    解决方案3 - C

    当然,如果您只想写C,您也可以获得解决方案。我在这里概述了一种可能性:

    %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"
    

    请注意,在此实例中,解决方案会更改从包装代码中公开的GetThings()的返回类型。它现在返回一个中间类型,它只存在于包装器StrArrHandle中。

    这种新类型的目的是揭示您使用真实函数给出的所有答案所需的额外功能。我这样做是通过声明和定义使用%inline一个额外的函数来包装对GetThings()的实际调用,以及一个额外的类型,它保存它返回的指针供我们以后使用。

    我使用%ignore%rename仍然声称我的包装函数被称为GetThings(即使它不是为了避免生成的C代码中的名称冲突)。我本可以跳过%ignore并且根本没有在文件底部添加%include,但是基于这样的假设:在现实世界中,头文件中可能还有更多内容,您也希望包装这个例子可能更有用。

    使用%extend我们可以向我们创建的包装器类型添加一个方法,该方法返回当前字符串(如果不在结尾处)并前进光标。如果您有责任免费使用原始功能的返回值,那么您也希望保留原始功能的副本,并使用%extend添加&#39;析构函数&#39;当对象被垃圾收集时,SWIG调用。

    我告诉SWIG不允许用户使用StrArrHandle构建%nodefaultctor对象。 SWIG将为str StrArrHandle成员生成一个getter。 %immutable阻止它生成一个setter,这根本就没有意义。您可以使用%ignore忽略它,或者将StrArrHandle拆分出来而不是使用%inline,而只是不告诉SWIG该成员。

    现在,您可以使用以下内容从Java调用它:

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

    如果你想,你可以将它与解决方案#1的部分结合起来,以便返回一个Java数组。您希望为此添加两个类型图,靠近顶部:

    %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()]);
    }
    

    与解决方案1的结果几乎相同,但方式却截然不同。