Wrapper JNI pour les classes C++ sous Android : Création et Callbacks
Intégrer du code C++ orienté objet dans une application Android via JNI nécessite de le convertir en fonctions C tout en stockant un pointeur vers l'objet. L'adresse de l'objet C++ est sauvegardée sous forme de long dans une classe Java, permettant la gestion du cycle de vie et de multiples instances.
Prenons une classe C++ basique :
class NativeClass {
public:
NativeClass() {}
~NativeClass() {}
void fun() {
__android_log_write(ANDROID_LOG_ERROR, log_tag, "Hello from native");
}
};
Fonctions JNI pour travailler avec la classe :
- Initialisation :
jlong Java_com_myprog_example_Native_initNativeClass(JNIEnv* env, jobject thiz)retourne(jlong) new NativeClass(); - Appel de méthode :
void Java_com_myprog_example_Native_NativeClassFun(JNIEnv env, jobject thiz, jlong cppClass)appelle((NativeClass) cppClass)->fun(); - Destruction :
void Java_com_myprog_example_Native_destroyNativeClass(JNIEnv env, jobject thiz, jlong cppClass)exécutedelete (NativeClass) cppClass;
Wrapper Java :
public class Native {
static { System.loadLibrary("Native"); }
public static native long initNativeClass();
public static native void destroyNativeClass(long cppClass);
public static native void NativeClassFun(long cppClass);
}
public class NativeClass {
long cppClass;
NativeClass() {
this.cppClass = Native.initNativeClass();
}
@Override protected void finalize() {
free();
}
private void free() {
Native.destroyNativeClass(cppClass);
}
public void fun() {
Native.NativeClassFun(cppClass);
}
}
Mise en œuvre des callbacks depuis le code natif
Pour des scénarios asynchrones, comme le scan réseau, le code natif doit appeler des méthodes Java pendant l'opération. Une interface Listener en C++ et un ThreadPool sont utilisés pour sérialiser les appels dans un seul thread.
Classe C++ étendue avec support des callbacks :
class NativeClass {
public:
class Listener {
public:
virtual void print(const char* str) = 0;
virtual ~Listener() {}
};
private:
bool started = false;
Listener* listener = nullptr;
static void worker(NativeClass* nc) {
while(nc->started) {
if(nc->listener != nullptr) {
nc->listener->print("From native");
}
sleep(1);
}
}
public:
NativeClass() {}
~NativeClass() {
if(listener != nullptr) delete listener;
}
void setListener(Listener* listener) {
this->listener = listener;
}
void scan() {
started = true;
std::thread thread = std::thread(worker, this);
thread.join();
started = false;
}
void stop() {
started = false;
}
};
Interface Java et wrapper :
public interface NativeListener {
void print(String str);
}
public class NativeClass {
long cppClass;
// constructeur, finalize, free comme ci-dessus
public void scan() { Native.NativeClassScan(cppClass); }
public void stop() { Native.NativeClassStop(cppClass); }
public void setListener(NativeListener listener) {
Native.NativeClassSetListener(cppClass, listener);
}
}
Composant clé — implémentation de Listener en C++ avec JNI :
class Listener : public NativeClass::Listener {
private:
JavaVM* jvm;
JNIEnv* env;
jweak listenerRef;
jmethodID printMethod;
public:
Listener(JavaVM* jvm, JNIEnv* env, jobject listener) {
this->jvm = jvm;
listenerRef = env->NewWeakGlobalRef(listener);
ThreadPool::execute({
jvm->AttachCurrentThread(&env, NULL);
if(env != NULL) {
jobject localRef = env->NewLocalRef(listenerRef);
if(localRef != NULL) {
jclass clazz = env->GetObjectClass(localRef);
printMethod = env->GetMethodID(clazz, "print", "(Ljava/lang/String;)V");
env->DeleteLocalRef(localRef);
}
}
});
}
~Listener() {
ThreadPool::execute({
env->DeleteWeakGlobalRef(listenerRef);
jvm->DetachCurrentThread();
});
}
void print(const char* msg) {
ThreadPool::execute({
jobject localRef = env->NewLocalRef(listenerRef);
if(localRef != NULL) {
jstring jMsg = env->NewStringUTF(msg);
env->CallVoidMethod(localRef, printMethod, jMsg);
env->DeleteLocalRef(jMsg);
env->DeleteLocalRef(localRef);
}
});
}
};
Fonction JNI pour définir le listener :
JNIEXPORT void JNICALL Java_com_myprog_example_Native_NativeClassSetListener(
JNIEnv* env, jobject thiz, jlong cppClass, jobject listener) {
((NativeClass*) cppClass)->setListener(new Listener(jvm, env, listener));
}
Recommandations de mise en œuvre
- ThreadPool monothread : Garantit des appels JNI séquentiels depuis un thread fixe.
- Références faibles (jweak) : Évite les fuites mémoire ; des références locales fortes sont créées uniquement pour la durée de l'appel.
- Gestion du cycle de vie : Le destructeur de Listener nettoie les ressources via le ThreadPool.
Liste des bonnes pratiques clés :
- Vérifiez toujours la valeur NULL sur les références locales avant utilisation.
- Utilisez
NewWeakGlobalRefpour les références de longue durée et supprimez-les à temps. - Appelez
AttachCurrentThread/DetachCurrentThreaduniquement dans le ThreadPool. - Pour les callbacks synchrones, ajoutez des variables de condition.
- Évitez le thread UI pour les opérations bloquantes comme
scan().
Points clés à retenir
- Stocker un pointeur d'objet C++ comme long en Java offre une sémantique objet complète.
- Un ThreadPool monothread est crucial pour des appels JNI stables depuis les threads natifs.
- Les références globales faibles (jweak) + références locales minimisent les risques de fuites mémoire.
- La finalisation du wrapper Java nettoie automatiquement les ressources natives.
- Les callbacks asynchrones conviennent aux tâches en temps réel comme le scan réseau.
— Editorial Team
Aucun commentaire pour le moment.