[Android NDK] C++로 안드로이드 코딩해보기 - Android NDK by C++

이현우·2020년 12월 14일
1

NDK 탐방하기

목록 보기
1/1
post-thumbnail

Android NDK Tutorial

안드로이드 NDK를 사용하기 이전에 이 툴을 왜 사용하는 지부터 생각을 해봐야한다. 게임이나 머신러닝과 같이 고성능의 계산을 요하는 프로그램들은 Java나 Kotlin으로 만들었을 때 퍼포먼스가 만족스럽게 나오지 않을 수가 있다. 그러나 C++ 네이티브 코드로 제작할 경우 안드로이드 플랫폼 내에서 최대한의 퍼포먼스를 낼 수 있게 할 수 있을 것이다.

따라서 계산 성능을 요하는 프로그램을 작성할 때에는 NDK를 활용하여 작성하는 것이 좋을 것이다.

이번 튜토리얼에서는 입문인만큼 Android NDK 프로젝트를 생성하는 법, C++ 코드를 작성하여 Build를 하는 법(CMake를 활용하여)에 대해 다룰 것이다.

Practice: NDK Tutorial

Project Setting



기존에 안드로이드 프로젝트를 Empty Activity로 시작했던 것과는 달리 Native C++로 시작한다. 표준은 자기가 원하는 버전(C++11, C++14, C++17 중 하나)으로 설정하면 된다.

Project Overview

기존 Kotlin 프로젝트와 달리 몇 가지 추가된 점이 있다. 한 번 살펴보도록 하자.

cpp 패키지


기존에 Java/Kotlin 파일을 담는 패키지에다 cpp 파일을 담을 수 있는 패키지가 있다. 내부를 살펴보면 cpp코드와 CMakeLists라는 파일이 있다.

CMakeLists
안드로이드에서 Java나 Kotlin 코틀린 파일들은 gradle 파일의 설정을 통해 빌드가 되는 것을 알고 있을 것이다. CMakeLists는 C++ 파일들의 빌드를 관리하는 CMake의 빌드스크립트 파일이다. 지금 이 글을 작성하고 있는 필자도 깊게 공부하지 않아 자세히는 모르지만 조금 더 자세히 공부하여 이후 게시글에 올릴 계획이다.

Gradle File

다음과 같은 코드들이 추가되었다.

android {
    ...
    
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++17"
            }
        }
    }

    ...
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
    ...
}

cppFlags는 C++ 표준을 설정하는 태그이다.
cmake는 빌드스크립트 파일인 CMakeLists의 절대 경로와 버전을 설정하는 태그이다.

Code Overview

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하는 부분인데 이는 다음과 같다.

  1. System.loadLibrary("native-lib"): 여기서 Android 플랫폼이 어떻게 C++ 코드를 사용하는 지 대략적으로 알 수 있다. C++ 코드는 네이티브 코드로 안드로이드 플랫폼에 직접 접근을 하는 것이 불가능하다. 그래서 C++ 빌드 파일을 라이브러리 파일로 변환해 System.loadLibrary() 함수를 이용하여 플랫폼에 적재, 내부의 네이티브 함수를 사용할 수 있도록 한 것이다.
    [TMI]: 라이브러리 파일은 파일명에 lib 접두사와 .so 확장자가 붙는다. 만약 native-lib가 파일명이면 라이브러리 파일 명은 libnative-lib.so가 된다.

  2. 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한다.

내가 만든 코드를 추가하고 싶어

Calculator.h/Calculator.cpp

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()

결과 화면

참고자료

profile
이현우의 개발 브이로그

0개의 댓글