用于包装C++对象的最佳 JNI 模式

Best JNI Pattern for wrapping C++ Objects?

本文关键字:最佳 JNI 模式 对象 包装 C++ 用于      更新时间:2023-10-16

我正在开发一个Java API,其中许多Java对象实际上是等效C++对象的包装器。Java 对象创建C++对象,并负责在不再需要它们时释放它们。我想知道为此使用的最佳模式,我可以看到两种可能的选择:

  1. 使用静态本机方法调用和 final 变量在构造函数中构造 C++ 对象,以保存本机句柄。

    public abstract class NativeBackedObject1 implements java.lang.AutoCloseable {
        protected final long _nativeHandle;
        protected final AtomicBoolean _nativeOwner;
        protected NativeBackedObject1(final long nativeHandle) {
            this._nativeHandle = nativeHandle;
            this._nativeOwner = new AtomicBoolean(true);
        }
        @Override
        public close() {
            if(_nativeOwner.copareAndSet(true, false)) {
                disposeInternal();
            }
        }
        protected abstract void disposeInternal();
    }
    public SomeFoo1 extends NativeBackendObject1 {
        public SomeFoo1() {
            super(newFoo());
        }
        @Override
        protected final void disposeInternal() {
            //TODO: any local object specific cleanup
            disposeInternal(_nativeHandle);
        }
        private native static long newFoo();
        private native disposeInternal(final long nativeHandle);
    }
    
  2. 使用实例本机方法调用和非最终变量在构造函数中构造 C++ 对象,以保存本机句柄。

    public abstract class NativeBackedObject2 implements java.lang.AutoCloseable {
        protected long _nativeHandle;
        protected boolean _nativeOwner;
        protected NativeBackedObject2() {
            this._nativeHandle = 0;
            this._nativeOwner = true;
        }
        @Override
        public void close() {
            synchronized(this) {
                if(_nativeOwner && _nativeHandle != 0) {
                    disposeInternal();
                    _nativeHandle = 0;
                    _nativeOwner = false;
                }
            }
        }
        protected abstract void disposeInternal();
    }
    public SomeFoo2 extends NativeBackendObject2 {
        public SomeFoo2() {
            super();
            _nativeHandle = newFoo();
        }
        @Override
        protected final void disposeInternal() {
            //TODO: any local object specific cleanup
            disposeInternal(_nativeHandle);
        }
        private native long newFoo();
        private native disposeInternal(final long nativeHandle);
    }
    

目前我认为(1)是更好的方法,因为:

  • 一个。这意味着我可以将_nativeHandle设置为不可变(final)。所以我不需要担心并发访问或意外更改(代码实际上比这些简单的例子更复杂)。
  • b. 由于构造函数,我在设计中形式化,NativeBackedObject的任何子类都是其各自本机对象(由 _nativeHandle 表示)的所有者,因为没有它就无法构造。
方法(2)与(1)

相比有什么优点,或者方法(1)有什么问题吗?

我还可以看到接近 (1) 的替代模式,我们称之为方法 (3):

public abstract class NativeBackedObject3 implements java.lang.AutoCloseable {
    protected final long _nativeHandle;
    protected final AtomicBoolean _nativeOwner;
    protected NativeBackedObject3() {
        this._nativeHandle = newInternal();
        this._nativeOwner = new AtomicBoolean(true);
    }
    @Override
    public close() {
        if(_nativeOwner.copareAndSet(true, false)) {
            disposeInternal();
        }
    }
    protected abstract long newInternal();
    protected abstract void disposeInternal();
}
public SomeFoo3 extends NativeBackendObject3 {
    public SomeFoo3() {
        super();
    }
    @Override
    protected final void disposeInternal() {
        //TODO: any local object specific cleanup
        disposeInternal(_nativeHandle);
    }
    @Override
    protected long newInternal() {
        return newFoo();
    };
    private native long newFoo();
    private native disposeInternal(final long nativeHandle);
}

(3) 相对于 (1) 的优势是我可以移回默认构造函数,这可以帮助创建用于测试等的模拟。不过,主要缺点是我无法再将其他参数传递给newFoo()

也许我错过了其他方法?欢迎提出建议...

您是否尝试过可以生成 c++ 对象的 Java 包装器的 SWIG(http://www.swig.org)?

%typemap(javabody) SWIGTYPE %{
    private long swigCPtr;
    protected boolean swigCMemOwn;
    public $javaclassname(long cPtr, boolean cMemoryOwn) {
        swigCMemOwn = cMemoryOwn;
        swigCPtr = cPtr;
    }
    public static long getCPtr($javaclassname obj) {
       return (obj == null) ? 0 : obj.swigCPtr;
    }
 %}

正如SWIG的文档所说,考虑简单的测试类:

class Test {
   string str;
public:
  Test() : str("initial") {}
};

它的输出是:

public class Test {
  private long swigCPtr;
  protected boolean swigCMemOwn;
  protected Test(long cPtr, boolean cMemoryOwn) {
    swigCMemOwn = cMemoryOwn;
    swigCPtr = cPtr;
  }
  protected static long getCPtr(Test obj) {
    return (obj == null) ? 0 : obj.swigCPtr;
  }
  protected void finalize() {
    delete();
  }
  // Call C++ destructor
  public synchronized void delete() {
    if(swigCPtr != 0 && swigCMemOwn) {
      swigCMemOwn = false;
          exampleJNI.delete_Test(swigCPtr);
        }
        swigCPtr = 0;
      }
  // Call C++ constructor
  public Test() {
    this(exampleJNI.new_Test(), true);
  }
}

根据基准测试,"通过调用,静态"方法确实是最有效的https://github.com/adamretter/jni-construction-benchmark但是JavaCPP基本上使用"通过调用,调用"(顺便说一句,可以通过缓存jfieldID来提高效率)。

我选择这样做的原因是生成一个更干净的 Java 接口文件。用户可以决定手动编写它或让该工具从头文件生成它。无论哪种方式,它最终都会像某些头文件的 Java 翻译一样阅读。但是,我们添加到该界面的粗糙越多,它看起来就越不像C++,编写或读取它就越困难。(这是我不喜欢SWIG的众多事情之一。

顺便说一句,不禁止修改 JNI 中的final变量,所以这可能是人们可能想要这样做的另一个原因。

当然,仍然可以

修改JavaCPP并支持计算效率更高的做事方式,但是我们几乎没有节省任何时间,因为它还没有被证明是一个问题。