4

I'm using Android NDK and need access to assets. A requirement for asset access seems to be obtaining an AssetManager reference.

Looking at the NDK samples (https://github.com/android/ndk-samples), the pattern seems to be:

  • A JNIEnv* is passed into the func when called directly from the JavaVM, along with some jobject
  • Use these to get AAssetManager* and then use this to open assets

That seems simple enough, except in my case, the functions are being called from Unity so I don't have access to either a JNIEnv* or jobject. Getting the JNIEnv* seems easy enough as I can make use of JNI_OnLoad to get access to a JavaVM* and then use that to get a JNIEnv* via vm->GetEnv. My questions about this are:

1) My understanding is that, an Android app can only have one instance of a Java VM. Am I safe to take the JavaVM* passed into JNI_OnLoad and save it for use in other function calls?

2) What about the JNIEnv*? Can I grab that once during JNI_OnLoad and save it, or should I grab a fresh one every time I need to use assets within a function? Is JNIEnv* something I need to explicitly free? (i.e. what's the lifetime/ownership situation with JNIEnv*?)

3) AAssetManager_fromJava also requires a jobject with the documentation (https://developer.android.com/ndk/reference/group/asset#group___asset_1gadfd6537af41577735bcaee52120127f4) saying: "Note that the caller is responsible for obtaining and holding a VM reference to the jobject to prevent its being garbage collected while the native object is in use.". I've seem some examples that simply pass in an empty (native) string like AAssetManager_fromJava(env, ""); - is that ok? I'd only be using the AssetManager for the lifetime of that call, and I could get a fresh one each time. (Again, is AAssetManager* a resource I need to manage, or am I just getting a reference to something owned elsewhere? The documentation seems to imply the latter.)

4) So given all the above, I'd probably do something like:

JavaVM* g_vm;
JNIEnv* g_env;

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    g_vm = vm;
    g_vm->GetEnv((void **)&g_env, JNI_VERSION_1_6);  // TODO: error checking
    return JNI_VERSION_1_6;
}

void do_asset_stuff() {

    AAssetManager* mgr = AAssetManager_fromJava(g_env, "");
    // do stuff...

}

Is that reasonable? No memory/resource leak issues? Any issues with multi-threading?

Thanks!

EDIT: Seems like there are some threading considerations with JNIEnv*. See: Unable to get JNIEnv* value in arbitrary context

ShhhSecret
  • 53
  • 4

3 Answers3

1

Point-by point answer to your questions:

  1. Yes, there can be only one VM in Android. You are allowed to store this pointer or use JNI_GetCreatedJavaVMs.

  2. JNIEnv pointers are tightly coupled to the thread they were created on. In your situation you will first have to attach the thread to the VM using AttachCurrentThread. This will fill in a JNIEnv * for you. Don't forget to DetachCurrentThread when you're done.

    Also note the caveat about FindClass: you need to look up classes from the main thread or via the classloader of a class you looked up in the main thread.

  3. The implementation of AAssetmanager_fromJava is pretty clear: passing it anything other than an AssetManager object is undefined behavior. This answer shows one approach to grabbing the asset manager, another might be to call your own JNI function with a reference to the AssetManager object. Make sure to keep a global reference so it does not get GCed.

  4. Given the above, it would probably look more like this:

JavaVM* g_vm;
jobject cached_assetmanager;

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    g_vm = vm;
    return JNI_VERSION_1_6;
}

void do_asset_stuff() {
    JNIEnv *env;
    JavaVMAttachArgs args = { JNI_VERSION_1_6, "my cool thread", NULL };
    g_vm->AttachCurrentThread((void **)&env, &args);
    AAssetManager* mgr = AAssetManager_fromJava(g_env, cached_assetmanager);
    // do stuff...
}

