通过JNI传递C和Java之间的指针

Passing pointers between C and Java through JNI

目前,我正在尝试创建一个使用CUDA功能的Java应用程序。CUDA和Java之间的连接工作很好,但是我有另一个问题,我想问一下,如果我的想法是正确的。

当我从Java调用一个本地函数时,我将一些数据传递给它,函数计算某个结果并返回结果。有没有可能,让第一个函数返回一个对这个结果的引用(指针),我可以把它传递给JNI,并调用另一个函数,用这个结果做进一步的计算?

我的想法是通过将数据保留在GPU内存中,并只传递一个对它的引用,以便其他函数可以使用它,从而减少在GPU之间复制数据所产生的开销。

在尝试了一段时间后,我自己想,这不可能,因为指针在应用程序结束后被删除(在本例中,当C函数终止时)。这是正确的吗?或者我只是为了看问题的解决方案而在C中做得太差了?

编辑:好吧,将问题稍微展开一点(或者更清楚地说):当函数结束时,JNI本机函数分配的内存是否被释放?或者,在JNI应用程序结束或手动释放它之前,我是否仍然可以访问它?

感谢您的输入:)


我采用了以下方法:

在JNI代码中,创建一个结构来保存对所需对象的引用。当您首先创建此结构时,将其指针返回给Java作为EDCOX1 0。然后,从Java中调用这个EDCOX1×0作为参数的任何方法,并在C中将其转换为指向结构的指针。

结构将在堆中,因此不会在不同的JNI调用之间清除。

编辑:我认为你不能使用long ptr=(long)&address;,因为地址是一个静态变量。按照gunslinger47建议的方式使用它,即创建类或结构的新实例(使用new或malloc)并传递其指针。


在C++中,您可以使用任何要分配/释放内存的机制:堆栈、MalC/C、Field、NeX/Delphi或任何其他自定义实现。唯一的要求是,如果使用一个机制分配一个内存块,则必须使用相同的机制释放它,这样就不能在堆栈变量上调用free,也不能在malloced内存上调用delete

JNI有自己的机制来分配/释放JVM内存:

  • 新建对象/删除本地引用
  • 新全局参考/删除全局参考
  • newweakglobalref/删除weakglobalref

它们遵循相同的规则,唯一的缺点是本地引用可以"集中"删除,可以使用PopLocalFrame显式删除,也可以在本机方法退出时隐式删除。

JNI不知道您是如何分配内存的,所以当您的函数退出时,它无法释放内存。堆栈变量显然会被破坏,因为你仍然在写C++,但是你的GPU内存将保持有效。

唯一的问题是如何在随后的调用中访问内存,然后您可以使用gunslinger47的建议:

1
2
3
4
5
6
7
8
9
JNIEXPORT jlong JNICALL Java_MyJavaClass_Function1() {
    MyClass* pObject = new MyClass(...);
    return (long)pObject;
}

JNIEXPORT void JNICALL Java_MyJavaClass_Function2(jlong lp) {
    MyClass* pObject = (MyClass*)lp;
    ...
}


Java不知道如何处理指针,但是它应该能够从一个原生函数的返回值存储指针,然后将指针交给另一个原生函数来处理。C指针只不过是核心的数字值。

另一个Continibutor必须告诉您,在JNI调用之间是否清除指向图形内存,以及是否有任何解决方法。


我知道这个问题已经得到了正式答复,但我想补充我的解决方案:不要尝试传递指针,而是将指针放在Java数组(索引0)中,并将其传递给JNI。JNI代码可以使用GetIntArrayRegion/SetIntArrayRegion获取和设置数组元素。

在我的代码中,我需要本机层来管理一个文件描述符(一个打开的套接字)。Java类保存一个EDCOX1×9的数组,并将其传递给本机函数。本机函数可以对其执行任何操作(get/set),并将结果放回数组中。


虽然@denis tulskiy接受的答案确实有道理,但我还是个人遵循了这里的建议。

因此,不要使用伪指针类型,如jlong(如果要在32位arch上节省一些空间,请使用jint),而是使用ByteBuffer。例如:

1
2
MyNativeStruct* data; // Initialized elsewhere.
jobject bb = (*env)->NewDirectByteBuffer(env, (void*) data, sizeof(MyNativeStruct));

以后您可以使用它:

1
2
jobject bb; // Initialized elsewhere.
MyNativeStruct* data = (MyNativeStruct*) (*env)->GetDirectBufferAddress(env, bb);

对于非常简单的情况,这个解决方案非常容易使用。假设你有:

1
2
3
4
struct {
  int exampleInt;
  short exampleShort;
} MyNativeStruct;

在Java方面,您只需要这样做:

1
2
3
4
5
6
7
public int getExampleInt() {
  return bb.getInt(0);
}

public short getExampleShort() {
  return bb.getShort(4);
}

这样可以避免编写大量样板代码!但是,我们应该注意这里解释的字节顺序。


如果在本机函数内部动态地(在堆上)分配内存,则不会删除内存。换句话说,您可以使用指针、静态变量等在不同的本地函数调用之间保留状态。

想一想另一种方式:你能做什么安全地保持在一个函数调用,从另一个C++程序调用?这里同样适用。当函数退出时,该函数调用的堆栈上的任何内容都将被销毁;但除非显式删除,否则将保留该堆栈上的任何内容。

简短回答:只要不取消分配返回到调用函数的结果,它将在稍后重新进入时保持有效。完成后一定要清理干净。


最好做到这一点,这正是.allocateMemory的不安全之处。

创建对象,然后将其键入(uintptr_t),它是一个32/64位无符号整数。

1
2
3
return (uintptr_t) malloc(50);

void * f = (uintptr_t) jlong;

这是唯一正确的方法。

这是健全性检查不安全。allocatememory检查不安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
inline jlong addr_to_java(void* p) {
  assert(p == (void*)(uintptr_t)p,"must not be odd high bits");
  return (uintptr_t)p;
}

UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
  UnsafeWrapper("Unsafe_AllocateMemory");
  size_t sz = (size_t)size;
  if (sz != (julong)size || size < 0) {
    THROW_0(vmSymbols::java_lang_IllegalArgumentException());
  }
  if (sz == 0) {
    return 0;
  }
  sz = round_to(sz, HeapWordSize);
  void* x = os::malloc(sz, mtInternal);
  if (x == NULL) {
    THROW_0(vmSymbols::java_lang_OutOfMemoryError());
  }
  //Copy::fill_to_words((HeapWord*)x, sz / HeapWordSize);
  return addr_to_java(x);
UNSAFE_END