UE4のC++からAndroid APIを呼んでみた

# はじめに

Unreal C++は機能が豊富でプラットフォーム間の差分もほとんど隠蔽されているのですが、どうしてもネイティブAPIが必要なときがあります。
そこでAndroid NDKを使ってAndroid APIを呼び出す方法をまとめてみました。
いくつか方法があるのですが、まずはベタにAndoird NDKを使う方法です。

# 前準備

Unreal Engine4のドキュメントの[1.Android SDK をインストールする](https://docs.unrealengine.com/latest/JPN/Platforms/Android/GettingStarted/1/index.html)に従ってAndroid Worksをインストールします。

```txt:AndroidWorksのインストーラ
[ENGINE INSTALL LOCATION]\Engine\Extras\AndroidWorks\Win64\CodeWorksforAndroid-1R4-windows.exe
```

これを利用することでAndroid開発に必要なAndroid SDK, Java Development Kit, Ant Scripting Tool, Android NDKをまとめてインストールすることができます。
ここでNsight Tegra, Visual Studio EditionもインストールするとVisual Studioのプロジェクト構成が若干変化します。が、UE 4.13.1ではあまり違いはありません。
[Android ゲームの開発のリファレンス](https://docs.unrealengine.com/latest/JPN/Platforms/Android/Reference/index.html)によるとデバイス上でAndroidゲームをデバッグできるので必要に応じてインストールするとよいと思います。

## 呼びたいJavaのコード

以下のような外部ストレージのPicturesフォルダのパスを取得する処理をAndroid NDKで実装してみます。

```java
import android.os.Environment;
import java.io.File;

public String getPicturesPath() {
    File f = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
    return f.getPath();
}
```

# C++プロジェクトを作る

Picturesフォルダのパスは変化しないので関数ライブラリを作ります。
関数ライブラリの作り方はalweiさんが解説する[UE4 C++コードをブループリントで使えるようにする（関数ライブラリー編）](http://unrealengine.hatenablog.com/entry/2014/09/10/202645)に全部載っていますので、
同じようにCppTestプロジェクトにBlueprintFunctionLibraryクラスを作ります。

# GetPicturesPath()を実装する

まずはベタに実装してみます。
JNIの詳しい使い方は最後の参考リンクをご覧ください。

```cpp:MyBluePrintFunctionLibrary.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "Kismet/BlueprintFunctionLibrary.h"
#include "MyBlueprintFunctionLibrary.generated.h"

/**
 * 
 */
UCLASS()
class CPPTEST_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()

public:
    UFUNCTION(BlueprintPure, Category = "MyBPLibrary")
    static FString GetPicturesPath();
};
```

```cpp:MyBluePrintFunctionLibrary.cpp
// Fill out your copyright notice in the Description page of Project Settings.

#include "CppTest.h"
#include "MyBlueprintFunctionLibrary.h"

#if PLATFORM_ANDROID
#include "Android/AndroidApplication.h"
#endif

FString UMyBlueprintFunctionLibrary::GetPicturesPath()
{
    FString result;
#if PLATFORM_ANDROID
    JNIEnv* Env = FAndroidApplication::GetJavaEnv();

    if (nullptr != Env)
    {
        jclass EnvCls = Env->FindClass("android/os/Environment");
        jfieldID DirectoryPicturesField = Env->GetStaticFieldID(EnvCls, "DIRECTORY_PICTURES", "Ljava/lang/String;");
        jmethodID getExternalStoragePublicDirectoryMethod = Env->GetStaticMethodID(EnvCls, "getExternalStoragePublicDirectory", "(Ljava/lang/String;)Ljava/io/File;");

        jstring DirectoryPictures = (jstring)Env->GetStaticObjectField(EnvCls, DirectoryPicturesField);
        jobject externalStoragePublicDirectory = Env->CallStaticObjectMethod(EnvCls, getExternalStoragePublicDirectoryMethod, DirectoryPictures);
        Env->DeleteLocalRef(DirectoryPictures);
        Env->DeleteLocalRef(EnvCls);

        jclass FileCls = Env->FindClass("java/io/File");
        jmethodID getPathMethod = Env->GetMethodID(FileCls, "getPath", "()Ljava/lang/String;");
        jstring pathString = (jstring)Env->CallObjectMethod(externalStoragePublicDirectory, getPathMethod, nullptr);
        Env->DeleteLocalRef(externalStoragePublicDirectory);
        Env->DeleteLocalRef(FileCls);

        const char *nativePathString = Env->GetStringUTFChars(pathString, 0);
        result = FString(nativePathString);

        Env->ReleaseStringUTFChars(pathString, nativePathString);
        Env->DeleteLocalRef(pathString);
        Env->DeleteLocalRef(externalStoragePublicPath);
    }
    else
    {
#endif
        result = FString("");
#if PLATFORM_ANDROID
    }
#endif

    return result;
}
```

ここではクラス、メソッド、フィールドを取り出すためにFindClass()、GetStaticFieldID()、GetStaticMethodID()を利用しています。
そしてjclass / jobject / jstringなどのオブジェクトはローカル参照が作成されるのですがデフォルトでは最大で16個までしか使えないので使い終わったらDeleteLocalRef()で削除しています。
ちょっと面倒すぎますね。。
しかもこのコードはJNIとしては駄目コードです。
FindClass()、GetStaticFieldID()、GetStaticMethodID()は内部でリフレクションするのでBlueprintのTickから呼び出すと負荷が大きくてアプリが落ちてしまいます。

# JNI呼び出しを改良する

jclass / jfieldID / jmethodIDは一度特定してしまえば変わらないのでstatic変数などにキャッシュすることができます。
そしてNewGlobalRef()を使ってGCされないように保護するのがよくやる方法で、以下のソースが参考になりました。
他にもJNIの注意点がまとまったページを最後の参考リンクに記載しましたのでご覧ください。

```
[ENGINE INSTALL LOCATION]\Engine\Source\Runtime\Core\Private\Android\AndroidJavaMediaPlayer.cpp
[ENGINE INSTALL LOCATION]\Engine\Source\Runtime\Core\Private\Android\AndroidMisc.cpp
```

ですが、今回取得したいPicturesフォルダのパスは変化しないので次のように、JNIベタ呼び出し関数をprivateにしてしまい、publicな関数のローカルstatic変数に保持することができます。

```cpp:MyBluePrintFunctionLibrary.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "Kismet/BlueprintFunctionLibrary.h"
#include "MyBlueprintFunctionLibrary.generated.h"

/**
 * 
 */
UCLASS()
class CPPTEST_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()

public:
    UFUNCTION(BlueprintPure, Category = "MyBPLibrary")
    static FString GetPicturesPath();

private:
    static FString GetPicturesPathJNI();
};
```

```cpp:MyBluePrintFunctionLibrary.cpp
// Fill out your copyright notice in the Description page of Project Settings.

#include "CppTest.h"
#include "MyBlueprintFunctionLibrary.h"

#if PLATFORM_ANDROID
#include "Android/AndroidApplication.h"
#endif

FString UMyBlueprintFunctionLibrary::GetPicturesPath()
{
    static FString PicturesPath = UMyBlueprintFunctionLibrary::GetPicturesPath();
    return PicturesPath;
}

FString UMyBlueprintFunctionLibrary::GetPicturesPathJNI()
{
    FString result;
#if PLATFORM_ANDROID
    JNIEnv* Env = FAndroidApplication::GetJavaEnv();

    if (nullptr != Env)
    {
        jclass EnvCls = Env->FindClass("android/os/Environment");
        jfieldID DirectoryPicturesField = Env->GetStaticFieldID(EnvCls, "DIRECTORY_PICTURES", "Ljava/lang/String;");
        jmethodID getExternalStoragePublicDirectoryMethod = Env->GetStaticMethodID(EnvCls, "getExternalStoragePublicDirectory", "(Ljava/lang/String;)Ljava/io/File;");

        jstring DirectoryPictures = (jstring)Env->GetStaticObjectField(EnvCls, DirectoryPicturesField);
        jobject externalStoragePublicDirectory = Env->CallStaticObjectMethod(EnvCls, getExternalStoragePublicDirectoryMethod, DirectoryPictures);
        Env->DeleteLocalRef(DirectoryPictures);
        Env->DeleteLocalRef(EnvCls);

        jclass FileCls = Env->FindClass("java/io/File");
        jmethodID getPathMethod = Env->GetMethodID(FileCls, "getPath", "()Ljava/lang/String;");
        jstring pathString = (jstring)Env->CallObjectMethod(externalStoragePublicDirectory, getPathMethod, nullptr);
        Env->DeleteLocalRef(externalStoragePublicDirectory);
        Env->DeleteLocalRef(FileCls);

        const char *nativePathString = Env->GetStringUTFChars(pathString, 0);
        result = FString(nativePathString);

        Env->ReleaseStringUTFChars(pathString, nativePathString);
        Env->DeleteLocalRef(pathString);
        Env->DeleteLocalRef(externalStoragePublicPath);
    }
    else
    {
#endif
        result = FString("");
#if PLATFORM_ANDROID
    }
#endif

    return result;
}
```

# まとめ

Javaではたった数行だったコードがNDKではかなり膨らんでしまい気軽に利用できるものではありません。
ですがちょっと待ってください。
UE4.10から追加された新しいAndroid plugin systemを使えばAndroid側のActivityにコードを追加することができるので本質的ではない部分のコードを大幅に減らす事ができます。
次はAndroid plugin systemの使い方を書きたいと思います。

# 参考リンク

* Unreal Engine4のドキュメント
    * [1.Android SDK をインストールする](https://docs.unrealengine.com/latest/JPN/Platforms/Android/GettingStarted/1/index.html)
    * [Android ゲームの開発のリファレンス](https://docs.unrealengine.com/latest/JPN/Platforms/Android/Reference/index.html)
* Android NDKの入門、注意点
    * [Android開発者のためのJNI入門](http://techbooster.jpn.org/andriod/application/7264/)
    * [JNI Tips](https://developer.android.com/training/articles/perf-jni.html)
    * [Java Native Interface を使用する上でのベスト・プラクティス](https://www.ibm.com/developerworks/jp/java/library/j-jni/)
* Android NDKを利用したプラグインプロジェクト
    * [opaquemultimedia/ProjectTangoPlugin](https://github.com/opaquemultimedia/ProjectTangoPlugin)
    * [gameDNAstudio/ParsePlatform](https://github.com/gameDNAstudio/ParsePlatform)