// Assuming you call `com.shhhsecret.app.storeassetmanager(mgr)` somewhere.
void Java_com_shhhsecret_app_storeassetmanager(JNIEnv *env, jclass cls, jobject am) {
    cached_assetmanager = env->NewGlobalRef(am);
}
Botje
  • 26,269
  • 3
  • 31
  • 41
  • thanks so much. Couple of follow-ups: 1) So I should call `DetachCurrentThread` at the end of `do_asset_stuff`? Do I need to do any other cleanup of `JNIenv*`? Can I just use `AttachCurrentThreadAsDaemon` instead? 2) Will I be able to use the `cached_assetmanager` across different threads once I have it? – ShhhSecret Nov 25 '19 at 11:07
  • 1) Call `DetachCurrentThread` if the lifetime of your app allows a normal stopping point. (no more work or some signal about quitting) Attaching your thread as a Daemon will allow the app to quit without quitting this thread, but that is not really a concern on Android. 2) Yes, in as much that the asset manager itself is thread safe. – Botje Nov 25 '19 at 11:09
1

I was able to read a json file from Unity c++ plugin. I had to extend UnityPlayerActivity to get assetManager as jobject. The tricky part also was to find the correct path to the asset in the plugin: I placed it into StreamingAssets/data and was able to read using this path 'data/myfile'

see my comment with the code: unity answers

Seems Botje's answer is precise (pity, I did not have it earlier)

xdimy
  • 46
  • 6
  • thanks! would it be possible to see the MyUnityPlayerActivity.java code? – ShhhSecret Nov 25 '19 at 12:27
  • yes, I attached it in a comment in the same [unity answers](https://answers.unity.com/questions/1679465/accessing-config-file-from-cc-android-native-plugi.html) – xdimy Nov 25 '19 at 13:13
  • I can only see the native code? Not the Java code? But it's cool - I've managed to solve it without having to override UnityPlayerActivity, using Botje's answer – ShhhSecret Nov 25 '19 at 15:25
  • I added a comment 2 times there but cannot see it too. Here is its main part: public class MyUnityPlayerActivity extends UnityPlayerActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AssetManager assetManager = getAssets(); loadConfigFile(assetManager); } public static native int loadConfigFile(AssetManager assetManager); static { System.loadLibrary("plugin"); } } – xdimy Nov 25 '19 at 15:45
0

Thought I'd post what I ended up doing in case it's of help to others...

#include <jni.h>
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>

JavaVM* g_JavaVM;
jobject g_JavaAssetManager;
bool g_Initialized = false;

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    g_JavaVM = vm;
    return JNI_VERSION_1_6;
}

// call this once from the main thread in C# land:
extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API NativeInit() {

    if (g_Initialized) { return; }
    g_Initialized = true;

    JNIEnv* env = nullptr;

    jint get_env_result = g_JavaVM->GetEnv((void **)&env, JNI_VERSION_1_6);

    if (get_env_result == JNI_EDETACHED) {
        jint attach_thread_result = g_JavaVM->AttachCurrentThreadAsDaemon(&env, nullptr);
        if (attach_thread_result != 0) {
            return;
        }
        get_env_result = JNI_OK;
    }

    if (env == nullptr || get_env_result != JNI_OK) {
        return;
    }

    jclass unity_player = env->FindClass("com/unity3d/player/UnityPlayer");
    jfieldID static_activity_id = env->GetStaticFieldID(unity_player, "currentActivity","Landroid/app/Activity;");
    jobject unity_activity = env->GetStaticObjectField(unity_player, static_activity_id);
    jmethodID get_assets_id = env->GetMethodID(env->GetObjectClass(unity_activity), "getAssets", "()Landroid/content/res/AssetManager;");
    jobject java_asset_manager = env->CallObjectMethod(unity_activity, get_assets_id);
    g_JavaAssetManager = env->NewGlobalRef(java_asset_manager);

}

Now g_JavaAssetManager can be used in any thread to call AAssetManager_fromJava.

ShhhSecret
  • 53
  • 4