既存のAndroidプロジェクトへのC/C++コードの導入(Android NDK - CMake)

本記事では,既存のAndroidプロジェクト(プログラム)にC/C++で書かれた処理コードを組み込み,JavaコードからあるデータをC/C++コードに渡して処理させ,処理結果をJavaコードで受け取ってViewに表示する方法についてまとめる.また,本記事ではndk-build(Application.mkやAndroid.mkを用いるもの)ではなくCMakeを用いたC/C++コードのビルド方法について説明する.説明に用いるサンプルプロジェクトはGitHubのtrileg/SampleAndroidCppAppで公開しているので,あわせて参考にしてほしい.

Android NDK環境の導入

まずはAndroidプロジェクトでC++コードを動かすためにAndroid NDK環境をインストールする.インストールにはAndroid SDKを用いるので,以下の画像で示すアイコンをクリックしてAndroid SDK Managerを起動する.

起動したら,SDK Toolsタブを選択し,C/C++ソースファイルのビルドに用いるCMakeとC/C++コードをデバッグするためのLLDBNDKにチェックマークを入れて右下のApplyをクリックする.

インストールできたら,次にProject Structureを起動する.

起動したらSDK Locationの最下部にあるAndroid NDK locationの下部にあるSelect default NDKを選択してAndroid NDKのパスをプロジェクトに設定する.

設定できたら,次にC++ソースファイル等を配置するcppディレクトリの作成と,C++ソースファイル,CMakeでのC/C++ソースファイルのビルドに用いるCMakeLists.txtファイルの作成を行う.

C++ソースファイルとCMakeLists.txtの追加

まずはcppディレクトリを作成する.プロジェクトツリーが表示されているペインで初期状態でAndroidと表示されている部分をクリックし,Projectに変更する1.変更できたら,app/src/main/で右クリックし,New->Directoryを選んでcppで作成する.

作成できたら,このcppディレクトリを右クリックし,New->C/C++ Source Fileを選んでC++のソースファイルを作成する.

今回の例では,以下のようなコードを用意した.

app/src/main/cpp/calculate.hpp

#ifndef SAMPLEANDROIDCPPAPP_CALCULATE_HPP
#define SAMPLEANDROIDCPPAPP_CALCULATE_HPP

class Calculate {  
public:  
  Calculate();
  int calc(int initial_value);

private:  
  static const int LOOP = 1000000;
};

#endif //SAMPLEANDROIDCPPAPP_CALCULATE_HPP

app/src/main/cpp/calculate.cpp

#include "calculate.hpp"
#include <android/log.h>

Calculate::Calculate() {}

int Calculate::calc(int initial_value) {  
  __android_log_print(ANDROID_LOG_INFO, "calculate", "method calc called, initial_value: %d", initial_value);
  int result_value = initial_value;

  for (int i = 0; i < LOOP; ++i) {
    ++result_value;
  }

  return result_value;
}

app/src/main/cpp/jni_main.cpp

#include "calculate.hpp"

#ifdef __cplusplus
extern "C" {  
#endif

#include <jni.h>

JNIEXPORT jint JNICALL Java_net_trileg_sampleandroidcppapp_MainActivity_calc  
  (JNIEnv *env, jobject thiz, jint _initial_value) {
    Calculate calculate;

    int initial_value = _initial_value;
    int result = calculate.calc(initial_value);

    jint _result = result;
    return _result;
}

#ifdef __cplusplus
}
#endif

calculate.cppにはandroid/log.hもインクルードしている.これは後述するCMakeLists.txtにおいて追加するlogライブラリを用いるためのもので,このライブラリを用いたlogcatへのログの出力はソースコード中の以下のコードで行っている.

__android_log_print(ANDROID_LOG_INFO, "calculate", "method calc called, initial_value: %d", initial_value);  

jni_main.cppはJavaコードとC/C++コードとの橋渡しを行うためのソースファイルである.JNI(Java Native Interface)という仕様に沿って記述する.JNI以外にもJNA(Java Native Access)SWIG(Simplified Wrapper and Interface Generator)といった仕様がある.上記コードを見てもらえれば何となく雰囲気はつかめるかと思うが,Javaから呼び出す関数名は,JNIEXPORT JNI型 JNICALL Java_パッケージ名(.は_に)_クラス名_メソッド名という風に命名しなければならない2

