Retour à l'accueil

Wrapper JNI C++ pour Android : classes et callbacks

L'article décrit la création de wrappers Java pour les classes C++ dans Android via JNI avec stockage des pointeurs sous forme de long. Implémentation de callbacks asynchrones depuis les threads natifs en utilisant ThreadPool et des références globales faibles. Exemples de code complets et recommandations de gestion de mémoire sont fournis.

Wrappers JNI C++ dans Android : des classes aux callbacks natifs
Advertisement 728x90

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 :

Google AdInline article slot
  • 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écute delete (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 :

Google AdInline article slot
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 :

Google AdInline article slot
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 NewWeakGlobalRef pour les références de longue durée et supprimez-les à temps.
  • Appelez AttachCurrentThread/DetachCurrentThread uniquement 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

Advertisement 728x90

Lire ensuite