..

Android NDK orqali nativ kutubxonalar yaratish

Loyihani qo'llab quvvatlash uchub buyerga bosing

BYD uchun O’zbek tilidagi yordamchini integratsiya qilish vaqtida kutilmagan vazifa tushdi, ya’ni shared kutubxonalar qilish. Android ham linux ustiga qurilganligi bois chuxnya masala deb o’ylagandim lekin java sababli linux va kutubxona arxitekturasida o’zgarishlar ham qilishga ehtiyoj bo’lgan ekan.

Menga yuklatilgan vazifada kutubxonani Cda yozish topshirildi, har holda bunday masalalarda ancha qulay til (c++ yanada). Lekin java uchun odatiy name linker berish metodlari boshqacha ekan. Aniqroq aytganda boshqa tillar ffi orqali nativ kutubxonaga murojaat qilsa, javada bu ish jni orqali amalga oshirilarkan. Menda boshlanishida ba’zi bir tushunmovchiliklar bo’ldi, shu sababli oz bo’lsada sizlar bilan bu haqida ulashay dedim.

Kichik nativ kutubxona

Ishni odatiy C da nativ kutubxonada yozishdan boshlasak bu keyinchalik ndk uchun qo’shimcha qilishga yordam beradi.

Demak bizda odatiy kutubxona kodi:

//mylib.c
#include <string.h>

int  add(int a, int b) {
	return a + b;
}

int  getlen( char * str ) {
	return strlen( str );
}

char * genStr() {
	return "Test string!";
}

Kutubxonani kompiliyatsiya qilish:

gcc -fPIC -shared -o libmylib.so mylib.c

Kutubxonadan foydalanish:

//app.c
#include <stdio.h>

int add(int a, int b);
int getlen( char * str );
char * genStr();

int main() {
    int result = add(2, 2);
    printf( "Result: %d\n", result );
    
	printf( "Length: %d\n", getlen( "Hello" ) );

	printf( "String: %s\n", genStr() );
	
    return 0;
}

Kutubxonadan foydalanish uchun app.c faylini kompiliyatsiya qilish:

gcc -L. -lmylib -o app app.c

app faylini ishga tushirish:

Binary faylimiz mylib.so faylini topa olishi uchun avvalo LD_LIBRARY_PATH orqali kutubxona turgan papkani ko’rsatish lozim. Yoki kutubxonani /usr/lib papkasiga ko’chirish orqali ham muammoni hal etish mumkin.

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
./app #ishga tushirish

Kutubxonadan php orqali foydalanib ko’rish:

<?php
$ffi = FFI::cdef("
    int add(int a, int b);
    int getlen(char* str);
    char* genStr();
", "./libmylib.so");

$result = $ffi->add(2, 2);
echo "add(2, 2) = $result\n";

$len = $ffi->getlen("Hello");
echo "getlen('Hello') = $len\n";

$str = $ffi->genStr();
echo "genStr() = " . FFI::string($str) . "\n";
FFI::free($str);
?>

JNI va NDK

Yuqoridagi misolda men kutubxonani x86_64 arxitekturada kompiliyatsiya qilib ko’rganman, o’z navbatida esa bu arm uchun ishlamaydi. Menimcha arm orqali kompiliyatsiya qilish imkoni hammada ham bo’lmasligi mumkin, aynan shunday vaziyatda ndk bizga bu vazifani bajarib undan tashaqari jni interfeysi uchun ulab beradi ham.

ndk o’rnatish

Ndk o’rnatish uchun android studio yoki boshqa narsalarni o’rnatishni o’ylab o’tirmang (bu juda og’ir jarayon). Eng yaxshi usuli google adnroidni o’zidan ndkning cli versiyasini yuklab olish va uni sozlash:

wget https://dl.google.com/android/repository/android-ndk-r26c-linux.zip
unzip android-ndk-r26c-linux.zip -d ~/android-ndk

NDK kerakli papkaga yuklangach undan foydalanish uchun linux path va ANDROID_NDK_HOME o’zgaruvchilarini e’lon qilish kerak. Bu uchun ~/.bashrc faylining oxiriga quyidagi qatorlarni qo’shamiz:

export ANDROID_NDK_HOME=~/android-ndk
export PATH=$PATH:$ANDROID_NDK_HOME

jni uchun kutuxbona kodi:

JNI interterfeysidan foydalanish uchun c kodimizga jni.h header faylini import qilish va funksiya nomlairniyam jni uchun moslash talab qilinadi.

Bunda har bir funksiya quyidagi prefikslar bilan boshlanishi mumkin:

JNIEXPORT jitype JNICALL Java_appid_Class_function
//mylib.c
#include <jni.h>
#include <string.h>

JNIEXPORT jint JNICALL Java_com_example_myapp_MainActivity_add(JNIEnv *env, jobject thiz, jint a, jint b) {
    return a + b;
}

JNIEXPORT jint JNICALL Java_com_example_myapp_MainActivity_getlen(JNIEnv *env, jobject thiz, jstring str) {
    const char *cStr = (*env)->GetStringUTFChars(env, str, NULL);
    jint len = strlen(cStr);
    (*env)->ReleaseStringUTFChars(env, str, cStr);
    return len;
}

JNIEXPORT jstring JNICALL Java_com_example_myapp_MainActivity_genStr(JNIEnv *env, jobject thiz) {
    return (*env)->NewStringUTF(env, "Test string!");
}

NDK ham xuddi gcc kabi makefayldan foydalanish imkoniga ega. Bunda .mk fayllar sodda Makefilega qaraganda ko’proq kompleks yechimlar berib ndk ishini kengaytirib bera oladi.

Demak bizdagi nativ kutubxona uchun Android.mk faylimiz:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := test
LOCAL_SRC_FILES := test.c

include $(BUILD_SHARED_LIBRARY)

Application.mk bu ndk-build uchun sozlamalar berishga foydalanadigan fayl hisoblanadi. Masalan qaysi arxitekturaga kompiliyatsiya qilish, c/c++ sozlamalari qanday bo’lishi kabi holatlar uchun yechim bera oladi.

APP_ABI := all

Bizning holatda APP_ABI ( Application binary interface ) all ko’rsatilgan, bu bir vaqtda barcha qo’llay oladigan arxitekturalar uchun kompiliyatsiya qilishni ko’rsatadi.

Arch ABI
32-bit ARMv7 APP_ABI := armeabi-v7a
64-bit ARMv8 (AArch64) APP_ABI := arm64-v8a
x86 APP_ABI := x86
x86-64 APP_ABI := x86_64

Yuqoridagi fayllar hosil qilingach terminal orqali ndk-build buyrug’ini beradigan bo’lsak bizda java orqali foydalanish mumkin bo’lgan libtest.so fayli hosil bo’ladi.

Kutubxonada foydalanish uchun esa javadan quyidagicha murojaat qilishimiz kerak (Xatolik bo’lsa uzr, men java bo’yicha mutaxassis emasman)

package com.example.myapp;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("test"); // lib prefiksisiz
    }

    public native int add(int a, int b);
    public native int getlen(String str);
    public native String genStr();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        int sum = add(3, 4);
        int length = getlen("Hello JNI");
        String message = genStr();
    }
}