作成できたら,これらソースファイルをビルドするためのCMakeLists.txtファイルを作成する.

今回の例では,以下のような内容で作成した.

app/src/main/cpp/CMakeLists.txt

cmake_minimum_required(VERSION 3.6)

find_library ( log-lib log )

add_library ( calculate SHARED calculate.cpp jni_main.cpp )  
target_link_libraries ( calculate ${log-lib} )  

この例では,C/C++コード内からAndroid Monitorのlogcatにログを出力できるように,Android NDKに標準で用意されているlogライブラリを追加している3

CMakeLists.txtを作成できたら,GradleからこのCMakeLists.txtを用いてビルドするようにリンク設定を行う.appディレクトリを右クリックし,Link C++ Project with Gradeを選ぶ.表示されるダイアログから先ほど作成したCMakeLists.txtまでのパスをファイルダイアログから選択して入力し,設定を行う.

この設定により,app/直下のbuild.gradleにおいて,androidで括られた内部に以下のコードが追加されているはずだ.

externalNativeBuild {  
    cmake {
        path 'src/main/cpp/CMakeLists.txt'
    }
}

さらに,C++11と最適化オプションを使うためのフラグ設定と,STLを用いるための設定を行う.同じくapp/直下のbuild.gradleにおいて,androidの中のさらにdefaultConfigの中に,以下のコードを追加する4

externalNativeBuild {  
    cmake {
        cppFlags "-std=c++11 -Ofast"
        arguments "-DANDROID_STL=c++_static"
    }
}

設定できたら,次はJava側からC++コードを利用するコードを記述する.

C++コードをJavaから利用するためのコードの追加

今回の例では,簡単に以下のような画面構成のアプリを用意した.

画面下部にあるCALCULATEボタンを押すことでjni_main.cppを通じてcalculate.cppのメソッドを呼び出し,計算結果を上部のResultに反映するというものだ.

この画面に対応するJavaコードは以下のように実装した.

package net.trileg.sampleandroidcppapp;

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

public class MainActivity extends AppCompatActivity {

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

  public native int calc(int initial_value);

  int initial_value = 3;
  int result_value = 0;

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

    final TextView resultText = (TextView) findViewById(R.id.resultText);
    final Button calculateButton = (Button) findViewById(R.id.calculateButton);

    calculateButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        result_value = calc(initial_value);
        resultText.setText(String.valueOf(result_value));
      }
    });
  }
}

この中でミソなのが以下のコードである.

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

public native int calc(int initial_value);  

まず,System.loadLibrary("calculate");CMakeLists.txtをもとにC++ソースファイルをビルドして得た共有ライブラリを読み込むのだが,この引数にはCMakeLists.txttarget_link_librariesの一つ目の引数で設定した名称(今回の例ではcalculate)を渡す(生成されるライブラリ名はlibcalculate.soという風になるが,lib.soを省いたものとなる).

次に,JNIのコードであるjni_main.cppに書いたメソッドに対応するネイティブメソッドを宣言する.これはJNIで指定したクラス直下であれば,コードの先頭でも末尾でも問題ない.

ここまででC++コードを呼び出す準備が整ったことになる.Javaコード内で通常のメソッドと同様にネイティブメソッドを呼び出せば,JNIを通じてC++コードで処理が行われ,結果が返ってくる.


参考


  1. Projectに変更せず,Androidのままでappディレクトリを右クリックしてディレクトリを作成,でも大丈夫だと思うが,ディレクトリの作成場所を明確にしておきたかったのでこの方法にした.

  2. JNIでのコードの書き方については,JNIメモ(Hishidama's Java native interface Memo)が参考になる.

  3. Android NDKで標準で使うことのできるライブラリについては,Android NDK ネイティブ API | Android Developersを参照のこと.

  4. 使用可能なC++ランタイムはC++ ライブラリ サポート | Android Developersを参照のこと.