IT/공부 정리

[Android/NDK] JNI폴더를 이용한 NDK생성 오류 해결

rinix_x 2021. 12. 18. 05:48
반응형
더보기

안녕하세요 :) rini입니다!

프로젝트를 진행하면서 꼭 JNI를 사용해야 할 상황이 생겨 기록 겸 작성하고자 합니다.


 

JNI

JNI란 Java Native Interface의 약자로서, 자바 외의 다른 언어들(native 한...)과 자바 사이에 연결을 위한 인터페이스를 뜻합니다. 말 그대로, 자바에서 C/C++를 사용할 수도 있고, 반대로 C/C++에서 자바를 사용할 수도 있습니다.

 

 

안드로이드에서 NDK(Native Development Kit)를 제공하며, Native언어를 사용할 수 있도록 지원하고 있다고 합니다.

이를 이용하여, 다음으로 사용 방법을 알아보죠!

 

NDK 설치 및 환경설정

먼저 안드로이드를 실행한 후, 

SDK Manager 실행

상단 맨 오른쪽쯤에 위치한 빨간 동그라미로 표시한 버튼을 클릭하여 SDK Manager를 띄우거나, Tools> SDK Manager를 이용하여 창을 띄워줍니다.

 

SDK Manager창이 켜지면 다음과 같은 화면을 볼 수 있는데,

1. SDK Tools탭 선택

2. NDK , CMake 체크

(이때 4번을 클릭하여 세부적으로 필요한 부분을 고르면 더욱 좋습니다. 특히 NDK 버전은 22.1.7171670을 추천합니다.)

3. OK를 눌러 설치

 

sdk Manager

이제 NDK tools 설치는 끝! 이어서 환경설정을 할 겁니다.

간단히 NDK Tool의 위치를 잡아주면 됩니다. 그러고 나서

File> Project Structure로 들어가 변경시켜 주면 됩니다.

Project Structure 창

가운데 비어있는 NDK location을 변경시켜 주면 되는데, 저 같은 경우엔 저기서 변경을 할 수 없어서

local.properties에서 추가

Project로 봤을 때, local.properties에 들어가면,

sdk.dir만 되어있었어요. 그래서 거기에 ndk의 위치를 추가해주었답니다.

보통 sdk폴더 바로 하위 폴더에 존재하는데,

bundle이 없다면, Project Structure창에서 Download Android NDK 부분을 눌러주어 새로 설치하면 됩니다.

그렇게 ndk-bundle로 경로를 설정해주면 됩니다. 

수정이 필요해요.. 지금 local.properites가 추후엔 없어질 거라 직접 입력하라는데 왜 안 되는 건지..ㅠㅠ

 

준비는 다 했고,

이제 간단한 예제를 통해 사용법을 알아볼 것이에요.

반응형

JNI 사용 예제

예제로 간단한 덧셈 함수를 호출하는 것을 해볼 것인데,

먼저 프로젝트 하나 생성하여 레이아웃부터 바꿔줍니다.

activity_main.xml인데,

그대로 복사 붙여 넣기 해주는 것보단 LinearLayout으로 수정해주고, gravity를 center로 주고,

TextView에 id를 추가해주면 됩니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/txt_result"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:textSize="30dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</LinearLayout>

이어서 MainAcitvity.java코드입니다. 

이것은 package 빼고 코드를 쓰면 될 것 같아요.

package com.example.pcltest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    public TextView txt_result;

    static {
        System.loadLibrary("jniCalculator");
    }

    public native int getSum(int num1, int num2);
	
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        txt_result = (TextView) findViewById(R.id.txt_result);

        int num1 = 10;
        int num2 = 20;

        int sum = getSum(num1, num2);
        txt_result.setText("JNI Sample :: 합계 : " + sum);
    }
}

System.loadLibrary("jniCalculator")는 후에 모듈을 호출하도록 도와준다고 해요. 이에 나중에 gradle단계에서 사용할 라이브러리 이름을 지어주면 됩니다!

 

이제 프로젝트 준비는 끝났고, JNI폴더를 생성해줍니다.

JNI폴더 생성

생성하는 방법은 두 가지인데, 

1. Directory에서 Jni라는 이름으로 생성.

2. Folder> JNI Folder 선택.

저 같은 경우, 두 번째를 했습니다.

 

이제 소스코드를 작성해야 해요.

소스 코드 생성

Jni폴더에서 new> C/C++ Source File을 선택하여 생성합니다.

나는 Calculator로 생성하였습니다.

이제 Calculator.cpp코드를 보면,

#include<Calculator.h>

JNIEXPORT jint JNICALL Java_com_example_pcltest1_MainActicty_getSum
    (JNIEnv *env, jobject thiz,jint num1, jint num2){
    return num1 + num2;
    }

