既存のC/C++コードをAndroidアプリケーションに組み込む


この記事について

既存のC/C++コードを、Android NDKを用いて、ネイティブ関数としてAndroidアプリケーションに組み込みます。
基本的には、公式のUSER GUIDE(https://developer.android.com/studio/projects/add-native-code )のまんまです。

想定するシチュエーション

  • 元々、CMakeを使って、C/C++アプリケーション開発をしていた
    • 普段の開発環境はVisual Studioとか、GCC
  • アプリケーションをAndroidに移植する必要が出てきた
    • 移植するのは、コアとなるライブラリ部のみ(関数)
    • アプリケーション部(main関数や描画処理)は移植しない。アプリケーション部に該当するのはJava側で作られるはず
  • 「移植」とはいえ、ライブラリ部は従来のC/C++アプリケーションでも使うし、新規に開発するAndroidアプリケーションでも使う。二重メンテはしたくないので、共用したい

既存のプロジェクト

以下のような非常にシンプルなプロジェクトを考えます。
Main.cpp にはmain関数のみがあります。普段のC/C++開発ではこのmain関数が「アプリケーション」として主に動いているとします。今回、Android側では使用しません。
Submodule には関数群があります。今回は、単に入力値を2倍するだけのSubModule_funcDouble という関数を用意しました。これを「ライブラリ」としてJavaから呼ぶことを今回の目標にします。

プロジェクト構造
MyProject
├─CMakeLists.txt
├─Main.cpp
└─SubModule
   ├─CMakeLists.txt
   ├─SubModule.cpp
   └─SubModule.h
TopのCMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project("C_application") 

add_executable(Main "Main.cpp")

add_subdirectory(SubModule)
target_include_directories(Main PUBLIC ./SubModule)
target_link_libraries(Main SubModule)
Main.cpp
#include <stdio.h>
#include "SubModule.h"

int main()
{
    SubModule_funcDouble(10);
    return 0;
}
SubModuleのCMakeLists.txt
cmake_minimum_required(VERSION 3.0)
add_library(SubModule STATIC SubModule.cpp SubModule.h)
SubModule.cpp
#include <stdio.h>
#include "SubModule.h"

int SubModule_funcDouble(int a)
{
    printf("SubModule_funcDouble: %d x 2 = %d\n", a, a * 2);
    return a * 2;
}
SubModule.h
#ifndef _SUB_MODULE_H_
#define _SUB_MODULE_H_

#ifdef __cplusplus
extern "C" {
#endif
#if 0
}
#endif

int SubModule_funcDouble(int a);

#ifdef __cplusplus
}
#endif

#endif  /* _SUB_MODULE_H_ */

Androidアプリケーションを作る

NDKのインストール

まず、Android Studioの適当なプロジェクトを開き、メニューバー -> Tools -> SDK Manager を開きます。
SDK Toolsから、LLDBCMakeNDK を選びOKをクリックしてインストールします。

(これは、後でプロジェクトを作ってからやっても良いです)

Androidプロジェクトの生成

Android Studioを開き、新規プロジェクトを作ります。
テンプレートを選ぶ際に、Native C++ を選びます。

プロジェクトの保存場所

僕は、C/C++と一緒にソースコード管理したかったので、以下のように同じ場所にAndroidApp というフォルダを作り、その中に保存しました。

プロジェクト構造
MyProject
├─AndroidApp/     <-- ここにAndroidプロジェクトを保存
├─CMakeLists.txt
├─Main.cpp
└─SubModule/
   ├─CMakeLists.txt
   ├─SubModule.cpp
   └─SubModule.h

メモ: 他の方法

C/C++コードとAndroid用コードを別々に管理する場合には、全然別の場所に保存しても大丈夫です。
また、C/C++コードも新規に作る場合には、Androidプロジェクトの中にCPPフォルダがあるのでそこに保存していけばいいと思います。おそらくこれが想定された使い方だと思います。
ただ、今回は「既存の」C/C++コードを使いたいということと、C/C++コードをAndroidとは関係なく既存のC/C++プロジェクトとして使用している人もいることを想定して、上記のような構成にしました。(共通のライブラリ関数を、Androidアプリでも使うし、C/C++プロジェクトでも使う場合を想定)

どういう構成で各種ファイルを保存するかは、プロジェクト体制やチームメンバー、運用方法によって決めてください。

メモ: 既存のAndroidプロジェクトにC/C++を組込む場合

取り込みたいプロジェクト(app) を右クリックして、Link C++ Project with Gradle を選べば大丈夫です。ただ、後で紹介するようなCMakeLists.txtのひな形などは作られないようです。

C/C++コードの編集

生成されたプロジェクトを見ると、上記のようにcpp というフォルダが作られ、その下にCMakeLists.txtnative-lib.cpp が作られています。デフォルトで、Hello Worldという文字列を返すだけの関数を用意したライブラリのひな形が作られています。

