Android – Hello JNI

Merhaba arkadaşlar. Uzun bir aradan sonra tekrar blog yazmaya başlıyorum. Bu yazıda e-posta ile gelen sorular üzerine biraz JNI üzerinde duracağız. Bu yazı giriş niteliğinde olup JNI ile ileri düzeye sonraki yazılarda geçeceğiz.

JNI Nedir?

JNI (Java Native Interface), native kodlarla (C, C++) java kodlarının konuşabilmesi için geliştirilmiş bir arayüzdür. JNI ile ilk calışmaya başladığımda kafamda bir karışıklık oluşmuştu. Sizde de oluşmaması için buraya not düşüyorum:

Android platformunun native dili Java’dır. Fakat native kod diye bahsettiğimiz kodlar Java değil JNI kodlarıdır. Bu ikisi farklı bağlamlarda geçerlidir. Bu yazı boyunca native diye bahsettiğimiz kodlar Java değil C ve C++ dilleridir.

JNI Temelde Nasıl Çalışır?

JNI aslında Android’in bir özelliği değildir. Java ile birlikte Android gelen ve Android için sadece C veya C++ dilleriyle yazılan kodlardır. Yazılan bu kodlar Android NDK build ile derlenerek shared object library(.so)‘ler oluşur. Bu shared object library’ler cihazlarda çalışmaya uygun binary kodlar ve derleme flaglerine göre de sembolleri içerir. Bu kütüphaneler çalışma anında Android sanal makinesi contexti (Dalwik veya ART) içerisine yüklenir. Bir Android uygulamasında bulunan her kod sanal makine contextinde çalıştırılır ve bu native kodları sanal makine contextinde çalıştırabilmek JNI kullanırız. JNI ve JNI implementasyonu Android içerisinde gömülü olarak gelir ve native geliştiriciler olarak bu implementasyona ulaşabilmemiz bir header dosyası vardır.

Android tarafından native kütüphanemizi load ettiğimizde, bunun için System.loadLibrary() methodunu kullanabiliriz, native implementasyonumuzda JNI_OnLoad() methodunu yazmışsak bu method tetiklenir. Ardından Java ile yazdığımız methodlar, Android tarafında native olarak tanımladığımız methodlar ile maplenir.

Android tarafından bir native methodu çağırdımızda Java kodlarının çalışması o thread için durdurulur ve native kodlar çalıştırılmaya başlanır. Native kodların çalışması bittiğinde ki bu koşula bir değer döndürmesi de dahildir Java kodlarının çalışması devam eder.

JNIFigure1.gif

Neden JNI Kullanmaya Ihtiyaç Duyarız?

JNI’in kullanılma amaçları temelde şöyle sıralayabiliriz:

  • En geçerli sebep olarak performans¹. C, C++ ile yazılan kodlar çalıştırıldığından dolayı donanım seviyesine Java’dan daha yakındır. Fakat native kodlar da Dalvik içerisinde çalıştırıldığını unutmamak gerekir. Buna rağmen yine de hızlıdır diyebiliriz.
  • Hali hazırda C, C++ ile implemente edilmiş bir kütüphanenizi kullanabilmek için.
  • Bazı durumlar için Android kodlarına göre daha fazla gizlilik sağlayabilir.

Fakat unutulmaması gereken önemli noktalardan biri native kütüphaneler makine kodlarından oluştuğu için uyumluluk açısından çalıştırılmak istenen her işlemci mimarisi için (ABI) derlenmesi gerekir ve bu da uygulama boyutunun artmasına yol açacaktır. Bir diğeri ise Java-native ve native-Java çağrılarının ve geçişlerinin bir maliyetinin olması. Yani JNİ çağrılarının sayısı artıkça performans konusunda kayıpa da yol açabilir².

Merhaba JNI

JNI’i ve çalışma mantığının temelini özetledikten sonra Android ile JNI kullanarak native kodlar içeren “Hello JNI” uygulamamıza geçebiliriz. Projenin kaynak kodlarına GitHub‘dan ulaşabilirsiniz.

Bir Android uygulamasına native kodlar için iki konum bulunur.

Bu konumlardan ilki native kaynak kodları bulundurduğumuz “jni” klasörüdür. Bu klasörün altında Android uygulamamızla birlikte geliştirdiğimiz C, C++ kodları bulunur.

Diğer bir konum ise “jniLibs/CPU_ABI” klasörüdür. Bu klasör altında ise daha önceden derlenen shared object library (.so) kütüphaneleri bulunur. Bu native kütüphaneler, “jniLibs” klasörü altında derlendikleri ABI yapılarına yani özetle işlemci mimarilerine göre klasörlerin içinde bulunmalıdırlar. Android tarafından desteklenen işlemci mimarileri Developer Android‘de belirtildiği gibi aşağıdaki gibidir:

