안드로이드 NDK를 사용하기 이전에 이 툴을 왜 사용하는 지부터 생각을 해봐야한다. 게임이나 머신러닝과 같이 고성능의 계산을 요하는 프로그램들은 Java나 Kotlin으로 만들었을 때 퍼포먼스가 만족스럽게 나오지 않을 수가 있다. 그러나 C++ 네이티브 코드로 제작할 경우 안드로이드 플랫폼 내에서 최대한의 퍼포먼스를 낼 수 있게 할 수 있을 것이다.
따라서 계산 성능을 요하는 프로그램을 작성할 때에는 NDK를 활용하여 작성하는 것이 좋을 것이다.
이번 튜토리얼에서는 입문인만큼 Android NDK 프로젝트를 생성하는 법, C++ 코드를 작성하여 Build를 하는 법(CMake를 활용하여)에 대해 다룰 것이다.
기존에 안드로이드 프로젝트를 Empty Activity로 시작했던 것과는 달리 Native C++로 시작한다. 표준은 자기가 원하는 버전(C++11, C++14, C++17 중 하나)으로 설정하면 된다.
기존 Kotlin 프로젝트와 달리 몇 가지 추가된 점이 있다. 한 번 살펴보도록 하자.
cpp
패키지
기존에 Java/Kotlin 파일을 담는 패키지에다 cpp
파일을 담을 수 있는 패키지가 있다. 내부를 살펴보면 cpp
코드와 CMakeLists
라는 파일이 있다.
CMakeLists
안드로이드에서 Java나 Kotlin 코틀린 파일들은 gradle 파일의 설정을 통해 빌드가 되는 것을 알고 있을 것이다. CMakeLists는 C++ 파일들의 빌드를 관리하는 CMake
의 빌드스크립트 파일이다. 지금 이 글을 작성하고 있는 필자도 깊게 공부하지 않아 자세히는 모르지만 조금 더 자세히 공부하여 이후 게시글에 올릴 계획이다.
다음과 같은 코드들이 추가되었다.
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags "-std=c++17"
}
}
}
...
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
...
}
cppFlags
는 C++ 표준을 설정하는 태그이다.
cmake
는 빌드스크립트 파일인 CMakeLists의 절대 경로와 버전을 설정하는 태그이다.
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Example of a call to a native method
findViewById<TextView>(R.id.sample_text).text = stringFromJNI()
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
external fun stringFromJNI(): String
companion object {
// Used to load the 'native-lib' library on application startup.
init {
System.loadLibrary("native-lib")
}
}
}
기존의 메인 Activity에서는 두 가지 부분이 눈에 띄인다. 바로 external 함수와 Library를 load하는 부분인데 이는 다음과 같다.
System.loadLibrary("native-lib"): 여기서 Android 플랫폼이 어떻게 C++ 코드를 사용하는 지 대략적으로 알 수 있다. C++ 코드는 네이티브 코드로 안드로이드 플랫폼에 직접 접근을 하는 것이 불가능하다. 그래서 C++ 빌드 파일을 라이브러리 파일로 변환해 System.loadLibrary() 함수를 이용하여 플랫폼에 적재, 내부의 네이티브 함수를 사용할 수 있도록 한 것이다.
[TMI]: 라이브러리 파일은 파일명에 lib 접두사와 .so 확장자가 붙는다. 만약 native-lib가 파일명이면 라이브러리 파일 명은 libnative-lib.so
가 된다.
external function: 라이브러리 내부에 다음과 같은 함수명이 있다는 것을 JVM에게 알려주기 위해 정의한 것이다.
native-lib.cpp
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_package_ndktutorial_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
기존 C++ 코드와 다른 점이 몇 가지가 눈에 띄인다 그러나 걱정하지 않아도 될 것은 대부분의 함수 형태를 안드로이드에서 템플릿 형태로 주기 때문에 함수 작성은 의외로 불편하지 않고 기존에 C/C++을 작성하는 것과 같이 작성해도 된다는 것이다.
extern "C": cpp 파일에서도 c 소스파일을 사용할 때가 있다. C++에서는 언어 자체에서 다형성을 제공하기에 클래스 속성을 활용하여 기존 함수/변수의 이름을 바꾸는 일이 비일비재하다. 그러나 extern "C"를 사용하면 해당 함수의 이름은 변형(이를 맹글링이라 한다)되지 않고 그대로 사용할 수 있음을 보장받는다. 따라서 외부 파일에서 이 함수를 사용하더라도 그대로 사용할 수 있음을 보장받는다.
jstring: JNI(자바와 외부 언어를 연결할 수 있게 하는 인터페이스)에서 사용하는 데이터 구조로, 자바 문자열의 포인터를 가리키는 포인터 타입이다.
Java_com_package_ndktutorial_MainActivity_stringFromJNI: Native C++의 함수 이름은 길어서 무서워 보이지만 사실은 함수 위치의 절대 경로이다. 이를 해석해보면 stringFromJNI
라는 함수는 java > com > package > ndktutorial > MainActivity
라는 파일에 있다는 것이다.
JNIEnv*: Virtual Machine을 가리키는 포인터이다.
jobject: 자바에서 this 객체를 가리키는 포인터라고 생각하면 된다.
이 함수는 jstring
변수를 반환하기를 요청했으므로 버추얼 머신을 통해 String 객체를 return한다.
1학년 C++ 시간에서 접할법한 간단한 계산기(클래스 간단 실습)를 구현한다
Calculator.h
//
// Created by Hyun Woo Lee on 12/14/20.
//
#ifndef NDKTUTORIAL_CALCULATOR_H
#define NDKTUTORIAL_CALCULATOR_H
class Calculator {
private:
int mNum;
public:
Calculator();
Calculator(int num);
int getAdd(const int& num);
int getMinus(const int& num);
~Calculator();
};
#endif //NDKTUTORIAL_CALCULATOR_H
Calculator.cpp
//
// Created by Hyun Woo Lee on 12/14/20.
//
#include "Calculator.h"
Calculator::Calculator()
: mNum(2) {
}
Calculator::Calculator(int num)
: mNum(num) {
}
int Calculator::getAdd(const int &num) {
return mNum + num;
}
int Calculator::getMinus(const int &num) {
return mNum - num;
}
Calculator::~Calculator() {
}
이렇게 클래스를 만들었다면, CMakeLists에 소스코드를 등록해야한다. 결국엔 빌드는 CMake를 통해 하는 것이기 때문이다.
CMakeLists.txt
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.cpp
Calculator.h
Calculator.cpp)
기존 공유 라이브러리에 내가 만든 소스코드를 추가한 후 Build > Refresh Linked C++ Projects를 누르면 C++ 파일과 Android 파일이 링크가 된다.
마지막으로 기존 라이브러리 코드에 우리가 만든 C++ 클래스를 활용한 함수를 만들어보자
extern "C" JNIEXPORT jstring JNICALL
Java_com_package_ndktutorial_MainActivity_stringFromHyunwooCustomizing(
JNIEnv *env,
jobject) {
Calculator ex = Calculator(5);
std::string answer = "5+6 = " + std::to_string(ex.getAdd(6));
return env->NewStringUTF(answer.c_str());
}
이제 MainActivty에서 해당 함수를 external function으로 등록하고
external fun stringFromJNI(): String
external fun stringFromHyunwooCustomizing(): String
TextView에 결과값을 등록하면 된다.
findViewById<TextView>(R.id.tv_calc).text = stringFromHyunwooCustomizing()
c++ 안드로이드 개발.. 낭만적이군요