AndroidでJNIを使う方法

AndroidでJNIを使う方法をドキュメントにまとめました。
PDF版はこちら
Androidコンパイル環境を構築されていることが前提です。
よろしければAndroidのコンパイル環境を整える方法にあるPDFのコンパイル環境構築資料をご覧ください。

AndroidでJNI – Android meets JNI

株式会社ブリリアントサービス 勉どろいどチーム 
                   和泉憲二
                   門口敏広
                   藤田竜史


 このドキュメントでは、androidアプリケーション(Dalvik VM)からJNI(Java Native Interface)を使用して、C/C++言語で作成した共有ライブラリのJNIメソッドをコールする、一連の方法について解説します。

開発環境

本ドキュメントでは、以下の開発環境が用意されている事を前提に、説明を進めます。

用意する環境 本ドキュメントにおける確認済みの環境
androidアプリケーション開発環境(eclipse + android ADT) WindowsXP SP3上
JDK開発環境 WindowsXP SP3上のJDK 1.6.0.12
android build環境*1 WindowsXP SP3のVMware上にUbuntu Linux 8.04日本語版で構築

本ドキュメントで作成する、サンプルアプリケーションの説明

androidアプリ側からJNIメソッドをコールし、JNIメソッドより返却された文字列をandroidアプリのTextViewで描画します。

androidアプリケーション JNItest JNIメソッドから返却された文字列をTextViewを使用して画面上に表示する。
共有ライブラリモジュール libJNItestNative コールされた戻り値として、文字列を返却する。

また今回作成するネイティブメソッドの仕様は、以下の通りとします。

ネイティブメソッド名 getTestStringFromNative
引数 なし
戻り値 String
説明 戻り値として、“from Native Code String”という文字列を返却します。
1.android アプリケーション JNItest の作成
androidアプリケーションの作成

まず、JNIヘッダの生成を行うために、ライブラリのロードとネイティブメソッド定義を含むソースコードを作成します。
eclipseandroid 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 : #endif
2.共有ライブラリ 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 : }
MakefileAndroid.mk」の作成

androidの個別ビルド用MakefileAndroid.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を
起動します。
起動に成功すると、以下のように実行結果が表示されます。

以上です。
もし、記述間違いなどがありましたらご指摘いただけると幸いです。

*1:androidソースコードツリーでシステムイメージがbuild出来る環境。ソースの入手方法・ビルド環境構築方法については、こちらを参照。