ABI Konum
armeabi jniLibs/armeabi
armeabi-v7a jniLibs/armeabi-v7a
arm64-v8a jniLibs/arm64-v8a
x86 jniLibs/x86
x86_64 jniLibs/x86_64
mips jniLibs/mips
mips64 jniLibs/mips64

Kullandığımız .so kütüphaneleri için durum böyle iken kodlarını jni klasörü altında tuttuğumuz native kütüphanelerimiz için ayarlamayı gradle üzerinden yapabiliriz. Gradle ile NDK Build kullanarak build aldığımız bu native kütüphaneler tek bir .so dosyasında birleştirilecektir. Gradle ile NDK Build ederken modülünüzün build.gradle dosyasına aşağıdaki kodları eklemeniz yeterli olacaktır.

ndk {

    moduleName "hello-jni"
    stl "gnustl_shared"
    abiFilters 'armeabi', 'armeabi-v7a', 'x86'
}

Burada dikkat etmeniz gereken bir nokta mevcut. Eğer jniLibs altına eklediğiniz natıve kütüphane armeabi destekliyorsa ve siz armeabi-v7a ile kendi kütüphanenizi build aldıysanız bu durumda armeabi-v7a destekleyen cihazlarda harici native kütüphanenizi kullanamayacasınız çünkü bu cihaz sadece armeabi-v7a klasörüne bakacaktır, aynı zamanda eklediğiniz harici natıve kütüphane armeabi-v7a destekliyor ve siz kendi kütüphanenizi armeabi ile build adıysanız bu defa da kendi kütüphanenizi kullanamayacaksınız ve uygulamanız her iki durumda da natıve kütüphaneyi yüklerken UnsatisfiedLinkError hatasını alacaksınız. Yani özetle harici kütüphaneniz varsa veya kullandığınız bir kütüphane kendi içerisinde native kütüphane barındırıyor ve siz bunlardan farklı mimaride kendi kütüphanenizi build alırsanız uygulamanız çalışmayacaktır.

Bu bilgilerden sonra örnek uygulamamıza geçebiliriz. Örnek uygulamamızı 3 aşamada oluşturabiliriz.

1. Native Kütüphane Klasörleri

Örnek uygulamamızı Android Studio’nun varsayılan modülü olan ‘app’ ile geliştiriyoruz. “app/main/” dizini altına jni dizinini oluşturuyoruz ve içine ‘hello-jni.cpp’ dosyasını oluşturuyoruz.

2. Kodlama

2.1. Android Java Kodlari

Uygulama açılışında kendi yazdığımız native kütüphanemizin yüklenmesi için “build.gradle” dosyasına yazdığımız ndk modül adıyla oluşan kütüphanemizi yükleyelim.

static{
    // Loading static library
    System.loadLibrary("hello-jni");
}

Java tarafından native tarafta çağıracağımız metodu belirtelim. Bu metodu normal java metodu çağırıyormuş gibi “addAndLongToastNumbers(100, 75)” gibi çağırabiliriz. Bu çağırada JNI araya girerek native tarafta implemente ettiğimiz bu kodu çalıştıracaktır.

private native void addAndLongToastNumbers(int number1, int number2);

Native taraftan Java tarafında çağıracağımız metodumuzu yazalım.

Native taraftan çağırmak için Java tarafına implemente ettiğiniz methodların sonuna, kodunuzun okunabilirliğini artırmak için “__N” ekini ekleyebilirsiniz.

// __N tag is for understanding this method will be called from native side.
private void makeToastLong__N(int result){
    Toast.makeText(this, "Native Long Result is: " + result, Toast.LENGTH_LONG).show();
}

2.1. Native Kodlar

Oluşturduğumuz “hello-jni.cpp” dosyasının içine test kodlarımızı, JNI’a özel değişkenleri ve methodları kullanabilmek header dosyasını ekleyelim.

#include <jni.h>

Java tarafından native tarafta implemente ettiğimiz kodu çağırmak için sadece tanımlamak yeterken native taraftan Java tarafına implemente ettiğimiz metodu çağırmak için:

  • Önce methodun implemente edildiği class’ın referansını JNI methodları ile bulalım.
  • // We should find class before calling its method
    jclass clazz = env->FindClass("com/birfincankafein/hellojni/MainActivity");
  • Class üzerinden çağırmak istediğimiz metodun referansını method adı, parametre ve geri dönüş tipi ile bulalım.
  • // (I)V means accepts int parameters and returns void
    jmethodID method = env->GetMethodID(clazz,"makeToastLong__N","(I)V");