로 작성합니다.. 간단하게 두 개의 숫자를 받아 합산 결과를 리턴하는 함수입니다.

 

이때, JNIEXPORT <리턴 타입> JNICALL Java_<패키 지명>_<클래스명>_<함수명>

이런 식으로 "."대신 "_"으로 대신해서 작성하면 돼 요

 

헤더 파일은 설정을 통해 자동으로 헤더 파일을 생성해주도록 툴을 만들어서 사용할 것입니다.

이 방법이 안되면 따로 직접 생성해도 되지만, 웬만하면 이렇게 생성하는 것을 추천합니다.

먼저 File> Settings 창을 켭니다.

Settings 화면

1,2,3 순서대로 Tools> External Tools>+ 를 눌러

다음과 같이 채워줍니다. Group이나 Desciription은 원하는 대로 변형해도 돼요.

Program은 java에 있는 javah파일의 경로를 작성하면 되는데, *JDK 8 버전부터 Javah가 Javac로 대체되었다고 합니다!

Arguments는 클래스 경로와 jni폴더 경로를 작성하면 되는데, 후에 에러가 나면 

에러 발생시

위처럼 직접 MainActivity의 위치를 넣어주면 됩니다.

working directory에는 Jni폴더 경로를 작성하면 됩니다.

작성 후에 OK를 눌러 마친 뒤에 헤더 파일을 추가해줍니다.

MainActivity.java;

1번을 클릭하여 빌드를 하면 2번처럼 자동으로 헤더 파일이 생성된 것을 볼 수 있어요. 이 이름은 수정해도 됩니다.

헤더 파일을 보면 Calculator.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_pcltest_MainActivity */

#ifndef _Included_com_example_pcltest_MainActivity
#define _Included_com_example_pcltest_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_pcltest_MainActivity
 * Method:    getSum
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_com_example_pcltest_MainActivity_getSum
  (JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

다음과 같이 작성된 것을 볼 수 있습니다. 아까 작성했던 소스파일에 헤더를 include 하도록 소스를 추가해 줍니다.

Calculator.cpp

#include<Calculator.h>

JNIEXPORT jint JNICALL Java_com_example_pcltest1_MainActicty_getSum
    (JNIEnv *env, jobject this,jint num1, jint num2){
    return num1 + num2;
    }

헤더 파일을 지정해둔 이름에 맞게 include 하면 됩니다.

 

NDK생성 과정

NDK를 생성하기까지 이제 얼마 안 남았습니다!!

위 코드들을 다 묶어주기 위해 Android.mk 파일을 작성합니다. jni폴더에서 New> File로 생성한 뒤에 작성하면 됩니다.

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := jniCalculator
LOCAL_SRC_FILES := Calculator.cpp
LOCAL_LDLIBS := -llog

include $(BUILD_SHARED_LIBRARY)

각 내용을 설명해보자면

 

더보기

LOCAL_PATH
- 소스 파일의 위치를 설명합니다. 작성된 매크로 함수를 그대로 사용하시면 됩니다.

CLEAR_VARS
- LOCAL_XXX 변수들을 자동으로 지워주는 역할을 합니다. 각각의 모듈을 설명하기 전에 선언해야 합니다.

LOCAL_MODULE
- 모듈의 이름을 설명합니다. 이전에 MainActivity에서 작성하셨던 이름과 동일하게 맞춰주시면 됩니다.


LOCAL_SRC_FILES
- 모듈에 추가할 소스파일의 이름을 설명합니다. 좀 전에 작성하신 C언어 파일 이름을 넣어주시면 됩니다.


LOCAL_LDLIBS
- 바이너리 빌드에 사용할 빌드 시스템의 외부 라이브러리 목록을 설명합니다. 각 라이브러리 이름 앞에 -l(link-against) 옵션이 선행된다. 그 뒤의 log는 로깅 라이브러리를 뜻합니다.


BUILD_SHARED_LIBRARY
- 가장 최근의 include 이후로 정의된 LOCAL_XXX 변수에 정의된 정보들을 수집하는 스크립트를 설명합니다.


좀 더 상세한 내용은 디벨로퍼 문서를 참고하시면 좋을 것 같습니다.
Android.mk Developer Guide
https://developer.android.com/ndk/guides/android_mk.html?hl=ko

 

다음은 Application.mk 파일을 생성해야 합니다.

이 부분은 파일 자체가 필요가 없을 수도 있는 것이라 Developer Guide문서를 참고하여 보는 것이 좋습니다.

APP_ABI := all

 나는 정확한 CPU 아키텍처를 몰라서 이렇게 지정해주었지만, 무조건 따라 하지 않고 문서를 참고하여 작성하는 것을 추천합니다.

 

build.gradle(Module:app)에 소스를 추가하면 됩니다.

import org.apache.tools.ant.taskdefs.condition.Os
plugins {
    id 'com.android.application'
}

def getNdkBuildPath() {
    Properties properties = new Properties()
    properties.load(project.rootProject.file('local.properties').newDataInputStream())

    def command = properties.getProperty('ndk.dir')
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        command += "\\ndk-build.cmd"
    } else {
        command += "/ndk-build"
    }

    return command
}

