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


72

目前,我正在尝试创建一个使用CUDA功能的Java应用程序。CUDA与Java之间的连接工作正常,但是我还有另一个问题,想问一下我对此是否正确。

当我从Java调用本机函数时,我将一些数据传递给它,这些函数计算出一些东西并返回结果。是否可以让第一个函数返回对此结果的引用(指针),我可以将其传递给JNI并调用另一个对结果进行进一步计算的函数?

我的想法是通过将数据保留在GPU内存中并仅将引用传递给它,以便其他功能可以使用它,从而减少往返于GPU的数据复制所带来的开销。

经过一段时间的尝试,我自己想,这是不可能的,因为在应用程序结束后(在这种情况下,当C函数终止时)指针被删除。这个对吗?还是我对C语言很不好而看不到解决方案?

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

感谢您的输入 :)


Answers:


47

我使用以下方法:

在您的JNI代码中,创建一个结构,该结构将保存对所需对象的引用。首次创建此结构时,以形式返回其指向java的指针long。然后,从Java中,您只需使用此方法long作为参数调用任何方法,然后在C中将其强制转换为指向您的结构的指针。

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

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


9
MyClass *pObject = ...; long lp = (long)pObject; pObject = (*pObject)lp;
Gunslinger47年9

11
在x32和x64平台上都使用很长时间?我很高兴我们不会很快转移到128位计算机上……
dhardy 2012年

3
我同意@dhardy的关注。该解决方案不是可移植的-无法保证long其大小足以容纳一个指针。
内森·奥斯曼

1
sun.misc.unsafe大量使用long作为指针,因此无论Oracle是否真正批准它都得到有效支持。因此,接受合同的风险由您自担。
菲利普·吉恩

1
我也分享@dhardy的关注。一种方法是传递一个Java字节数组,其中数组中的每个字节都是指针值的8位(并使数组的长度取决于系统)。但是,这意味着您还必须将指针的大小传递给JNI函数。
AlexanderNajafi'3

16

在C ++中,可以使用要分配/释放内存的任何机制:堆栈,malloc / free,new / delete或任何其他自定义实现。唯一的要求是,如果你分配的内存块有一个机制,你有相同的机制来释放它,所以你不能叫free一个堆栈变量,你不能叫deletemalloc版内存。

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

  • NewObject / DeleteLocalRef
  • NewGlobalRef / DeleteGlobalRef
  • NewWeakGlobalRef / DeleteWeakGlobalRef

这些遵循相同的规则,唯一的不足是PopLocalFrame,当本机方法退出时,可以显式地使用或隐式“整体”删除本地引用。

JNI不知道如何分配内存,因此在函数退出时无法释放它。显然,由于仍在编写C ++,堆栈变量将被破坏,但是GPU内存将保持有效。

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

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;
    ...
}

11

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

另一位竞争者将不得不告诉您,是否在JNI调用之间清除了指向图形的内存,以及是否有任何解决方法。


2
关于第二段:唯一要注意的是确保分配的所有内存也都被释放。推荐的方法是在保存引用的对象上使用某种close / dispose()方法。终结器很诱人,但它们有一些缺点,因此有可能在可能的情况下避免使用它们。
Fredrik

我本来不应该写“唯一的东西”……JNI充满了陷阱。
Fredrik

我已经包含了释放已分配内存的函数,因此应该没问题:)主要问题仍然是:如果我执行net free,内存是否仍保持分配状态?我的意思是,包括地址和值...我知道如果我不这样做,我将得到内存泄漏等等,所以我已经包括了;-)
Volker

2
@沃尔克:这取决于您如何分配它。通常,如果在堆栈上分配了某些内容,则您必须自己重新分配/释放内存的规则的唯一例外。如果是这样,则在函数退出时将堆栈指针移回时将被“释放”。因此,如果使用某种内存分配功能(“ alloca”除外)分配了内存,则必须释放它。实际上,它与Java完全无关。一旦使JNI跳转,除非您使用Java对象,否则规则就来自C世界。
Fredrik

2
“ C指针不过long是核心值。” -C标准中没有任何内容可以说明这一点,并且这正在调用未定义的行为。
asveikau 2012年

9

我知道这个问题已经被正式回答,但是我想添加我的解决方案:与其尝试传递指针,不如将指针放在Java数组(索引为0)中并将其传递给JNI。JNI代码可以使用GetIntArrayRegion/获取并设置数组元素SetIntArrayRegion

在我的代码中,我需要本机层来管理文件描述符(开放套接字)。Java类保存一个int[1]数组并将其传递给本机函数。本机函数可以执行任何操作(获取/设置),并将结果放回数组中。


9

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

因此,不要使用诸如的伪指针类型jlong(或者,jint如果您想在32bits arch上节省一些空间),请使用ByteBuffer。例如:

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

您可以稍后将其重复使用:

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

对于非常简单的情况,此解决方案非常易于使用。假设您有:

struct {
  int exampleInt;
  short exampleShort;
} MyNativeStruct;

在Java方面,您只需要执行以下操作:

public int getExampleInt() {
  return bb.getInt(0);
}

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

这样可以避免编写大量样板代码!但是,应注意此处说明的字节顺序。


6

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

用另一种方式思考:您可以安全地保留从另一个C ++程序调用的函数调用中的什么?同样的情况在这里适用。当一个函数退出时,该函数调用的堆栈中的所有内容都会被破坏;但是除非明确删除,否则堆中的所有内容都会保留。

简短的答案:只要不取消分配返回给调用函数的结果,它将在以后重新进入时仍然有效。只要确保完成后将其清理即可。


0

最好完全按照Unsafe.allocateMemory的方式执行此操作。

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

return (uintptr_t) malloc(50);

void * f = (uintptr_t) jlong;

这是唯一的正确方法。

这是Unsafe.allocateMemory进行的检查。

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

不是标准的定义uintptr_t。这样做是一个非常糟糕的主意。它被定义为足够大以容纳任何指针,并且可以是所需的任何长度。通常在64位系统上为64位,但是该标准甚至不允许这种假设。您绝对不应假设的大小uintptr_t
StephenG 2015年

1
这就是JVM在32位和64位系统上分配内存的方式。分配uintptr_t可以将其转换为jlong​​。我不会讨论这是否是一种好方法,但是这是JVM的一种方法。
债券
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.