当作为 wrapped class 的参数传递时,在 Java 和 C++ class 实例之间转换

Convert between Java and C++ class instance when passing as arguments of wrapped class

给定一个任意的Javaclass,说

public class Coord {
    public double x;
    public double y;
    public double z;

    public Coord(double x, double y, double z) {
         this.x = x; this.y = y; this.z = z;
    }
}

和任意 C++ class(或结构),例如 C++11 STL class std::array<double, 3>,

和使用 Swig 包装的 C++ class 生成 Java 代理 class,例如

class Object {
    move_center(std::array<double, 3> vec) {
        // C++ code
    }
}

我希望能够写成Java

Object obj;
// initialize obj
Coord vec = new Coord(1.5,2.5,3.5);
obj.move_center(vec);

并让包装器构造一个 std::array<double, 3>vec[0] = 1.5 等等

我知道我可以在 C++ 中定义一个 Coord class,让 Swig 包装它并在 Java 中使用代理 Coord,但是有两个不这样做的原因:

我可以想象要走的路是使用 i 文件并使用 typemap

%typemap(jstype) std::array<double, 3> "com.foo.bar.Coord"

但从那里我真的不知道去哪里。

对此有很多考虑因素以及解决此问题的几种可能方法,因此我将按照我认为的逻辑顺序来解决其中的一些问题。

我首先创建了一个测试头文件以在我的演示中使用,它对您展示的内容进行了细微调整:

#include <array>

struct Object {
    void move_center(std::array<double, 3> vec) {
        // C++ code
    }
};

首先,如果您使用的是足够新的 SWIG 版本(肯定比 3.0.2 更新,但不确定具体是哪个版本),您将获得对 std::array 的一些库支持,我们可以将其用作一个起点。

%module test

%{
#include "test.hh"
%}

%include <std_array.i>
%template(Vec3) std::array<double, 3>;

%include "test.hh"

作为起点就足够了,您将获得一个可用的接口,该接口采用类型 Vec3,这是 std::array<double, 3> 的可用包装形式。

很明显,尽管使用 Coord,这实际上并不能满足您的要求,因此我们想编写一些类型映射,以便在函数调用时在 Java/C++ 类型之间进行转换。实际上,您可以在多个地方执行此操作。最简单的就是写成javain typemap:

%module test

%{
#include "test.hh"
%}

%include <std_array.i>
%template(Vec3) std::array<double, 3>;

%typemap(jstype) std::array<double, 3> "Coord"
%typemap(javain,pre="    Vec3 temp$javainput = new Vec3();\n"
                    "    temp$javainput.set(0, $javainput.x);\n"
                    "    temp$javainput.set(1, $javainput.y);\n"
                    "    temp$javainput.set(2, $javainput.z);",
         pgcppname="temp$javainput") std::array<double, 3>, const std::array<double, 3>& "$javaclassname.getCPtr(temp$javainput)"

%include "test.hh"

基本上,在您向我们展示的类型图上方所做的所有这些都是将代码插入到生成的 Java 函数调用中。这段代码只是读出 x、y、z 并将它们放入一个临时的 Vec3 中,该临时 Vec3 是专门为通话期间创建的。

(如果你愿意,你可以添加一个 javaout 类型映射,用于从 C++ 函数返回这些,以及另一个具有支持非常量引用的 post 属性的变体,详情请咨询)

你会注意到这里虽然它满足了你要求的功能要求,但另一个目标是避免过多的跨语言函数调用,但这里我们有 4 个额外的包括一个内存分配。

因此,为了解决这个问题,我们可以开始让我们的界面更智能一些。 SWIG(多年来)在其库 arrays_java.i 中有一些数组辅助代码。我们可以使用它在一次调用中构造我们的临时 C++ 对象,方法是使用 %extend 添加一个以 double[3] 作为输入的新构造函数:

%module test

%{
#include "test.hh"
#include <algorithm>
%}

%include <std_array.i>
%include <arrays_java.i>

%template(Vec3) std::array<double, 3>;

%extend std::array<double, 3> {
  std::array<double, 3>(double in[3]) {
    std::array<double, 3> temp;
    std::copy_n(in, 3, std::begin(temp));
    return new std::array<double, 3>(temp);
  }
}

%typemap(jstype) std::array<double, 3> "Coord"
%typemap(javain,pre="    Vec3 temp$javainput = new Vec3(new double[]{$javainput.x, $javainput.y, $javainput.z});",
         pgcppname="temp$javainput") std::array<double, 3>, const std::array<double, 3>& "$javaclassname.getCPtr(temp$javainput)"

%include "test.hh"

虽然我们可以做得更好,但为什么不将临时构造从 Java 移到 C++ 中呢?作为下一步,我想让 SWIG 将 double[3] 传递给 C++,然后在 in 类型映射中处理它。我尝试了以下方法:

%module test

%{
#include "test.hh"
#include <algorithm>
%}

%include <arrays_java.i>

%apply double[3] { std::array<double, 3> };
%typemap(jstype) std::array<double, 3> "Coord"
%typemap(javain) std::array<double, 3>, const std::array<double, 3>& "new double[]{$javainput.x, $javainput.y, $javainput.z}"

%typemap(in) std::array<double, 3> //....

%include "test.hh"

请注意,我们现在已经放弃了 SWIG 库中对 std_array.i 的要求,而只依赖 arrays_java.i。尽管这实际上不起作用(%apply 在这里无效)。

这真的不是什么大问题,我也没有花太多时间在这上面,因为无论如何我们都可以通过自己编写 arrays_java 提供的 JNI 调用来解决这个问题:

%module test

%{
#include "test.hh"
#include <algorithm>
%}


%typemap(jstype) std::array<double, 3> "Coord"
%typemap(javain) std::array<double, 3>, const std::array<double, 3>& "new double[]{$javainput.x, $javainput.y, $javainput.z}"
%typemap(jtype) std::array<double, 3> "double[]"
%typemap(jni) std::array<double, 3> "jdoubleArray"
%typemap(in) std::array<double, 3> {
  if (!$input || JCALL1(GetArrayLength, jenv, $input) != 3) {
    SWIG_JavaThrowException(jenv, SWIG_JavaIndexOutOfBoundsException, "incorrect array size");
    return $null;
  }
  double *arr = JCALL2(GetDoubleArrayElements, jenv, $input, NULL);
  std::copy_n(arr, 3, .begin());
  JCALL3(ReleaseDoubleArrayElements, jenv, $input, arr, JNI_ABORT);
}

%include "test.hh"

这开始尽可能接近我们的目标最小开销包装。我们编写的 JNI 足以让我们一次性将整个 double[] 数组复制到 std::array 中。 (您的 C++ 编译器应该能够很好地优化该复制操作)。我们仍在 Java 内分配一个由 3 个双精度值组成的临时数组,这在这种方法中基本上是不可避免的,因为我们没有办法增加传递给 C++ 的参数数量,只能减少该数量。

如果你想要 argout 类型映射可以支持通过非常量引用传递给修改输入的函数。你想使用 GetDoubleArrayElements 调用的最后一个参数来查看它是否是一个副本,并将取消映射保存到 argout 类型映射,并在那时制作你自己的副本。

作为一种完全替代的方法,我们可以选择将 Coord 对象作为 jobject 一直传递到 in 类型映射,并在那里进行 3 次 JNI 调用以获取 x、y 和 z 成员变量值在那时候。就我个人而言,我不像上面的数组那样喜欢这个想法,我会使用上面的例子来论证将 Coord class 的组件存储为内部数组并公开它们如果您想给它们命名,请在 Java 中使用访问器函数。