메모형식으로 작성한 글입니다..


안드로이드 스튜디오에서 NDK로 C++11과 STL을 사용한 소스를 빌드하는 법.

C++가 아닌 C를 사용할 때는 makefile과 소스코드에서 약간의 차이가 있다. (본문 내에서 설명)



테스트 환경

Android Studio : 2.2.2

Gradle : 2.2.2

buildToolsVersion : 24.0.0




테스트 소스 코드

https://github.com/tibyte/practice-unclassified/tree/master/Android/NDKTest3




NDK 설치

File - Settings - Appearance & Behavior - System Settings - Android SDK

에서 SDK Tools로 들어가면 NDK를 설치할 수 있다.






javah 추가

File - Settings - Tools - External Tools에서 추가를 눌러 javah를 추가한다.

하단의 Tool settings에 들어갈 내용은 아래와 같다.

Program : jdk에 포함되어 있는 javah의 경로

Parameters : -v -jni -d $ModuleFileDir$/src/main/jni $FileClass$

Working directory : $SourcepathEntry$





java코드에서 네이티브 함수 사용

System.loadLibrary()로 사용할 라이브러리를 로드한다.  라이브러리 이름은 임의로 정해도 되는데, 이 글에서는 main.cpp를 만들 것이므로 여기에서는 main으로 정한다.

그리고 사용할 네이티브 함수명을 native키워드를 통해 선언한다.

package kr.tibyte.ndktest3;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends Activity {

static {
System.loadLibrary("main");
}
public native String getNativeText();


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

TextView tv = (TextView)findViewById(R.id.text_view);

tv.setText(getNativeText());
}

} 





JNI용 헤더파일 생성


프로젝트 탭에서 보기 설정을 Project로 한다.




네이티브 함수를 사용한 Java파일을 우클릭하여 External Tools - javah를 클릭한다.

그러면 src/main/jni 경로에 프로젝트_경로.h파일이 생성된다. 






C++ 소스코드 작성

JNI디렉토리 내에 cpp파일을 만들고 소스코드를 작성하는데,

함수 이름에 패키지 경로가 들어있어야 한다. 이 부분이 실제 패키지 경로와 다르면 빌드가 되지 않는다. 아래 예제에서는 테스트 목적으로 STL과 C++11 기능을 사용했다. 

#include <jni.h>

#include <vector>

#include <string>

#include "kr_tibyte_ndktest3_MainActivity.h"


using namespace std;


