Zurück zur Startseite

JNI C++-Wrapper für Android: Klassen und Callbacks

Der Artikel beschreibt das Erstellen von Java-Wrappers für C++-Klassen in Android über JNI mit Speichern von Zeigern als long. Implementierte asynchrone Callbacks aus nativen Threads mit ThreadPool und schwachen globalen Referenzen. Vollständige Code-Beispiele und Empfehlungen zur Speicherverwaltung werden bereitgestellt.

C++ JNI-Wrapper in Android: von Klassen zu nativen Callbacks
Advertisement 728x90

JNI-Wrapper für C++-Klassen in Android: Erstellung und Callbacks

Die Integration von objektorientiertem C++-Code in eine Android-App über JNI erfordert die Umwandlung in C-Funktionen, während ein Zeiger auf das Objekt gespeichert wird. Die Adresse des C++-Objekts wird als long in einer Java-Klasse gespeichert, was Lebenszyklusverwaltung und mehrere Instanzen ermöglicht.

Betrachten Sie eine einfache C++-Klasse:

class NativeClass {
public:
    NativeClass() {}
    ~NativeClass() {}
    void fun() {
        __android_log_write(ANDROID_LOG_ERROR, log_tag, "Hello from native");
    }
};

JNI-Funktionen für die Arbeit mit der Klasse:

Google AdInline article slot
  • Initialisierung: jlong Java_com_myprog_example_Native_initNativeClass(JNIEnv* env, jobject thiz) gibt (jlong) new NativeClass(); zurück
  • Methodenaufruf: void Java_com_myprog_example_Native_NativeClassFun(JNIEnv env, jobject thiz, jlong cppClass) ruft ((NativeClass) cppClass)->fun(); auf
  • Zerstörung: void Java_com_myprog_example_Native_destroyNativeClass(JNIEnv env, jobject thiz, jlong cppClass) führt delete (NativeClass) cppClass; aus

Java-Wrapper:

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

Implementierung von Callbacks aus nativem Code

Für asynchrone Szenarien, wie Netzwerkscans, muss nativer Code Java-Methoden während des Betriebs aufrufen. Eine Listener-Schnittstelle in C++ und ein ThreadPool werden verwendet, um Aufrufe in einem einzelnen Thread zu serialisieren.

Erweiterte C++-Klasse mit Callback-Unterstützung:

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

Java-Schnittstelle und Wrapper:

public interface NativeListener {
    void print(String str);
}

public class NativeClass {
    long cppClass;
    // Konstruktor, finalize, free wie oben
    public void scan() { Native.NativeClassScan(cppClass); }
    public void stop() { Native.NativeClassStop(cppClass); }
    public void setListener(NativeListener listener) {
        Native.NativeClassSetListener(cppClass, listener);
    }
}

Schlüsselkomponente – Implementierung von Listener in C++ mit 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);
            }
        });
    }
};

JNI-Funktion zum Setzen des Listeners:

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

Implementierungsempfehlungen

  • Single-threaded ThreadPool: Stellt sequenzielle JNI-Aufrufe von einem festen Thread sicher.
  • Schwache Referenzen (jweak): Verhindern Speicherlecks; starke lokale Referenzen werden nur für die Dauer des Aufrufs erstellt.
  • Lebenszyklusverwaltung: Der Listener-Destruktor bereinigt Ressourcen über den ThreadPool.

Liste wichtiger Praktiken:

  • Immer auf NULL bei lokalen Referenzen vor der Verwendung prüfen.
  • NewWeakGlobalRef für langlebige Referenzen verwenden und zeitgerecht löschen.
  • AttachCurrentThread/DetachCurrentThread nur innerhalb des ThreadPool aufrufen.
  • Für synchrone Callbacks Bedingungsvariablen hinzufügen.
  • UI-Thread für blockierende Operationen wie scan() vermeiden.

Wichtige Erkenntnisse

  • Das Speichern eines C++-Objektzeigers als long in Java bietet volle Objektsemantik.
  • Ein single-threaded ThreadPool ist entscheidend für stabile JNI-Aufrufe von nativen Threads.
  • Schwache globale Referenzen (jweak) + lokale Referenzen minimieren Speicherleck-Risiken.
  • Java-Wrapper-Finalisierung bereinigt native Ressourcen automatisch.
  • Asynchrone Callbacks eignen sich für Echtzeitaufgaben wie Netzwerkscans.

— Editorial Team

Advertisement 728x90

Weiterlesen