Android studio 2.2 버전부터 NDK가 정식으로 지원되기 시작하면서 기존보다 훨씬 더 간단하게 NDK를 이용하여 네이티브 어플리케이션을 작성할 수 있게 되었습니다. 연구실에서 간단한 사용법을 작성하라고 해서 3달 전에 작성했는데, 요즘 바빠서 글쓸 시간도 없고 이거라도 올리자는 생각이 들어서 그 문서를 여기에 올립니다. 이 글에서는 NDK를 이용하는 것에 초점을 맞췄기 때문에 아주 간단한 앱을 만들어 보겠습니다.
Pre – Condition
1. Android studio 2.2이상이어야 함. (2.1 이하 버전에서도 가능하지만 2.2 버전에서는 NDK가 정식으로 포함되어 매우 편리함.)
2. SDK Tool이 설치되어 있어야함.
1. Android studio 2.1 이하 버전을 사용하고 있다면 Android studio 2.2 버전으로 업그레이드를 한다.
[Help] – [Check for Updates] Click하여 Android 2.2 버전으로 업그레이드.
2. SDK tool을 설치한다.
2-1. SDK manager를 연다.
2-1-(a). 프로젝트 화면에서 [Tool] – [Android] – [SDK Manager]를 클릭 하면 SDK manager가 열린다.
2-1-(b). 위의 빨간 박스 아이콘을 누르면 SDK manager가 열린다.
2-1-(c). [File] – [Close Project]를 누르면 나오는 Welcome 화면에서 [Configure] – [SDK Manager]를 누르면 SDK Manager가 나온다.
2-2. SDK tools를 클릭하고 오른쪽 하단의 Show Package Details 체크 박스를 체크한다.
2-3. 필요한 Tool들인 LLDB, CMake, NDK를 체크한다.
NDK – JNI Test
1. Welcome 화면에서 [Start a new Android Studio project]를 누르거나 프로젝트 화면에서 [File] – [New] – [New project]를 눌러 project 마법사를 실행한다.
2. Project 마법사를 실행하면 Android 2.2 Version에서는 2.1까지는 없던 Include C++ Support라는 체크박스가 생긴다. NDK를 Test 할 것이므로 체크를 하고 Next를 누른다.
3. 다음 화면에서 만들려는 앱의 플랫폼을 선택하고 minimum SDK를 선택한 후 Next를 누른다.
4. [Empty Activity]를 선택하고 Next를 누른다. Add No Activity를 선택하면 기본 layout도 추가가 안돼서 직접 추가해야 하니 귀찮다. Basic Activity는 별로 원하지 않는 메뉴바 등이 추가되니 Empty Activity를 선택한다.
5. Activity 이름과 Layout 이름을 입력하고 다음을 누른다.
6. [Include C++ Support]를 체크 했으므로 Customize C++ support 화면이 나온다. 그냥 Finish를 누른다. 그러면 프로젝트가 만들어진다.
a. C++ Standard : 사용할 C 언어의 표준을 정할 수 있다. 드롭 다운 메뉴를 보면 Toolchain Default가 있고, C++11을 선택할 수 있다. 만약 cpp에서 Smart pointer나 Thread Class 등 C++11에서 지원하는 기능을 사용하거나 사용할 예정이라면 C++11을 선택하고 아니라면 Toolchain Default를 사용한자. Toolchain Default를 선택하면 default CMake setting을 사용한다. Default로는 32-bit, ARM-based GCC 4.8 toolchain을 사용하는데, Processor Architecture 마다 사용할 Toolchain을 선택할 수 있다. 자세한 내용은 Standalone Toolchain을 참고하면 좋다.
b. Exception Support : 이 박스를 check하면 C++ exception handling이 가능하다. 이 박스를 체크하면 Android Studio는 module level(일반적으로 [app] level)의 build.gradle파일에 cmake cppFlags에 -fexceptions라는 속성을 추가하고 Gradle은 Cmake에게 저 속성값들을 전달한다.
c. Runtime Type Information Support: RTTI를 사용하고 싶으면 이 checkbox를 체크한다. 체크 안하고 나중에 필요하다 싶으면 moule level build.gradle파일에 cmake cppFlags에 -frtti라는 속성을 추가하면 된다. (RTTI는 프로그램 실행중에 클래스의 정확한 타입을 알아내는 기능이다. C++의 dynamic_cast 등에서 사용된다. RTTI에 대한 자세한 정보를 알고 싶다면 RTTI를 참고하면 좋다.
7. 나오는 프로젝트를 보면 Hello from C++이 textBox에 출력되는 기능이 구현되어 있는것을 볼수 있다. 이제부터 원하는 기능을 C++로 구현하여 사용하는 방법에 대한 설명이다. 이 예제에서는 간단한 피보나치 함수를 만들어 테스트한다. 왼쪽의 Project Pane에서 app을 클릭하면 일반적인 안드로이드 프로젝트와는 다르게 cpp폴더가 보일 것이다. cpp폴더를 클릭하고 마우스 오른쪽 클릭 [New] – [C/C++ Source File]을 클릭하면 New C/C++ Source File 창이 열린다.
8. Cpp file의 이름을 입력하고 OK를 눌러준다. 아래의 Type의 드롭 다운 메뉴에서 cpp와 header 중 선택 할 수 있으며, Create an associated header check box를 체크하면 header도 자동으로 만들어진다.
9. 아직 sync가 되지 않았으므로 왼쪽 android project pane에서는 보이지 않는다. 왼쪽의 project pane을 Project로 바꿔준다.
10. Project로 바꾼 다음 [app] – [src] – [main] – [cpp]를 보면 pibonacci.cpp가 추가된 것을 볼 수 있다. 이제 이 cpp파일을 cmake에게 알려줘야 한다. [app] – [CMakeLists.txt]를 더블 클릭한다.
11. 내용을 보면 cmake_minimum_required(VERSION 3.4.1)이 있는데 말 그대로 cmake의 최소 버전을 명시하는 것이다. 우리가 주목할 부분은 add_library() 부분이다. Add_library의 사용법은 다음과 같다.
add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
source1 [source2 ...])
<name>에는 library의 이름을 정해주고 두번째 인자는 shared library로 할 지 static library로 할지 정해준다. 두번째 인자는 생략해도 상관이 없다. 그리게 세번째 인자는 source의 경로를 적어준다. CmakeLists.txt 파일이 있는 위치를 기준으로 상대경로로 작성하는 것에 유의 한다. 이 예제 같은 경우에는 add_library 부분을 다음과 같이 수정해준다.
add_library( # Specifies the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp src/main/cpp/pibonacci.cpp)
만약 만든 cpp source를 새로운 라이브러리로 만들고 싶으면 다음과 같이 library를 add_library 함수를 이용하여 추가해주면 된다. 예를 들면 다음과 같다.
add_library(pibonacci-lib SHARED src/main/cpp/pibonacci.cpp)
대신에 나중에 자바에서 사용할 때 해당 라이브러리를 System. Loadlibrary()를 이용하여 다음과 같이 해당 라이브러리를 load해줘야 한다.
Add_library 함수에 대하여 더 자세히 알고 싶으면 링크를 클릭한다.
12. CMakeLists.txt 수정을 완료했으면 [Build] – [Refresh Linked C++ Projects]를 누른다.
13. Project Pane을 Android로 바꾼 다음 [app] – [cpp]를 보면 pibonacci.cpp가 생긴 것을 볼 수 있다.
14. 이제 자바에서 native method를 정의해줘야 한다. MainActivity.java에 정의 할 수도 있지만 개인적으로 MainActivity에 이런 저런 method를 정의하는 것을 좋아하지 않기때문에 Pibonacci라는 class를 따로 만들기로 했다. 다음과 같이 자바의 패키지 디렉토리에 Pibonacci라는 자바 클래스를 만들어준다. (androidTest) 와 (test)가 아닌 아무것도 안 써져 있는 디렉토리에 만들어 준다.
15. 다음과 같이 작성을 해준다.
public class Pibonacci {
public native static int pibonacci(int input);
public static String getPibonacci(int input){
return String.valueOf(pibonacci(input));
}
}
Static을 붙인 이유는 MainActivity에서 객체를 만들지 않고 함수를 콜해주기 위해서 이다. 객체를 만들어서 사용할 거면 static은 생략해도 좋다.
여기서 public native static int pibonacci (int input) 에서 native가 중요한데 저 native는 이 함수가 JNI 함수라는 것을 알려준다. (함수에 대한 자세한 설명은 생략.)
현 상태에서는 pibonacci가 빨간색으로 표시된다. 아직 저 함수를 pibonacci 함수로 구현을 하지 않았기 때문이다. 이제 아까의 pibonacci.cpp로 가서 JNI 함수를 작성해준다. JNI 함수명을 작성할 때는 규칙이 있다.
Java_Package Name_ClassName_functionName 이다. 패키지 이름의 .은 언더바(_)로 대체해서 써준다.
이 프로젝트의 경우는 Java_com_example_medialab_ndktest_Pibonacci_pibonacci 이다.
헷갈리는 경우는 빨간색 글씨 위에 마우스를 살포시 얹으면 다음과 같이 있어야 할 JNI function name과 함께 오류 메시지가 뜬다. 이제 cpp function을 작성하자.
16. 이제 pibonacci 함수를 다음과 같이 구현하면 된다.
#include <jni.h>
extern "C"
jint Java_com_example_medialab_ndktest_Pibonacci_pibonacci(JNIEnv *env, jobject callingObj, jint input){
if(input<=1) return input;
return Java_com_example_medialab_ndktest_Pibonacci_pibonacci(env,callingObj,input-1)
+Java_com_example_medialab_ndktest_Pibonacci_pibonacci(env,callingObj,input-2);
}
함수명을 왜 저렇게 작성하는 지는 전 단계에서 설명을 했다. 여기서 주목해야 할 것은 jint인데 여기서 int 대신에 jint를 써야 함수가 제대로 작동한다. Jint 는 Java Native Method (JNI)의 데이터 타입이다. JNI는 다른 언어로 작성된 코드를 자바에서 호출할 수 있도록 만들어진 규약이다. 이 pibonacci 함수는 Java에서 호출할 용도로 제작하는 것이므로 각 입력 parameter와 return 값은 JNI 데이터 타입을 이용해야 한다. (JNI Data Type에 대해서는 링크를 참조함다.) 함수 안에서는 C/C++ data type을 이용해도 되고, 만약 JAVA에서 호출되지 않고 같은 C/C++함수끼리만 호출되는 함수는 입출력 데이터 타입을 JNI DataType으로 하지 않아도 된다. 대신 입출력이 JNI DataType이 아닌 함수들은 Java에서 사용은 불가하고 같은 C/C++로 구현된 함수에서만 호출이 가능하다. 즉 위의 코드를 다음과 같이 작성해도 완전 똑같이 아주 잘 작동이 된다.
#include <jni.h>
int pib(int input){
if(input<=1) return input;
return pib(input-1)+pib(input-2);
}
extern "C"
jint Java_com_example_medialab_ndktest_Pibonacci_pibonacci(JNIEnv *env, jobject callingObj, jint input){
jint returnVal = pib(input);
return returnVal;
}
extern “C”라고 jni를 쓰는 함수 앞에 붙여줘야 한다. 필요시 Block을 지정하여 사용해도 된다. Extern “C”를 붙이는 이유는 우리가 C#같은 언어에서 C/C++ dll 이나 static library를 사용할 때 extern “C”를 붙여주는 것과 같은 이치인 것 같다. 아니면 JNI가 C를 base로 해서일 수도 있다. 보통 extern의 의미는 분류를 할 때 external linkage를 갖게 하기 위해서다. 즉, 외부 모듈이 링크를 할 수 있게 해주기 위해서다. 하지만 extern “C”를 하면 name mangling을 하지 않기 때문에 다형성 구현에 문제가 생기는데 이 점에 대해서는 더 조사할 필요가 있다. 어쨌든 다음은 JNI에서 C/C++을 구현하는 각각의 예이다.
Code Example 2-1 Implementing a Native Method Using C
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (
JNIEnv *env, /* interface pointer */
jobject obj, /* "this" pointer */
jint i, /* argument #1 */
jstring s) /* argument #2 */
{
/* Obtain a C-copy of the Java string */
const char *str = (*env)->GetStringUTFChars(env, s, 0);
/* process the string */
...
/* Now we are done with str */
(*env)->ReleaseStringUTFChars(env, s, str);
return ...
}
Code Example 2-2 Implementing a Native Method Using C++
extern "C" /* specify the C calling convention */
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (
JNIEnv *env, /* interface pointer */
jobject obj, /* "this" pointer */
const char *str = env->GetStringUTFChars(s, 0);
env->ReleaseStringUTFChars(s, str);
JNIEnv는 JNI interface pointer로써 여러가지 유용한 변환 함수들을 제공한다. 또한 그 다음 오는 jobject 변수는 C/C++에서 this로 생각하면 된다. Python class에서 this를 위해 함수의 parameter로 self를 넘기는 것과 같다. JNI에 대한 자세한 내용은 링크를 참조한다.
17. 이제 MainActivity로 온다. 우리는 native-lib에 source 추가를 했으므로 따로 더 library를 load할 필요가 없다. 다음과 같이 코드를 작성한다. 그리고 [Run] – [Run app]을 누른다.
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText("Pibonacci 10 : "+Pibonacci.getPibonacci(10));
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
18. 결과 화면