SWIG 从字符串中获取返回类型作为 java 中的字符串数组

SWIG get returntype from String as String array in java

对于一个小型 Java 项目,我需要与用 C 编写的现有代码进行交互,以便让事情变得简单(不幸的是,我不是 C/C++ 程序员..)我决定使用痛饮。

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

C 头文件中的函数声明:

DllImport char *GetProjects dsproto((void));

生成的 JNI java 文件:

public final static native String GetProjects();

任何 help/pointers 将不胜感激!

解决方案 1 - Java

在 SWIG 中有许多不同的方法可以解决这个问题。我已经开始使用一个解决方案,它只需要您多写一点 Java(在 SWIG 界面内),它会自动应用于使您的函数 return String[] 具有语义你想要的。

首先,我写了一个小 test.h 文件,让我们练习我们正在努力的类型映射:

static const char *GetThings(void) {
  return "Hello[=10=]World[=10=]This[=10=]Is[=10=]A[=10=]Lot[=10=]Of Strings[=10=]";
}

没什么特别的,只是一个将多个字符串拆分为一个并以双 [=17=] 终止的函数(最后一个隐含在 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 类型映射只是将生成的函数 return 类型更改为我们想要的类型:String[]。 javaout 类型映射解释了我们如何从 JNI 调用的 returns 进行转换(一个 long 因为我们故意阻止它被包装为一个普通的 null 终止字符串)而是使用了一些我们在模块 (pptr2array) 中编写了额外的 Java 来为我们完成这项工作。

pptr2array 中,我们实质上是将输出数组逐字节构建到每个字符串中。我使用了 ArrayList 因为我宁愿动态增长它也不愿对输出进行两次传递。使用 ByteArrayOutputStream 是一种逐字节构建字节数组的巧妙方法,它有两个主要优点:

  1. 多字节 unicode 可以像这样正常工作。这与将每个字节转换为 char 并单独附加到 String(Builder) 形成对比。
  2. 我们可以为每个字符串重复使用相同的 ByteArrayOutputStream,这样可以重复使用缓冲区。在这种规模下并不是真正的交易破坏者,但从第一天开始就不会造成任何伤害。

还有一点需要注意:为了正确设置 $owner 并指明我们是否需要 free() 从 C 函数中 return 编辑内存,您我将需要使用 %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 = ;
  while (*pos) {
    while (*pos++); // SKIP
    ++count;
  }
  $result = JCALL3(NewObjectArray, jenv, count, JCALL1(FindClass, jenv, "java/lang/String"), NULL);
  pos = ;
  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(); // Iff you need to free the C function's return value
}

%include "test.h"

这里我们基本上做了同样的事情,但又添加了 3 个类型映射。 jtype 和 jnitype 类型映射告诉 SWIG 什么 return 类型生成的 JNI 代码和相应的 native 函数将 return,分别作为 Java 和 C (JNI) 类型。 javaout 类型映射变得更简单,它所做的只是将 String[] 作为 String[].

直接传递

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

所有剩下要做的就是释放函数 returned 的结果(如果需要)。 (在我的示例中,它是一个 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() 的 return 类型,如从包装代码中公开的那样。它现在 return 是一个中间类型,只存在于包装器 StrArrHandle 中。

这种新类型的目的是公开处理真实函数给出的所有答案所需的额外功能。我通过使用 %inline 声明和定义一个额外的函数来实现这一点,该函数包装了对 GetThings() 的真正调用,以及一个额外的类型来保存它 return 的指针供我们稍后使用。

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

然后我们可以使用 %extend 向我们创建的包装器类型添加一个方法,该方法 return 是当前字符串(如果不在末尾)并前进光标。如果您有责任释放原始函数的 return 值,您也希望保留该值的副本并使用 %extend 添加一个 'destructor' 供 SWIG 在对象变为垃圾时调用集。

我告诉 SWIG 不允许用户使用 %nodefaultctor 构造 StrArrHandle 对象。 SWIG 将为 StrArrHandlestr 成员生成一个 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 的部分内容结合起来,以便 return 一个 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 的结果几乎相同,但方式却截然不同。