将数组作为指针+大小或范围传递给包装函数

8

如果给定一个如下格式的头部:

#include <iostream>
#include <algorithm>
#include <iterator>

inline void foo(const signed char *arr, size_t sz) {
  std::copy_n(arr, sz, std::ostream_iterator<int>(std::cout, "\n"));
}

inline void bar(const signed char *begin, const signed char *end) {
  std::copy(begin, end, std::ostream_iterator<int>(std::cout, "\n"));
}

为了方便,我使用了C++11,在改变实现的情况下它可能是C语言或C++。如何包装这些函数,以便在Java端只需要传递一个数组,并使用数组的(已知)大小来提供这些函数的第二个参数呢?


提供一个Java的包装方法怎么样?毕竟,在Java中接受数组的方法也会带有“int offset,int length”参数... - Samuel Audet
@SamuelAudet - 你可以这样做,但我认为这不是一个设计良好的接口(仅仅为了复制信息而重复)。问题在于,如果你有byte[],你将需要编写一个类型映射(大多数情况下)将其转换为signed char *,或者使用%array_class和一个for循环来进行复制。这两种方法都相当丑陋。 - Flexo
@SamuelAudet - 我更新了我的回答并添加了一个手动封装的方法。在我看来,它非常丑陋。 - Flexo
嗯,我一直觉得SWIG很反人类,就像试图从汇编语言中使用C++一样。是的,我忘了,它甚至不会默认将char*映射到byte[],并且需要在Java和C++之上再使用另一种语言来指定它...不过现在你提到它,它的%typemap东西似乎与我在JavaCPP中放置的@Adapter注释非常相似。 - Samuel Audet
1个回答

12

重点是,要包装这两个函数之一,您需要使用多参数类型映射

引言对于SWIG来说非常标准。我使用了我个人最喜欢的编译指示,自动加载共享库而无需接口使用者知道:

%module test

%{
#include "test.hh"
%}

%pragma(java) jniclasscode=%{
  static {
    try {
        System.loadLibrary("test");
    } catch (UnsatisfiedLinkError e) {
      System.err.println("Native code library failed to load. \n" + e);
      System.exit(1);
    }
  }
%}

首先,您需要使用一些Java类型映射来指示SWIG将byte[]用作Java接口的两个部分(JNI和调用它的包装器)的类型。在生成模块文件中,我们将使用JNI类型jbyteArray。我们将直接从SWIG接口将输入传递给它生成的JNI。

%typemap(jtype) (const signed char *arr, size_t sz) "byte[]"
%typemap(jstype) (const signed char *arr, size_t sz) "byte[]"
%typemap(jni) (const signed char *arr, size_t sz) "jbyteArray"
%typemap(javain) (const signed char *arr, size_t sz) "$javainput"

完成这个后,我们就可以编写一个多参数类型映射:

%typemap(in,numinputs=1) (const signed char *arr, size_t sz) {
  $1 = JCALL2(GetByteArrayElements, jenv, $input, NULL);
  $2 = JCALL1(GetArrayLength, jenv, $input);
}

在typemap中的工作是将JNI调用所提供的内容转换为实际函数期望的输入。我使用numinputs=1来表示这两个真实函数参数在Java端只需要一个输入,但这是默认值,因此不需要明确说明。

在这个typemap中,$1是typemap的第一个参数,即在本例中我们函数的第一个参数。我们通过请求指向Java数组底层存储的指针(可能是复制品)来设置它。我们将第二个typemap参数$2设置为数组的大小。

这里的JCALLn宏确保typemap可以与C和C++ JNI一起编译。它会展开适合该语言的调用。

我们需要另一个typemap来在真实函数调用返回后进行清理:

%typemap(freearg) (const signed char *arr, size_t sz) {
  // Or use  0 instead of ABORT to keep changes if it was a copy
  JCALL3(ReleaseByteArrayElements, jenv, $input, $1, JNI_ABORT); 
}

这里调用ReleaseByteArrayElements函数,通知JVM我们已经完成了对数组的使用。该函数需要指针和我们获得它的Java数组对象,并且还需要一个参数来指示内容是否应在我们获得的指针是副本的情况下复制回去。我们传递NULL表示我们未传递jboolean类型的可选指针,该指针指示我们是否得到了一个副本。

对于第二种变体,类型映射基本相似:

%typemap(in,numinputs=1) (const signed char *begin, const signed char *end) {
  $1 = JCALL2(GetByteArrayElements, jenv, $input, NULL);
  const size_t sz = JCALL1(GetArrayLength, jenv, $input);
  $2 = $1 + sz;
}

