Android NDK の初期化と dll の分離

NDK では C/C++ のコードをそのまま使うことが出来ます。
Windows 上で作っていた lib やアプリケーションを移植し Android 上で
走らせています。
実際に動作するのですが少々問題があって、正しく起動する場合もあれば
アイコンをタッチした瞬間に落ちる場合があります。

Activity が終了しても Process イメージは残り、再び onCreate した時に
常駐している Native コードが再利用される可能性があるからです。
bss/data セグメントが初期化されていないことが原因でしょう。
static 変数は初期値に戻らず global オブジェクトのコンストラクタも走りません。

NVIDIA の資料でアプリ本体を別の dll に分離する手法が紹介されていたので
試してみました。

GameSauce 2010: Fast and Pretty: Making Responsive, Quality 3D Content on Android

Java Application からロードされるダミー (Thunk) dll を作り、その中で
自分でアプリ本体の dll を load/unload します。
確実に Unload できる点がポイントです。

NDK で複数の Dynamic Link Library (~.so) を build するには下記のようにします。

# Andriod.mak

LOCAL_PATH:= $(call my-dir)

#--------------------------------------
include $(CLEAR_VARS)

LOCAL_SRC_FILES := android_main.cpp ~
LOCAL_MODULE    := libappmain
LOCAL_CFLAGS    :=
LOCAL_C_INCLUDES:=
LOCAL_LDLIBS    := -llog -lGLESv2

include $(BUILD_SHARED_LIBRARY)
#--------------------------------------

#--------------------------------------
include $(CLEAR_VARS)

LOCAL_SRC_FILES := jniproxy.cpp
LOCAL_MODULE    := libjniproxy
LOCAL_CFLAGS    :=
LOCAL_LDLIBS    := -llog

include $(BUILD_SHARED_LIBRARY)
#--------------------------------------

ndk-build で 2つの dll ファイル libappmain.so, libjniproxy.so が作られます。
include $(CLEAR_VARS) は LOCAL_~ の変数を再定義可能にします。

Java 側でロードする dll は libjniproxy.so だけです。

// appmain.java

package jp.flatlib.appmain;
public class appmain {
     static {
         System.loadLibrary( "jniproxy" );
     }
     public static native void init();
     public static native void quit();
     public static native int  render();
     ~
}

jniproxy.cpp は libappmain.so のロード管理や呼び出しを行います。

// jniproxy.cpp

#include    
#include    
#include    

template
void auto_load( T& a, void* ptr )
{
    a= reinterpret_cast( ptr );
}

#define get_proc_0( image, func_name )  auto_load( proc_##func_name,  dlsym( image, #func_name ) )
#define get_proc( func_name )   get_proc_0( LoadApplication, func_name )

//-----------------------------------------------------------------------------
static void JNICALL (*proc_app_init)( JNIEnv* env, jobject obj );
static void JNICALL (*proc_app_quit)( JNIEnv* env, jobject obj );
static jint JNICALL (*proc_app_render)( JNIEnv* env, jobject obj );
~
//-----------------------------------------------------------------------------

static void*   LoadApplication= NULL;

static void InitializeApplication()
{
    // lib 読み込み
    LoadApplication= dlopen( "/data/data/jp.flatlib.appmain/lib/libappmain.so", RTLD_LAZY );
    if( !LoadApplication ){
    	~ error
    }

    // API 取り出し
    get_proc( app_init );
    get_proc( app_quit );
    get_proc( app_render );
    ~
}


static void FinalizeApplication()
{
    if( LoadApplication ){
        dlclose( LoadApplication );
        LoadApplication= NULL;
    }
}


extern "C" {
//-----------------------------------------------------------------------------

JNIEXPORT void JNICALL Java_jp_flatlib_appmain_appmain_init( JNIEnv* env, jobject obj )
{
    if( !LoadApplication ){
        InitializeApplication();
        proc_app_init( env, obj );
    }
}

JNIEXPORT void JNICALL Java_jp_flatlib_appmain_appmain_quit( JNIEnv* env, jobject obj )
{
    if( LoadApplication ){
        proc_app_quit( env, obj );
        FinalizeApplication();
    }
}

JNIEXPORT jint JNICALL Java_jp_flatlib_appmain_appmain_render( JNIEnv* env, jobject obj )
{
    return  LoadApplication ? proc_app_render( env, obj ) : 0;
}

~
//-----------------------------------------------------------------------------
};

元の android_main.cpp では jni 名を app_~ に変更しておきます。

// android_main.cpp
~
extern "C" {

JNIEXPORT void JNICALL app_init( JNIEnv* env, jobject obj )
{
}

JNIEXPORT void JNICALL app_quit( JNIEnv* env, jobject obj )
{
}

JNIEXPORT jint JNICALL app_render( JNIEnv* env, jobject obj )
{
}

~
};

アプリケーション側で dll の読み込みと終了タイミングを指定します。

スレッドの違いに注意が必要です。
例えば GL API は異なるスレッドから呼び出すことが出来ませんが、
GLSurfaceView.Renderer は別スレッドで動作しています。
Activity から呼び出す場合は GLSurfaceView.queueEvent() を使います。

mView.queueEvent( new Runnable(){ public void run(){ appmain.quit(); }  });

現在 Activity の onPause() と GLSurfaceView.Renderer の
onSurfaceChanged() を使っています。

dll の分離はうまくいっており、比較的簡単な追加コードだけで確実に初期化が
行われるようになりました。libjniproxy.so は 4KB 程度。
安定して動いています。

その代わり onPause()/onResume() で unload/load となるので、
端末を Sleep させたり別のアプリに切り替えただけでアプリケーションの
ndk 部は再起動とほぼ同様の状態となります。

関連エントリ
Android アプリケーションとプロセス