web-dev-qa-db-fra.com

Passer des pointeurs entre C et Java via JNI

En ce moment, j'essaie de créer une application Java qui utilise la fonctionnalité CUDA. La connexion entre CUDA et Java fonctionne bien, mais j'ai un autre problème et je voulais demander si mes pensées à ce sujet sont correctes.

Lorsque j'appelle une fonction native depuis Java, je lui transmets des données, les fonctions calculent quelque chose et retournent un résultat. Est-il possible de laisser la première fonction renvoyer une référence (pointeur) à ce résultat que je peux passer à JNI et appeler une autre fonction qui effectue d'autres calculs avec le résultat?

Mon idée était de réduire les frais généraux liés à la copie de données vers et depuis le GPU en laissant les données dans la mémoire du GPU et en leur passant simplement une référence pour que d'autres fonctions puissent les utiliser.

Après avoir essayé un certain temps, je me suis dit que cela ne devrait pas être possible, car les pointeurs sont supprimés après la fin de l'application (dans ce cas, lorsque la fonction C se termine). Est-ce correct? Ou suis-je tout simplement mauvais en C pour voir la solution?

Edit: Eh bien, pour développer un peu la question (ou le rendre plus clair): la mémoire allouée par les fonctions natives JNI est-elle désallouée lorsque la fonction se termine? Ou puis-je toujours y accéder jusqu'à ce que l'application JNI se termine ou lorsque je la libère manuellement?

Merci pour votre contribution :)

67
Volker

J'ai utilisé l'approche suivante:

dans votre code JNI, créez une structure qui contiendrait des références aux objets dont vous avez besoin. Lorsque vous créez cette structure pour la première fois, renvoyez son pointeur à Java en tant que long. Ensuite, à partir de Java vous appelez simplement n'importe quelle méthode avec ce long comme paramètre, et en C transformez-le en un pointeur vers votre structure.

La structure sera dans le tas, elle ne sera donc pas effacée entre les différents appels JNI.

EDIT: Je ne pense pas que vous pouvez utiliser long ptr = (long)&address; puisque l'adresse est une variable statique. Utilisez-le comme Gunslinger47 l'a suggéré, c'est-à-dire créez une nouvelle instance de classe ou une structure (en utilisant new ou malloc) et passez son pointeur.

44
Denis Tulskiy

En C++, vous pouvez utiliser n'importe quel mécanisme que vous souhaitez allouer/libérer de la mémoire: la pile, malloc/free, new/delete ou toute autre implémentation personnalisée. La seule exigence est que si vous avez alloué un bloc de mémoire avec un mécanisme, vous devez le libérer avec le même mécanisme, donc vous ne pouvez pas appeler free sur une variable de pile et vous ne pouvez pas appeler delete sur malloced mémoire.

JNI possède ses propres mécanismes d'allocation/libération de mémoire JVM:

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

Celles-ci suivent la même règle, le seul hic est que les références locales peuvent être supprimées "en masse" soit explicitement, avec PopLocalFrame, soit implicitement, lorsque la méthode native se termine.

JNI ne sait pas comment vous avez alloué votre mémoire, il ne peut donc pas la libérer lorsque votre fonction se termine. Les variables de pile seront évidemment détruites car vous écrivez toujours en C++, mais votre mémoire GPU restera valide.

Le seul problème est alors de savoir comment accéder à la mémoire lors des invocations suivantes, et vous pouvez ensuite utiliser la suggestion de 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;
    ...
}
15
Dan Berindei

Java ne saurait pas quoi faire avec un pointeur, mais il devrait être capable de stocker un pointeur à partir de la valeur de retour d'une fonction native, puis de le transmettre à une autre fonction native pour qu'il s'en occupe. Les pointeurs C ne sont rien de plus que des valeurs numériques au cœur.

Un autre contributeur devrait vous dire si la mémoire graphique pointée serait effacée entre les invocations JNI et s'il y aurait des solutions.

10
Gunslinger47

Je sais que cette question a déjà reçu une réponse officielle, mais je voudrais ajouter ma solution: au lieu d'essayer de passer un pointeur, placez le pointeur dans un tableau Java (à l'index 0) et passez JNI. Le code JNI peut obtenir et définir l'élément de tableau à l'aide de GetIntArrayRegion/SetIntArrayRegion.

Dans mon code, j'ai besoin de la couche native pour gérer un descripteur de fichier (un socket ouvert). La classe Java contient un int[1] array et le passe à la fonction native. La fonction native peut faire n'importe quoi avec elle (get/set) et remettre le résultat dans le tableau.

7
noamtm

Bien que la réponse acceptée de @ denis-tulskiy ait du sens, j'ai personnellement suivi les suggestions de ici .

Ainsi, au lieu d'utiliser un type de pseudo-pointeur tel que jlong (ou jint si vous souhaitez économiser de l'espace sur Arch 32 bits), utilisez plutôt un ByteBuffer. Par exemple:

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

que vous pourrez ensuite réutiliser avec:

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

Pour les cas très simples, cette solution est très simple à utiliser. Supposons que vous ayez:

struct {
  int exampleInt;
  short exampleShort;
} MyNativeStruct;

Du côté Java côté, il vous suffit de faire:

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

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

Ce qui vous évite d'écrire beaucoup de code passe-partout! Il faut cependant faire attention à l'ordre des octets comme expliqué ici .

6
malat

Si vous allouez de la mémoire dynamiquement (sur le tas) à l'intérieur de la fonction native, elle n'est pas supprimée. En d'autres termes, vous pouvez conserver l'état entre différents appels dans des fonctions natives, en utilisant des pointeurs, des variables statiques, etc.

Pensez-y différemment: que pourriez-vous faire en toute sécurité dans un appel de fonction, appelé à partir d'un autre programme C++? Les mêmes choses s'appliquent ici. Lorsqu'une fonction est quittée, tout ce qui se trouve sur la pile pour cet appel de fonction est détruit; mais tout ce qui se trouve sur le tas est conservé, sauf si vous le supprimez explicitement.

Réponse courte: tant que vous ne désallouez pas le résultat que vous retournez à la fonction appelante, il restera valide pour une nouvelle entrée plus tard. Assurez-vous simplement de le nettoyer lorsque vous avez terminé.

6
Marc Paradise

Il est préférable de le faire exactement comme le fait Unsafe.allocateMemory.

Créez votre objet puis tapez-le dans (uintptr_t) qui est un entier non signé 32/64 bits.

return (uintptr_t) malloc(50);

void * f = (uintptr_t) jlong;

C'est la seule façon correcte de le faire.

Voici la vérification de la raison que Unsafe.allocateMemory fait.

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
0
bond