%typemap(freearg) (const signed char *begin, const signed char *end) {
  // Or use  0 instead of ABORT to keep changes if it was a copy
  JCALL3(ReleaseByteArrayElements, jenv, $input, $1, JNI_ABORT);
}

%typemap(jtype) (const signed char *begin, const signed char *end) "byte[]"
%typemap(jstype) (const signed char *begin, const signed char *end) "byte[]"
%typemap(jni) (const signed char *begin, const signed char *end) "jbyteArray"
%typemap(javain) (const signed char *begin, const signed char *end) "$javainput"
唯一的区别是使用本地变量sz来计算begin指针的end参数。
唯一剩下的事情就是告诉SWIG使用我们刚刚编写的typemaps来包装头文件本身。
%include "test.hh"

我用以下内容测试了这两个函数:

public class run {
  public static void main(String[] argv) {
    byte[] arr = {0,1,2,3,4,5,6,7};
    System.out.println("Foo:");
    test.foo(arr);
    System.out.println("Bar:");
    test.bar(arr);
  }
}

这符合预期。

为了方便起见,我在我的网站上分享了我用于编写此内容的文件。按照这个答案的顺序,可以重建该存档中每个文件的每一行。


如果需要,我们可以完全不使用任何JNI调用来完成整个过程,而是使用%pragma(java) modulecode生成一个重载函数,将输入(在纯Java中)转换为真实函数所需的形式。对于这个功能,模块文件将是:

%module test

%{
#include "test.hh"
%}

%include <carrays.i>
%array_class(signed char, ByteArray);

%pragma(java) modulecode = %{
  // Overload foo to take an array and do a copy for us:
  public static void foo(byte[] array) {
    ByteArray temp = new ByteArray(array.length);
    for (int i = 0; i < array.length; ++i) {
      temp.setitem(i, array[i]);
    }
    foo(temp.cast(), array.length);
    // if foo can modify the input array we'll need to copy back to:
    for (int i = 0; i < array.length; ++i) {
      array[i] = temp.getitem(i);
    }
  }

  // How do we even get a SWIGTYPE_p_signed_char for end for bar?
  public static void bar(byte[] array) {
    ByteArray temp = new ByteArray(array.length);
    for (int i = 0; i < array.length; ++i) {
      temp.setitem(i, array[i]);
    }
    bar(temp.cast(), make_end_ptr(temp.cast(), array.length));
    // if bar can modify the input array we'll need to copy back to:
    for (int i = 0; i < array.length; ++i) {
      array[i] = temp.getitem(i);
    }
  }
%}

// Private helper to make the 'end' pointer that bar expects
%javamethodmodifiers make_end_ptr "private";
%inline {
  signed char *make_end_ptr(signed char *begin, int sz) {
    return begin+sz;
  }
}

%include "test.hh"

%pragma(java) jniclasscode=%{
  static {
    try {
        System.loadLibrary("test");
    } catch (UnsatisfiedLinkError e) {
      System.err.println("Native code library failed to load. \n" + e);
      System.exit(1);
    }
  }
%}
除了明显需要两个副本来将数据转换为正确的类型(从byte[]到SWIGTYPE_p_signed_char没有简单的方法),并且有另一个缺点 - 它只适用于函数foo和bar,而我们之前编写的typemaps不特定于给定的函数-它们将应用于任何匹配的地方,甚至在相同的函数上多次匹配,如果您碰巧有一个接受两个范围或两个指针+长度组合的函数。以这种方式操作的一个优点是,即使您已经包装了返回SWIGTYPE_p_signed_char的其他封装函数,如果需要,则仍然可以使用重载。即使您有来自%array_class的ByteArray,您仍然无法在Java中执行所需的指针算术以为您生成结束。原始的方法提供了更清晰的Java界面,具有不制作过多副本和更高的可重用性的附加优势。
另一种包装的替代方法是为foo和bar编写一些%inline重载:
%inline {
  void foo(jbyteArray arr) {
    // take arr and call JNI to convert for foo
  }
  void bar(jbyteArray arr) {
    // ditto for bar
  }
}

这些在Java接口中被呈现为重载,但它们仍然是特定于模块的。此外,所需的JNI比通常需要的更为复杂 - 你需要想办法获取jenv,而这默认情况下是不可访问的。选项是慢速调用来获取它,或者使用numinputs=0类型映射自动填充参数。无论哪种方式,多参数类型映射似乎更好。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接