android {
    compileSdkVersion 28

    defaultConfig {
        applicationId "com.example.pcltest"
        minSdkVersion 28
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    sourceSets.main {
        // Compile된 Native Library가 위치하는 경로를 설정합니다.
        jniLibs.srcDir 'src/main/libs'
        // 여기에 JNI Source 경로를 설정하면 Android Studio에서 기본적으로 지원하는 Native
        // Library Build가 이루어집니다. 이 경우에 Android.mk와 Application.mk를
        // 자동으로 생성하기 때문에 편리하지만, 세부 설정이 어렵기 때문에 JNI Source의
        // 경로를 지정하지 않습니다.
        jni.srcDirs = []
    }
    ext {
        // 아직은 Task 내에서 Build Type을 구분할 방법이 없기 때문에 이 Property를
        // 이용해 Native Library를 Debugging 가능하도록 Build할 지 결정합니다.
        nativeDebuggable = true
    }
    // NDK의 ndk-build 명령을 이용하여 Native Library를 Build하기 위한 Task를 정의합니다.
    //noinspection GroovyAssignabilityCheck
    task buildNative(type: Exec, description: 'Compile JNI source via NDK') {
        if (nativeDebuggable) {
            commandLine getNdkBuildPath(), 'NDK_DEBUG=1', '-C', file('src/main').absolutePath
        } else {
            commandLine getNdkBuildPath(), '-C', file('src/main').absolutePath
        }
    }
    // App의 Java Code를 Compile할 때 buildNative Task를 실행하여 Native Library도 같이
    // Build되도록 설정합니다.
    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn buildNative
    }
    // NDK로 생성된 Native Library와 Object를 삭제하기 위한 Task를 정의합니다.
    //noinspection GroovyAssignabilityCheck
    task cleanNative(type: Exec, description: 'Clean native objs and lib') {
        commandLine getNdkBuildPath(), '-C', file('src/main').absolutePath, 'clean'
    }
    // Gradle의 clean Task를 실행할 떄, cleanNative Task를 실행하도록 설정합니다.
    clean.dependsOn 'cleanNative'
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

간혹 맨 아래 dependecies 첫 줄에서 오류가 발생할 때가 있었는데, 프로젝트의 안드로이드 버전과 맞지 않아 발생한 오류였습니다. 이는 28을 맞는 버전에 맞춰 바꾸거나

projectStructure 창

ProjectStructure의 Modules의 compile Sdk Version과 Build Tools Version을 맞게 변경하면 됩니다. 대신 프로젝트 버전을 변경했을 경우 맨 윗부분에 android에서 sdk버전을 맞춰줘야 합니다.

이제 실행해보면 됩니다.

Build> Make project로 먼저 빌드하고 실행하는 것을 추천합니다.

 

오류 메모

보통 잘 나와야 하는데 저는 여기서 오류가 발생하여 멈춰 있습니다. 

더보기

Execution failed for task ':app:buildNative'.

> process 'command 'C:users\user\AppData\Local\Android\Sdk\ndk-bundle\ndk-build.cmd'

프로젝트 빌드 후 첫 오류

ndk-bundle위치를 다시 확인해보는 게 보통이며,

저 같은 경우는 코드 문제였습니다.. (스펠링 실수.. 였어요) 위에 수정된 것이니.. 걱정 하시마세요..

프로젝트 빌드는 잘 되는데, 아래와 같은 오류가 빌드 중에 발생합니다..

더보기

Bad file descriptor

APP_PLATFORM not set. Defaulting to minimum supported version android-16.

WARNING: APP_PLATFORM android-16 is higher than android:minSdkVersion 1 in./AndroidManifest.xml.

buildNative error

AndroidManifest.xml에서 

<uses-sdk
    android:minSdkVersion="28"
    android:targetSdkVersion="28" />

를 추가해주면 됩니다.ㅠㅠ

mergeDebugNativeLibs error

Application.mk설정을 해주니 사라졌어요!

실행 오류

여전히 안됩니다 왜지 ㅎ

찾아보니

계속 내부적으로 돌고 있어서 그런 거 같아요..

제 안드로이드 문제라.. 고칠 수 없는 거지 다른 컴퓨터에선 잘 작동될 겁니다 ㅠㅠ

실행 화면

아래와 같이 나오면 잘한 겁니다 :)

 

참고한 블로그는 https://re-build.tistory.com/7

입니다!

반응형