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

78

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

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

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

经过一段时间的尝试后,我认为这不可能,因为指针在应用程序结束后被删除(在这种情况下,当C函数终止时)。这是正确的吗?还是我太菜了,看不到解决方案?

编辑: 好的,为了扩展问题(或使其更清晰):如果JNI本地函数分配的内存在函数结束时被释放了吗?我是否仍然可以访问它,直到JNI应用程序结束或手动释放它?

感谢您的回答 :)


1
另外,https://dev59.com/H1bUa4cB1Zd3GeqPAJyV - Pacerier
8个回答

50
我使用了以下方法:
在JNI代码中创建一个结构体,其中存储您需要的对象引用。当您首次创建此结构体时,将其指针作为long类型返回给Java。然后,在Java中,只需将该long类型作为参数调用任何方法,并在C语言中将其转换为指向您的结构体的指针。
这个结构体将位于堆上,因此它不会在不同的JNI调用之间清除。
编辑:我认为您不能使用long ptr = (long)&address; 因为address是静态变量。请像Gunslinger47建议的那样使用它,即创建类或结构体的新实例(使用new或malloc)并传递其指针。

11
MyClass *pObject = ...; long lp = (long)pObject; pObject = reinterpret_cast(lp);这段代码将对象指针转换为长整型(long),然后再将长整型转换回原始的对象指针类型。使用reinterpret_cast操作符进行转换,它可以将一个指针类型转换为另一种指针类型。注意,在进行这种类型的转换时需要特别小心,因为它可能会导致未定义的行为。 - Gunslinger47
13
在x32和x64平台上都使用long型?我很高兴我们暂时不会转向128位机器... - dhardy
5
我理解您的意思。这个解决方案不可移植——long类型的大小不能保证足够容纳一个指针。@dhardy也有同样的担忧。 - Nathan Osman
2
我也分享@dhardy的担忧。一种方法是传递一个Java字节数组,其中数组中的每个字节都是指针值的8位(并根据系统使数组长度不同)。但是,这意味着您还必须将指针的大小传递给JNI函数。 - AlexanderNajafi
2
@AlexanderNajafi:在这种情况下,我认为最好为您的数据设置一个已知大小的键。保持从键到指针的哈希映射,并从本地代码返回键到Java。 - Denis Tulskiy
显示剩余4条评论

20
在C++中,您可以使用任何机制来分配/释放内存:堆栈、malloc/free、new/delete或任何其他自定义实现。唯一的要求是,如果您使用一种机制分配了一块内存块,则必须使用相同的机制释放它,因此您不能在堆栈变量上调用free,也不能在malloc分配的内存上调用delete
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;
    ...
}

16

虽然来自@denis-tulskiy的接受答案很有道理,但我个人更喜欢遵循这里的建议。

因此,不要使用类似于伪指针类型jlong(或者在32位架构上想节省一些空间时使用jint),而是使用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);
}

这可以使你免于编写大量的样板代码!然而,应注意字节顺序,如此处所解释。


11

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

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


2
关于您的第二段:唯一需要注意的是确保任何分配的内存也得到释放。推荐的方法是在持有引用的对象上拥有某种关闭/处理(dispose())方法。虽然终结器很诱人,但它们带来了一些缺点,如果可能的话最好避免使用它们。 - Fredrik
顺便说一句,我不应该写“唯一的事情”... JNI 充满了陷阱。 - Fredrik
我已经包含了释放分配内存的函数,所以那应该不是问题 :) 主要问题仍然是:如果我不释放它,内存是否会保持分配状态?我的意思是,包括地址和值...我知道如果我不这样做,就会出现内存泄漏等问题,所以我已经包含了它 ;-) - Volker
2
@Volker:这取决于你如何分配它。唯一的例外是,如果某些东西在堆栈上分配,则通常无需自己释放/释放内存。如果是这样,当函数退出时,堆栈指针被移回时将被“释放”。因此,如果您使用某种内存分配函数(除了“alloca”),则必须释放它。这与Java实际上没有任何关系。一旦您进行了JNI跳转,规则就来自C世界,除非您使用Java对象。 - Fredrik
Fredrik,你的答案是正确的,而且应该被移动到自己的答案中以获得认可。 - Jonathan Feinberg
2
"C指针在本质上只是long值,C标准中没有规定这一点,这会引发未定义的行为。" - asveikau

10
我知道这个问题已经有官方答案,但我想加入我的解决方案:不要试图传递指针,而是将指针放在Java数组中(索引为0)并将其传递到JNI。 JNI代码可以使用 GetIntArrayRegion / SetIntArrayRegion 获取和设置数组元素。
在我的代码中,我需要本地层管理文件描述符(打开的套接字)。 Java类包含一个 int [1] 数组,并将其传递给本地函数。 本地函数可以对其进行任何操作(获取/设置),并将结果放回数组中。

7
如果你在本地函数中动态分配了内存(在堆上),则该内存不会被删除。换句话说,你可以使用指针、静态变量等,在不同的本地函数调用之间保留状态。
以另一种方式考虑:从另一个C++程序调用的函数,你能安全地保留哪些东西?这里应用相同的原则。当一个函数退出时,该函数调用的堆栈上的所有内容都会被销毁;但是除非你显式地删除它,否则堆上的任何内容都将得以保留。
简而言之:只要不释放返回给调用函数的结果,它将保持有效,以便稍后重新进入。只需确保在完成操作后清理它即可。

2
最好按照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 - Help Ukraine
1
这是JVM在32位和64位系统上分配内存的方式。使用uintptr_t进行赋值可以干净地转换为jlong。我不会争论它是否是一种好的方式,但这是JVM的方式。 - bond

0
这里是另一种方法,使用Java-C粘合文件中的全局静态变量,通过引用将变量传递给C,并将从C更改的变量返回给Java。但需要两个Java函数调用。
以下是在Kotlin中使用Android开发的示例:
首先,C代码(一个简单的通过引用增加变量的函数):
// lib.c
void cfunction(int *number)
{
    *number += 1;
}

通常我们会有另一个C代码,像胶水一样,用来处理Java的特性
// glue.c
static int number;

void cfunction(int *);

JNIEXPORT void JNICALL
Java_com_example_someapp_MainActivity_inc(JNIEnv* env, jobject thiz, jint value)
{
    number = value;
    cfunction(&number);
}

JNIEXPORT jint JNICALL
Java_com_example_someapp_MainActivity_getnumber(JNIEnv* env, jobject thiz)
{
    return number;
}

它使用一个全局变量number(仅限于文件glue.c内部访问,感谢static关键字),并且可以通过getter函数getnumber在外部访问。所以基本上我们需要一个额外的函数和一个静态全局变量来调用一个期望指针并改变其引用的C库。
一个CMakeLists.txt看起来会像这样:
cmake_minimum_required(VERSION 3.18.1)

project("someapp")

add_library(somelib SHARED
            glue.c
            lib.c)

最后是Android(Kotlin)部分:
package com.example.someapp

// imports

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // How to use a C call by ref in Kotlin
        inc(4)
        val number = getnumber()
        Toast.makeText(this, number.toString(), Toast.LENGTH_LONG).show()
    }

    external fun inc(number: Int)

    external fun getnumber(): Int

    companion object {
        init {
            System.loadLibrary("somelib")
        }
    }
}

应该显示5

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