AndroidでJNIを使う方法
AndroidでJNIを使う方法をドキュメントにまとめました。
PDF版はこちら
Androidのコンパイル環境を構築されていることが前提です。
よろしければAndroidのコンパイル環境を整える方法にあるPDFのコンパイル環境構築資料をご覧ください。
AndroidでJNI – Android meets JNI
株式会社ブリリアントサービス 勉どろいどチーム
和泉憲二
門口敏広
藤田竜史
このドキュメントでは、androidアプリケーション(Dalvik VM)からJNI(Java Native Interface)を使用して、C/C++言語で作成した共有ライブラリのJNIメソッドをコールする、一連の方法について解説します。
本ドキュメントで作成する、サンプルアプリケーションの説明
androidアプリ側からJNIメソッドをコールし、JNIメソッドより返却された文字列をandroidアプリのTextViewで描画します。
androidアプリケーション JNItest JNIメソッドから返却された文字列をTextViewを使用して画面上に表示する。 共有ライブラリモジュール libJNItestNative コールされた戻り値として、文字列を返却する。 また今回作成するネイティブメソッドの仕様は、以下の通りとします。
ネイティブメソッド名 getTestStringFromNative 引数 なし 戻り値 String 説明 戻り値として、“from Native Code String”という文字列を返却します。 1.android アプリケーション JNItest の作成
androidアプリケーションの作成
まず、JNIヘッダの生成を行うために、ライブラリのロードとネイティブメソッド定義を含むソースコードを作成します。
eclipseのandroid ADTを使用し、以下の構成で新規プロジェクトの作成を行います。
プロジェクト名 JNItest パッケージ名 jp.co.brilliantservice.JNItestPkg アクティビティー名 JNItest アプリケーション名 JNItest
次に、TextViewにJNIメソッドからの返却文字列を設定するために、レイアウトファイル(main.xml)の編集を行います。
パッケージエクスプローラーの [JNItest]-[res]-[layout]-main.xmlを以下のように編集します。01 : <?xml version="1.0" encoding="utf-8"?> 02 : <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 03 : android:orientation="vertical" 04 : android:layout_width="fill_parent" 05 : android:layout_height="fill_parent" 06 : > 07 : <TextView 08 : android:id="@+id/txtTest" 09 : android:layout_width="fill_parent" 10 : android:layout_height="wrap_content" 11 : /> 12 : </LinearLayout>8行目にandroid id txtTest定義を付加し、ウィザードで自動生成されるhello world文字列の表示を削除します。
次に、JNItest.javaの編集を行います。01 : package jp.co.brilliantservice.JNItestPkg; 02 : 03 : import android.app.Activity; 04 : import android.os.Bundle; 05 : import android.widget.TextView; 06 : 07 : public class JNItest extends Activity { 08 : static { 09 : // ライブラリをロード 10 : System.loadLibrary("JNItestNative"); 11 : } 12 : // ネイティブメソッドを定義 13 : public native String getTestStringFromNative(); 14 : 15 : /** Called when the activity is first created. */ 16 : @Override 17 : public void onCreate(Bundle savedInstanceState) { 18 : super.onCreate(savedInstanceState); 19 : setContentView(R.layout.main); 20 : 21 : // ネイティブメソッドをコールして文字列を取得 22 : String strText = getTestStringFromNative(); 23 : 24 : // 取得文字列をTextViewに設定 25 : TextView txtTest = (TextView)findViewById(R.id.txtTest); 26 : txtTest.setText(strText); 27 : } 28 : }10行目で共有ライブラリのロードを行います。また、13行目ではネイティブメソッドの定義を行っています。
JNIヘッダファイルの生成
「androidアプリケーションの作成」で、eclipse上でのコンパイルが正常に行われ、以下のディレクトリにJNItest.classが生成されている事を確認します。
JNItest\bin\jp\co\brilliantservice\JNItestPkg\次に、javah(JDK付属のJNIヘッダ作成ツール)を使用して、JNI実装用のC/C++言語ヘッダを生成します。
コマンドプロンプトを起動し、eclipseプロジェクトのJNItestディレクトリにcd で移動の上、以下のコマンドを実行します。JNItest>javah -classpath bin -d jni jp.co.brilliantservice.JNItestPkg.JNItestディレクトリJNItest\jniにjp_co_brilliantservice_JNItestPkg_JNItest.h が生成されます。このファイルが、JNIヘッダファイルです。
JNIヘッダファイル jp_co_brilliantservice_JNItestPkg_JNItest.h
01 : /* DO NOT EDIT THIS FILE - it is machine generated */ 02 : #include <jni.h> 03 : /* Header for class jp_co_brilliantservice_JNItestPkg_JNItest */ 04 : 05 : #ifndef _Included_jp_co_brilliantservice_JNItestPkg_JNItest 06 : #define _Included_jp_co_brilliantservice_JNItestPkg_JNItest 07 : #ifdef __cplusplus 08 : extern "C" { 09 : #endif 10 : /* 11 : * Class: jp_co_brilliantservice_JNItestPkg_JNItest 12 : * Method: getTestStringFromNative 13 : * Signature: ()Ljava/lang/String; 14 : */ 15 : JNIEXPORT jstring JNICALL Java_jp_co_brilliantservice_JNItestPkg_JNItest_getTestStringFromNative 16 : (JNIEnv *, jobject); 17 : 18 : #ifdef __cplusplus 19 : } 20 : #endif 21 : #endif2.共有ライブラリ libJNItestNative.so の作成
文字列を返却するメソッドを、android build環境上にC言語で実装します。
その際、「JNIヘッダファイルの生成」 で自動生成した、JNIヘッダで宣言されているJNI関数プロトタイプに合わせて実装します。
本ドキュメントにおける、android build環境(ソースコードツリー)のビルドルート及び共有ライブラリ作成位置は以下の通りとします。
ビルドルート ~/mydroid 共有ライブラリ作成位置 ~/mydroid/external/libJNItestNative 共有ライブラリファイル名 libJNItestNative.so 上記共有ライブラリ作成位置には以下のファイルを作成または用意します。本項ではJNIメソッドの実装・Makefileの作成方法について説明します。
JNIメソッドソースファイル名 GetTestStringFromNative.c Makefile Android.mk JNIヘッダファイル jp_co_brilliantservice_JNItestPkg_JNItest.h JNIメソッドの実装
JNIのメソッドは以下のようなソースになります。
メソッド実行時に、単純に文字列を返却するのみのコードです。getTestStringFromNative.c
1 : #include "jp_co_brilliantservice_JNItestPkg_JNItest.h" 2 : 3 : JNIEXPORT jstring JNICALL Java_jp_co_brilliantservice_JNItestPkg_JNItest_getTestStringFromNative 4 : ( JNIEnv *env, jobject obj ) 5 : { 6 : return (*env)->NewStringUTF(env, (char *)"from Native Code String"); 7 : }Makefile 「Android.mk」の作成
androidの個別ビルド用Makefile 「Android.mk」 の作成を行います。
Android.mk
01 : LOCAL_PATH:= $(call my-dir) 02 : 03 : include $(CLEAR_VARS) 04 : 05 : LOCAL_SRC_FILES := \ 06 : getTestStringFromNative.c 07 : 08 : LOCAL_C_INCLUDES := \ 09 : $(JNI_H_INCLUDE) \ 10 : 11 : LOCAL_MODULE := libJNItestNative 12 : 13 : LOCAL_PRELINK_MODULE := false 14 : 15 : include $(BUILD_SHARED_LIBRARY)5・6行目にソースファイル名、11行目にライブラリモジュール名を定義しています。
上記をふまえ、androidでJNIを実現する上において、最も注目すべき点は、以下の3点です。
- JNIヘッダをインクルードするための定義(9行目)
- prelinkを解除するための定義(13行目)
- 共有ライブラリをビルドするための定義(15行目)
prelink関連の情報については、後述のNote:でまとめます。
共有ライブラリのビルド
端末コンソールを起動の上、以下のコマンドを実行し、ディレクトリ内のみのビルドを行えるように、~/mydroid/build/ にある、envsetup.sh を評価しておきます。
$cd ~/mydroid $. build/envsetup.sh続いて、以下のコマンドを実行し、共有ライブラリ位置のビルドを行います。
$cd external/libJNItestNative/ $mmビルドが成功すると、以下のようにログが表示されます。
make: ディレクトリ `/home/kenken/mydroid' に入ります build/core/product_config.mk:211: WARNING: adding test OTA key ============================================ TARGET_PRODUCT=generic TARGET_BUILD_VARIANT=eng TARGET_SIMULATOR= TARGET_BUILD_TYPE=release TARGET_ARCH=arm HOST_ARCH=x86 HOST_OS=linux HOST_BUILD_TYPE=release BUILD_ID= ============================================ build/core/main.mk:180: implicitly installing apns-conf_sdk.xml target thumb C: libJNItestNative <= /home/kenken/mydroid/external/libJNItestNative/getTestStringFromNative.c target SharedLib: libJNItestNative (out/target/product/generic/obj/SHARED_LIBRARIES/libJNItestNative_intermediates/LINKED/libJNItestNative.so) target Non-prelinked: libJNItestNative (out/target/product/generic/symbols/system/lib/libJNItestNative.so) target Strip: libJNItestNative (out/target/product/generic/obj/lib/libJNItestNative.so) Install: out/target/product/generic/system/lib/libJNItestNative.so Finding NOTICE files: out/target/product/generic/obj/NOTICE_FILES/hash-timestamp Combining NOTICE files: out/target/product/generic/obj/NOTICE.html gzip -c out/target/product/generic/obj/NOTICE.html > out/target/product/generic/obj/NOTICE.html.gz make: ディレクトリ `/home/kenken/mydroid' から出ます共有ライブラリ libJNItestNative.so は以下のディレクトリに格納されます。
~mydroid/out/target/product/generic/system/lib/
Note: prelink map 及び、共有ライブラリにおけるprelink定義について
androidの共有ライブラリはデフォルトでprelink mapというテキストファイルに、メモリマップテーブルに固定アドレスでマッピングするように構成されています。
(~/mydroid/build/core/prelink-linux-arm.map)
これは、メモリ上への頻繁なロード・アンロードを避け、速度アップをするための処置となっています。~/mydroid/build/core/prelink-linux-arm.map (抜粋)
001 : 002 : # 0xC0000000 - 0xFFFFFFFF Kernel 003 : # 0xB0100000 - 0xBFFFFFFF Thread 0 Stack 004 : # 0xB0000000 - 0xB00FFFFF Linker 005 : # 0xA0000000 - 0xBFFFFFFF Prelinked System Libraries 006 : # 0x90000000 - 0x9FFFFFFF Prelinked App Libraries 007 : # 0x80000000 - 0x8FFFFFFF Non-prelinked Libraries 008 : # 0x40000000 - 0x7FFFFFFF mmap'd stuff 009 : # 0x10000000 - 0x3FFFFFFF Thread Stacks 010 : # 0x00000000 - 0x0FFFFFFF .text / .data / heap 011 : 012 : # core system libraries 013 : libdl.so 0xAFF00000 ・ ・ ・ 112 : libctest.so 0x9A700000 113 : libUAPI_jni.so 0x9A500000 114 : librpc.so 0x9A400000 115 : libtrace_test.so 0x9A300000 116 : libsrec_jni.so 0x9A200000上記のように、prelink mapに登録するモジュール名及び固定アドレスを列挙して定義を行います。
しかし、固定アドレスにマッピングされるという事は、ビルドの度にシステムイメージまで作成し、入れ替えないといけないという事を意味しています。
これではJNIを使用するアプリケーションはインストールを自由に行うことが極めて困難であると言わざるを得ません。
Android.mkでは、デフォルト動作がprelink map有効となっており、prelink mapに共有ライブラリを登録しないと、ビルドエラーが発生してしまいます。ビルドエラーの例
01 : ~/mydroid/external/libJNItestNative$ mm 02 : make: ディレクトリ `/home/kenken/mydroid' に入ります 03 : build/core/product_config.mk:211: WARNING: adding test OTA key 04 : ============================================ 05 : TARGET_PRODUCT=generic 06 : TARGET_BUILD_VARIANT=eng 07 : TARGET_SIMULATOR= 08 : TARGET_BUILD_TYPE=release 09 : TARGET_ARCH=arm 10 : HOST_ARCH=x86 11 : HOST_OS=linux 12 : HOST_BUILD_TYPE=release 13 : BUILD_ID= 14 : ============================================ 15 : build/core/main.mk:180: implicitly installing apns-conf_sdk.xml 16 : target thumb C: libJNItestNative <= /home/kenken/mydroid/external/libJNItestNative/getTestStringFromNative.c 17 : target SharedLib: libJNItestNative (out/target/product/generic/obj/SHARED_LIBRARIES/libJNItestNative_intermediates/LINKED/libJNItestNative.so) 18 : target Prelink: libJNItestNative (out/target/product/generic/symbols/system/lib/libJNItestNative.so) 19 : build/tools/apriori/prelinkmap.c(137): library 'libJNItestNative.so' not in prelink map 20 : make: *** [out/target/product/generic/symbols/system/lib/libJNItestNative.so] エラー 1 21 : make: ディレクトリ `/home/kenken/mydroid' から出ます上記のビルドエラーの場合、19行目に、「prelink mapに含まれていない」(library 'libJNItestNative.so' not in prelink map)というエラーが確認できます。
prelink mapに登録せず、ビルドを行うためには、Android.mkに以下の1行を追加し、ビルドを行います。(prelink 無効化の定義)
LOCAL_PRELINK_MODULE := false
3.アプリケーションの実行
androidアプリ実行の前に、共有ライブラリ libJNItestNative.so を、あらかじめエミュレータの/sysytem/lib ディレクトリにコピーします。
1. コマンドプロンプトを起動し、androidエミュレータを以下のコマンドで起動します。>start emulator
2. エミュレータ上のディレクトリ /system/lib への書き込みを有効にするために、以下のコマンドを実行します。
>adb remount
3. 共有ライブラリを /system/lib ディレクトリにコピーします。カレントディレクトリに、「2. 共有ライブラリ libJNItestNative.so の作成」で作成したモジュールをあらかじめ用意した上、以下のコマンドを実行します。
>adb push libJNItestNative.so /system/lib
4. androidエミュレータの起動を維持したままの状態で、eclipseからandroidアプリJNItestを
起動します。
起動に成功すると、以下のように実行結果が表示されます。
以上です。
もし、記述間違いなどがありましたらご指摘いただけると幸いです。