Metodu bulmak için JNI’in “GetMethodID” fonksiyonunu kullanacağız. Bu metodun ilk parametresi metodun tanımlı olduğu class’ın referansı, ikinci parametresi metod adı, üçüncü parametre ise JNI formatlı method imzası. Bu metod imzası “(PARAMETRE_TİPLERİ)GERİ_DÖNÜŞ_TİPİ” şeklinde oluşur. Java tarafındaki genel tiplerin JNI imzaları Oracle’in dokümantasyonuna göre şöyledir:

Java Tipi Native Tipi JNI İmzasi
boolean jboolean Z
byte jbyte B
char jchar C
short jshort S
int jint I
long jlong J
float jfloat F
double jdouble D
void void V
Object jobject L<PAKETADI>
Object[] jobjectArray [L<PAKETADI>
boolean[] jbooleanArray [Z
byte[] jbyteArray [B
char[] jcharArray [C
short[] jshortArray [S
int[] jintArray [I
long[] jlongArray [J
float[] jfloatArray [F
double[] jdoubleArray [D

Java tarafında implemente ettiğiniz metod, bu tiplerden farklı bir tipde veri kabul ediyorsa, örneğin String kabul eden bir metodunuz varsa ve bu method integer dizisi döndürüyorsa method imzası “(Ljava/lang/String)[I” olacaktır.

Metod referansımızı da bulduktan sonra artık metodumuzu çağırabiliriz.

env->CallVoidMethod(instance, method, result);

Java tarafındaki metodları çağırırken JNI headerinda bulunan tanımlı metodları kullanmamız gerekir. Bu methodlar çağırdığımız metodun imzasına, geri dönüş tipine ve static olup olmadığına göre değişkenlik gösterir. Bu methodlar Oracle dokumantasyonunda belirtilmiştir.

Özetle Java tarafından çağıracağımız ve içerisinde Java tarafına implemente ettiğimiz metodu çağıracak kod şu şekilde olacaktır:

#include <jni.h>
#include <stddef.h>

extern "C" {
 JNIEXPORT void JNICALL
 Java_com_birfincankafein_hellojni_MainActivity_addAndLongToastNumbers(JNIEnv *env, jobject instance,
 jint number1, jint number2) {
 int result = ((int) number1) + ((int) number2);
 // We should find class before calling its method
 jclass clazz = env->FindClass("com/birfincankafein/hellojni/MainActivity");
 // (I)V means accepts int parameters and returns void
 jmethodID method = env->GetMethodID(clazz,"makeToastLong__N","(I)V");
 if (method == 0) {
 // prevent crashed from method not found
 return;
 }
 env->CallVoidMethod(instance, method, result);
 }
}

 

3. Gradle Ayarı

Android proje modülümüzün build script dosyası olan build.gradle dosyasında, yazdığımız kütüphanemizi NDK-Build ile derlemek, Java tarafında System.loadLibrary metodunda yazacağımız kütüphane adını belirlemek ve diğer ayarlar için istediğiniz build flavor‘ınızda aşağıdaki kodları kullanabilirsiniz. Örnek projede bir build flavor olmadığı için bu kodları Gradle’da android.defaultConfig altında kullanabiliriz.

ndk {
    moduleName "hello-jni"
    abiFilters 'armeabi', 'armeabi-v7a', 'x86'
}

 

Artık örnek uygulamızı çalıştırabilir ve Toast mesajlarımızı görebiliriz. Eğer jniLibs altında uygun işlemci mimarisi klasörlerinde harici native kütüphaneleriniz varsa bunlar otomatik olarak oluşan apk içerisine dahil edilecektir. Bu harici native kütüphaneleri de System.loadLibrary() kullanarak uygulamanızda kullanabilirsiniz.

Örnek uygulamaya GitHub üzerinden ulaşabilirsiniz. Sormak istediğiniz bir soru olursa bu yazının altına yorum yazabilir veya email adresimden bana e-posta atabilirsiniz. Bir sonraki yazılarda görüşmek dileğiyle. Takipte kalın!

 

Kapak gorseli: https://www.slideshare.net/mauimauer/android-ndk-and-the-x86-platform
Reklamlar

Bir Cevap Yazın

Aşağıya bilgilerinizi girin veya oturum açmak için bir simgeye tıklayın:

WordPress.com Logosu

WordPress.com hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap / Değiştir )

Twitter resmi

Twitter hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap / Değiştir )

Facebook fotoğrafı

Facebook hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap / Değiştir )

Google+ fotoğrafı

Google+ hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap / Değiştir )

Connecting to %s