extern "C" {


JNIEXPORT jstring JNICALL Java_kr_tibyte_ndktest3_MainActivity_getNativeText(JNIEnv *env, jobject obj)

{

    string str = "";

    vector<char> vec;

    vec.push_back('a');

    vec.push_back('b');

    vec.push_back('c');


    for(auto& x : vec) {

        x ^= 32;

        str += x;

    }


    return env->NewStringUTF(str.c_str());


}



이 부분에서 C와 C++의 차이는 다음과 같다.

- 소스코드 확장자 (.c, .cpp)
- C++문법 사용여부
- C++에서는 함수 형식이 JNIEXPORT jstring JNICALL이지만, C에서는 jstring
- JNI함수를 호출할 때 C++에서는 env->NewStringUTF("string") 형식을 사용하지만,
 C에서는 (*env)->NewStringUTF(env, "string"); 형식으로 리턴.

- C++에서 extern으로 네임 맹글링을 어떤 규칙으로 할 것인지 지정(참고링크)





Makefile 설정


jni디렉토리 안에 Android.mk와 Application.mk 파일을 생성하고 각각 아래와 같은 내용을 적는다.


Android.mk

LOCAL_PATH := $(call my-dir)


include $(CLEAR_VARS)


LOCAL_DEFAULT_CPP_EXTENSION := cpp

LOCAL_MODULE    := main

LOCAL_SRC_FILES := main.cpp

LOCAL_LDLIBS    := -llog -latomic

LOCAL_C_INCLUDES := $(LOCAL_PATH)/include-all


include $(BUILD_STATIC_LIBRARY)

C로 빌드할 때에는 LOCAL_DEFAULT_CPP_EXTENSION := cpp가 필요하지 않다.

LOCAL_MODULE과 LOCAL_SRC_FILES는 작성한 C/C++네이티브 소스파일과 관련된 내용을 적는다.


Application.mk

NDK_TOOLCHAIN_VERSION := 4.9

APP_STL := gnustl_static

APP_MODULED := main

APP_ABI := armeabi-v7a

APP_CPPFLAGS += -std=c++11

LOCAL_C_INCLUDES += ${ANDROID_NDK}/sources/cxx-stl/gnu-libstdc++/4.9/include

NDK_TOOLCHAIN_VERSION : NDK설치경로/sources/cxx-stl/gnu-libstdc++ 에 있는 툴체인 버전을 적는다.

APP_STL : 정적 라이브러리를 사용하고 싶을 경우 gnustl_static를, 공유라이브러리를 쓸 대는 gnustl_shared를 적을 수 있다.

APP_ABI : 빌드할 대상 아키텍쳐를 적는다. all로 적으면 모든 대상으로 빌드할 수 있다.

APP_CPPFLAGS : C++11로 빌드하려면 -std=c++11 플래그를 넣는다.

LOCAL_C_INCLUDES : 자신이 가진 버전에 맞는 경로를 적는다. (NDK설치경로 확인)





build.gradle ndk 정보 추가

src디렉토리 내에 있는 defaultConfig안에 다음과 같은 내용을 추가한다.

 ndk {

stl "gnustl_static"

moduleName "main"

}

stl에서 gnustl_static은 Application.mk에서 설정했던 것과 같은 것이고,

moduleName은 Android.mk, Application.mk, java파일에서 썼던 것과 같다.

만약 STL을 사용하지 않을 것이면 stl은 작성하지 않아도 된다..




실행화면

STL vector와 C++11의 auto를 사용한 코드가 정상적으로 빌드된다. 











이 글을 작성했을 때보다 더 나중에 나온

안드로이드 스튜디오 2.2.2로 빌드하는 방법을 새로 작성하였습니다.

http://tibyte.kr/272








개발환경

Windows 10 64bit

Android Studio 1.5.1

jdk 1.8.0_71


target sdk version : 23

min sdk version : 15


기본 설정


- JDK 설치

- JDK 환경변수 (PATH)설정

- 안드로이드 스튜디오 설치

- NDK 다운로드


안드로이드 스튜디오에서 새 프로젝트를 만든다. 

이 포스트에서는 프로젝트 이름을 ndktest로 정한다.


프로젝트 창에서는 Project Files를 선택해야 디렉터리 경로를 보기 쉽다.





File - Project Structure - Android NDK location 에서 다운받은 NDK 경로를 지정한다.

안드로이드 스튜디오를 통해 설치했다면 자동지정된다.






java 어플리케이션 만들기


안드로이드에서 빈 액티비티로 프로젝트를 만들면 TextView하나가 나와있는데,

C/C++ 네이티브 코드에서 보낸 메시지를 여기에 띄워 본다.


프로젝트 디렉터리를 기준으로

app/src/main/res/layout/content_main.xml 

파일에 있는 TextView에게 id를 붙여준다.


    android:id="@+id/textView" 



MainActivity.java 파일에 네이티브 라이브러리를 로드하는 구분과 메서드 선언을 적는다.


  static {

       System.loadLibrary("ndktest");

   }

   public native String getStringFromNative();



onCreate 메서드 내에서는 위에서 선언한 함수를 호출하여 텍스트뷰에 쓴다.


 TextView view = (TextView)findViewById(R.id.textView);

 view.setText(getStringFromNative());



Build - Make Project로 빌드한다.




javah로 JNI용 C++ 헤더파일 제작


이 작업을 하기 전에 먼저 환경변수에 ANDROID_HOME이라는 값을 안드로이드 SDK 경로로 지정해 두면 편하다.



Windows 환경에서 안드로이드 스튜디오를 통해 안드로이드 SDK를 설치했을 때 기본 경로는 C:\Users\유저이름\AppData\Local\Android\sdk 이다.


프로젝트 디렉터리를 기준으로

app/src/main

으로 이동하여 커맨드창을 열고 다음과 같이 javah를 실행한다. (Windows 기준)

도메인 경로와 프로젝트 이름(여기서는 ndktest)는 프로젝트에 맞게 설정.


$ javah -d jni -classpath %ANDROID_HOME%/platforms/android-23/android.jar;%ANDROID_HOME%/extras/android/support/v7/appcompat/libs/android-support-v7-appcompat.jar;%ANDROID_HOME%/extras/android/support/v4/android-support-v4.jar;../../build/intermediates/classes/debug 도메인.경로.ndktest.MainActivity


만약 ANDROID_HOME 경로의 extras/android 디렉터리에 support가 없다면

Tools - Android - SDK Manager - Appearance - System Settings - Android SDK - SDK Tools 로 들어가

Android Support Library 를 설치한다.


성공적으로 실행되었다면

app/src/main/jni 에 헤더파일(.h)이 생성된다.




C++ 네이티브 코드 작성


간단한 C++ 코드를 작성한다.

여기서 주의할 점은 위에서 생성한 헤더파일을 로드해야하고, 함수 이름도

Java_도메인_경로_프로젝트이름_Java클래스명_Java메서드명 형식으로 되어야 한다는 것이다.


JNIEXPORT jstring JNICALL Java_kr_tibyte_ndktest_MainActivity_getStringFromNative(JNIEnv *env, jobject obj)


아래 코드에 있는 도메인 (kr_tibyte)는 프로젝트에 맞게 고친다.


#include "kr_tibyte_ndktest_MainActivity.h"

#include <string>

#include <sstream>


using namespace std;


JNIEXPORT jstring JNICALL Java_kr_tibyte_ndktest_MainActivity_getStringFromNative(JNIEnv *env, jobject obj) {


    stringstream ss;

    for(int i=1; i<=9; i++) {

        for (int j=1; j<=9; j++) {

            int n = i*j;

            ss << " ";

            if(n < 10) ss << "0";

            ss << n;

        }

        ss << endl;

    }


    string str = ss.str();

    return env->NewStringUTF(str.c_str());

}






makefile(*.mk) 설정


app/src/main/jni 안에 Android.mk를 만든다.

LOCAL_PATH := $(call my-dir)


include $(CLEAR_VARS)


LOCAL_MODULE    := ndktest

LOCAL_SRC_FILES := main.cpp

LOCAL_LDLIBS := -llog


include $(BUILD_SHARED_LIBRARY) 


LOCAL_MODULE은 생성될 네이티브 모듈의 이름,

LOCAL_SRC_FILES는 모듈을 만드는 데 사용될 소스파일들의 목록이다.



app/src/main/jni 안에 Application.mk를 만든다.


APP_ABI := armeabi

APP_STL := gnustl_static


여기서는 컴파일을 위한 옵션 플래그들을 지정할 수 있다.

APP_STL을 지정하여 C++에서 STL 요소들을 사용할 수 있다.


APP_ABI는 ABI(Application Binary Interface)를 지정할 수 있는데 armeabi, armeabi-v7a, mips, x86등이 있으니 필요에 따라 설정한다.






gradle 설정

app/build.gradle을 수정한다.


apply plugin: 'com.android.application'


android {

    compileSdkVersion 23

    buildToolsVersion "23.0.2"


    defaultConfig {

        applicationId "kr.tibyte.ndktest"

        minSdkVersion 15

        targetSdkVersion 23

        versionCode 1

        versionName "1.0"

    }

    buildTypes {

        release {

            minifyEnabled false

            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

        }

        debug {

            jniDebuggable true

        }

    }


    sourceSets.main {

        //네이티브 라이브러리 경로

        jniLibs.srcDir 'src/main/libs'

       //JNI소스 경로

        jni.srcDirs = []

    }


   //안드로이드 스튜디오에서 Java 소스를 빌드할 때 buildNative task를 실행시킴

    tasks.withType(JavaCompile) {

        compileTask -> compileTask.dependsOn buildNative

    }


   //네이티브 소스 빌드

    task buildNative(type: Exec, description: 'Compile JNI source via NDK') {

        def ndkDir = android.ndkDirectory

        commandLine "$ndkDir/ndk-build.cmd", 'NDK_DEBUG=1', '-C', file('src/main').absolutePath

    }


    //네이티브 라이브러리 삭제

    task cleanNative(type: Exec, description: 'Clean JNI object files') {

        def ndkDir = android.ndkDirectory

        commandLine "$ndkDir/ndk-build.cmd", '-C', file('src/main').absolutePath, 'clean'

    }

    

    clean.dependsOn 'cleanNative'


}

dependencies {

    compile 'com.android.support:appcompat-v7:23.1.1'

    compile 'com.android.support:design:23.1.1'

}





결과


이제 애플리케이션을 빌드하고 기기에서 실행시키면

C++ 코드에서 처리한 결과가 MainActivity의 TextView로 표시된다.





코드 모음


https://github.com/tibyte/practice/tree/master/android/ndktest





참조


https://stackoverflow.com/questions/21096819/jni-and-gradle-in-android-studio

http://stackoverflow.com/questions/21999829/how-do-i-read-properties-defined-in-local-properties-in-build-gradle

http://stackoverflow.com/questions/21096819/jni-and-gradle-in-android-studio

http://i5on9i.blogspot.kr/2015/02/android-studio-ndk-hello-world.html

https://www.davidlab.net/ko/tech/using-the-android-ndk-with-android-studio-part1/





+ Recent posts