Volver al inicio

Wrapper JNI de C++ para Android: clases y callbacks

El artículo describe la creación de wrappers Java para clases de C++ en Android mediante JNI con almacenamiento de punteros como long. Implementados callbacks asíncronos desde hilos nativos usando ThreadPool y referencias globales débiles. Ejemplos de código completos y recomendaciones de gestión de memoria proporcionadas.

Wrappers JNI de C++ en Android: desde clases hasta callbacks nativos
Advertisement 728x90

Wrapper JNI para Clases C++ en Android: Creación y Callbacks

Integrar código C++ orientado a objetos en una aplicación Android mediante JNI requiere convertirlo en funciones C mientras se almacena un puntero al objeto. La dirección del objeto C++ se guarda como un long en una clase Java, permitiendo la gestión del ciclo de vida y múltiples instancias.

Considera una clase C++ básica:

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

Funciones JNI para trabajar con la clase:

Google AdInline article slot
  • Inicialización: jlong Java_com_myprog_example_Native_initNativeClass(JNIEnv* env, jobject thiz) devuelve (jlong) new NativeClass();
  • Llamada a método: void Java_com_myprog_example_Native_NativeClassFun(JNIEnv env, jobject thiz, jlong cppClass) llama a ((NativeClass) cppClass)->fun();
  • Destrucción: void Java_com_myprog_example_Native_destroyNativeClass(JNIEnv env, jobject thiz, jlong cppClass) ejecuta 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);
    }
}

Implementación de Callbacks desde Código Nativo

Para escenarios asíncronos, como el escaneo de red, el código nativo debe llamar a métodos Java durante la operación. Se utiliza una interfaz Listener en C++ y un ThreadPool para serializar llamadas en un solo hilo.

Clase C++ extendida con soporte para 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;
    }
};

Interfaz Java y wrapper:

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

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

Componente clave — implementación de Listener en C++ con 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);
            }
        });
    }
};

Función JNI para establecer el 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));
}

Recomendaciones de Implementación

  • ThreadPool de un solo hilo: Garantiza llamadas JNI secuenciales desde un hilo fijo.
  • Referencias débiles (jweak): Previenen fugas de memoria; las referencias locales fuertes se crean solo durante la llamada.
  • Gestión del ciclo de vida: El destructor de Listener limpia recursos mediante el ThreadPool.

Lista de prácticas clave:

  • Siempre verifica NULL en referencias locales antes de usarlas.
  • Usa NewWeakGlobalRef para referencias de larga duración y elimínalas a tiempo.
  • Llama a AttachCurrentThread/DetachCurrentThread solo dentro del ThreadPool.
  • Para callbacks síncronos, añade variables de condición.
  • Evita el hilo de UI para operaciones bloqueantes como scan().

Conclusiones Principales

  • Almacenar un puntero a objeto C++ como long en Java proporciona semántica completa de objetos.
  • Un ThreadPool de un solo hilo es crucial para llamadas JNI estables desde hilos nativos.
  • Referencias globales débiles (jweak) + referencias locales minimizan riesgos de fugas de memoria.
  • La finalización del wrapper Java limpia automáticamente recursos nativos.
  • Los callbacks asíncronos son adecuados para tareas en tiempo real como escaneo de red.

— Editorial Team

Advertisement 728x90

Leer después