以後、CMakeLists.txtを変更した場合は、まずはメニューバー -> Build -> Reflesh Linked C++ Projects をするといいと思います。

CMakeListsの編集

C/C++コードも新規で作る場合にはこのCMakeListsもがりがり編集して良いと思うのですが、今回は既存のC/C++コードを取り込むだけなので、出来るだけ変更の手間が少なくなるようにします。

以下のように、既存のC/C++コードのSubModuleのadd_subdirectory と、インクルードパスの追加、リンク設定だけを最後に追加しています。パスの指定は、このCMakeLists.txt からの相対パスになります。
今後SubModuleの方でファイルを増やしたり、さらに別のモジュールを呼んだりしても、ここを変更する必要はなくなります。(依存性を駄々洩れにしたヘッダファイルを作ったりしなければ、ですが)

native-lib/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)

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

~略~

target_link_libraries( # Specifies the target library.
        native-lib

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

# ↓追加
add_subdirectory(../../../../../SubModule SubModule)
target_include_directories(native-lib PUBLIC ../../../../../SubModule)
target_link_libraries(native-lib SubModule)
# ↑追加

native-lib.cppの編集

元々用意されている、Hello worldという文字列を返すだけの関数があります。
別にこれは名前を変えてもいいのですが、そのまま使います。

今回呼びたい関数はSubModule の中にあるので、基本的にはここにはWrapperとしての役割を担ってもらいます。
JNIに則って、JavaとC/C++のインターフェイス変換をしてもらいます。また、今回はintだけなので簡単ですが、配列やオブジェクトが引数や戻り値にある場合には、その変換もしてもらいます。

native-lib.cpp
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

// ↓追加
#include "SubModule.h"
extern "C" JNIEXPORT jint JNICALL
Java_com_example_myapplication_MainActivity_funcDouble(
        JNIEnv *env,
        jobject /* this */,
        jint a
) {
    int ret = SubModule_funcDouble(a);
    return ret;
}
// ↑追加

JavaコードからC/C++関数を呼ぶ

いよいよ、JavaコードからC/C++関数を呼んでみます。
ライブラリ(so)ファイルのロード処理(loadLibrary )は既に自動生成されています。

C/C++ネイティブ関数の宣言をします。どうも一番下に書くのがお作法みたいです。
その後、関数コールは普通のJavaコードとして実装できます。

MainActivity.java
package com.example.myapplication;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

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 = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());

        // ↓追加
        int ret = funcDouble(10);
        Log.i("MyApp", "funcDouble: " + ret);
        // ↑追加
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();

    // ↓追加
    public native int funcDouble(int a);
    // ↑追加
}

メモ

printf

C/C++側でのprintfはどこにも出力されず、無視されるみたいでした。ただ、おそらく環境依存なので、#ifdef __ANDRDOID__ などでprintfにするか__android_log_print にするかを切り替えられるようにした方がよさそうです

fopen

万が一、C/C++側でファイル読み書きするコードがある場合、何の考慮もしないとSIGSEGVでクラッシュします。
ファイル読み書きに対応するには、

  • Manifestに、<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> を追加
    • その後、Androidのアプリ設定から権限付与。または、本記事のようにアプリから自発的に権限付与を催促する
  • fopenで指定するパスをAndroidシステム上で存在するディレクトリにする
    • カレントディレクトリに出力したいからと、fopen("test.txt", "w"); とするのはダメ
    • 例えばシステムメモリのトップに出力するためには、fopen("/sdcard/test.txt", "w"); とする

OpenMP

native-lib下のCMakeLists.txt に以下を追加することで、普通に使えました。
SubModule下のCMakeLists.txtにつけるのだと、上手くいきませんでした。(ビルドエラー発生)

native-lib下のCMakeLists.txtに以下を追加
# ↓追加
find_package(OpenMP REQUIRED)
if(OpenMP_FOUND)
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}")
endif()
# ↑追加

add_subdirectory(../../../../../SubModule SubModule)
target_include_directories(native-lib PUBLIC ../../../../../SubModule)
target_link_libraries(native-lib SubModule)

また、UIスレッドからの呼び出しじゃないとCrashするという情報がネットにありましたが、UIスレッド以外からの呼び出しでも問題なく動いているようです。(NDKバージョンは19.2)
具体的には、OpenCVのCameraBridgeViewBase.CvCameraViewListeneronCameraFrame コールバック内でネイティブ関数を呼び、その中でOpenMPでfor文を回してみましたが、クラッシュなく動作していました。高速化もされていました。

おわりに

かなり的を絞ったユースケースを想定した方法です。
もっといい方法があるかもしれません。
改善点があれば、コメントなどで教えていただけると嬉しいです。