오늘은 JNI를 이용한 C모듈을 안드로이드에 적용하는 방법을 설명하겠습니다.

이 부분은 좀 방대한 부분이고, 저 또한 짧은 시간에 글을 적어야 하므로 충분한 캡처를 동반하지 못함을 아쉽게 생각합니다.

 

우선 기본환경은

이클립스  +  NDK r5b + 우분투 11.04 이며, 모듈 컴파일은 Cygwin에서 사용해도 상관 없습니다.

(참고로 저는 윈도우즈 환경에서 우분투를 VM 으로 사용합니다.)

 

작업순서는 다음과 같이 하려 합니다.

 

가. 모듈 컴파일을 위한 NDK  다운로드 및 환경설정

나. *.java 에서 *.h, *.c의 JNI 파일 만드는 방법

다. 안드로이드 프로젝트 내에 JNI 쓰는 방법

 

가. 리눅스에서 NDK  설정

 

1. 우선 NDK 부터 사이트에서 받도록 합니다. 윈도우즈 환경에서 MS cl.exe 컴파일러를 이용해 *.dll 모듈을 만들어도 되지만

 저는 리눅스의 *.so 모듈을 만들기 위해서 리눅스용 버전을 다운로드 받습니다.

http://developer.android.com/sdk/ndk/index.html  

 

2. 다운로드를 받았으면 압축을 해제하고 자신의 홈 계정 디렉토리 밑으로 옮깁니다.

 예) home/maluchi/android-ndk-r5b


maluchi@ubuntu:~/android-ndk-r5b$ ls
GNUmakefile  build               ndk-build  projects  tests
README.TXT   docs                ndk-gdb    samples   toolchains
RELEASE.TXT  documentation.html  platforms  sources
maluchi@ubuntu:~/android-ndk-r5b$ pwd
/home/maluchi/android-ndk-r5b

3. 폴더 위치에 상관없이 ndk-build 명령이 수행되도록 PATH를 다음처럼 추가합니다.

   자신의 홈으로 가서 .bash_profile 파일을 만들어서 다음처럼 추가하고 저장합니다.

   PATH=$PATH:$HOME/android-ndk-r5b

maluchi@ubuntu:~/android-ndk-r5b$ cd
maluchi@ubuntu:~$ ls
android-ndk-r5b          androiddev        workspace  문서      사진
android_gingerbread      examples.desktop  공개       바탕화면  음악
android_gingerbread.tgz  froyo2.2.tgz      다운로드   비디오    템플릿
maluchi@ubuntu:~$ vi .bash_profile 

4. 콘솔에서 source ~/.bash_profile 을 해서 바로 업데이트 하고, 잘 입력이 되었는지 env 명령을 넣어 확인합니다.

maluchi@ubuntu:~$ source ~/.bash_profile 
maluchi@ubuntu:~$ env
......

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/maluchi/android-ndk-r5b
....

 

5. 마지막으로 NDK 폴더 내 samples->hello-jni->jni->Android.mk 를 열어 보면 다음과 같은 부분이 있습니다.

LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.c

 

LOCAL_MODULE 은 모듈명이며, LOCAL_SRC_FILES는 C의 구현파일입니다. 여러개 존재한다면 '\ ' 추가합니다.

차후에 Android.mk를 복사해서 수정할 것입니다.

예)

LOCAL_SRC_FILES := hello-jni.c \

                                    test.c

 

6. Android.mk 파일 내용을 봐 보겠습니다. 리눅스에서 해당 파일을 받기 힘드시면 아래 내용으로 파일을 만들어도 됩니다. 

maluchi@ubuntu:~/android-ndk-r5b/samples/hello-jni/jni$ more Android.mk 
# Copyright (C) 2009 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := hello-jni
LOCAL_SRC_FILES := hello-jni.c

include $(BUILD_SHARED_LIBRARY)

maluchi@ubuntu:~/android-ndk-r5b/samples/hello-jni/jni$

 

이상으로 리눅스에서 모듈을 컴파일을 위한 환경은 완료가 되었습니다.

 

-------------------------------------------------------------------------------------------------

 

나. *.java 에서 *.h, *.c의 JNI 파일 만드는 방법

 

1. 이제 C모듈(*.so)를 java로 가져오는 부분이 필요합니다.  CalcJni.java 라는 파일을 만듭니다.

그리고 다음처럼 추가합니다. 사칙연산을 수행하는 네이티브 메서드를 정의하고 "CalcJni" 라는 C 모듈을 로드하는 역할을 합니다.

01.//package com.maluchi.CalcJNITest;
02.public class CalcJni
03.{
04.public CalcJni(){};
05.public native int Sum(int a, int b);
06.public native int Sub(int a, int b);
07.public native int Mul(int a, int b);
08.public native int Div(int a, int b);
09.public native String message();
10. 
11.static{
12.System.loadLibrary("CalcJni");
13.};}


2. 위의 Java 파일로부터 JNI의 C헤더(*.h) 파일을 만드는 것을 수작업으로 하기에는 번거로운 면이 있습니다.

하지만, Java에서 친절하게도 C헤더 파일을 만들어주는 명령이 있습니다. cmd 창을 하나 띄웁니다.

 

3. 자바 SDK가 설치되었다는 전제하에 해당 경로로 이동하여 *.class 파일을 만듭니다.

 >javac CalcJni.java

 

4. *.h 헤더 파일을 만듭니다.

>javah CalcJni

 

5. 다음처럼 CalcJni.h 헤더가 만들어진 것을 확인할 수 있습니다.

C:\tmp\jni>dir
 C 드라이브의 볼륨: Win7
 볼륨 일련 번호: D037-8597

 C:\tmp\jni 디렉터리

2011-06-16  오후 04:23    <DIR>          .
2011-06-16  오후 04:23    <DIR>          ..
2011-06-16  오후 04:22               425 CalcJni.class
2011-06-16  오후 04:23             1,011 CalcJni.h
2011-06-16  오후 04:22               391 CalcJni.java
               3개 파일               1,827 바이트
               2개 디렉터리  20,543,619,072 바이트 남음

6. CalcJni.h 에 맞춰 CalcJni.c 파일을 만듭니다. 이 때 가장 중요한 메서드 명명 규칙이 있습니다.

javah로 *.h를 만들면 아래처럼 패키지명이 적용되지 않고 만들어집니다.

 JNIEXPORT jint JNICALL Java_CalcJni_Sum(JNIEnv *, jobject, jint, jint);

 

중요한 것은 JNICALL Java_패키지명_클래스명_메서드(...)  규칙으로 적용해야 한다는 것입니다.

실제 안드로이드 패키지를 만드고 그곳에서 C 모듈을 로드한다면 반드시 패키지명_클래스명 을 확인해야 합니다.

그렇지 않으면 에러의 원인이 됩니다. 더불어 JNI에서의 구현부는 반드시 파라미터명을 주도록 되어 있으므로 생략되어 있는 파라미터 변수명들을 넣습니다.

 

 다시 1번에 가서 보시면 //package com.maluchi.CalcJNITest; 주석 처리된 것을 볼수 있습니다.

이는 후에 저 패키지에 자바파일이 들어가게 되고  메서드명들을 아래처럼 모두 바꿀 필요가 있기에 임시 주석처리를 한 것입니다. 

예) JNIEXPORT jint JNICALL Java_com.maluchi.CalcJNITest_CalcJni_Sum(JNIEnv *, jobject, jint, jint);

 

CalcJni.h

01./* DO NOT EDIT THIS FILE - it is machine generated */
02.#include <jni.h>
03./* Header for class CalcJni */
04. 
05.#ifndef _Included_CalcJni
06.#define _Included_CalcJni
07.#ifdef __cplusplus
08.extern "C" {
09.#endif
10./*
11.* Class:     CalcJni
12.* Method:    Sum
13.* Signature: (II)I
14.*/
15.JNIEXPORT jint JNICALL Java_com.maluchi.CalcJNITest_CalcJni_Sum
16.(JNIEnv *, jobject, jint, jint);
17. 
18./*
19.* Class:     CalcJni
20.* Method:    Sub
21.* Signature: (II)I
22.*/
23.JNIEXPORT jint JNICALL Java_com.maluchi.CalcJNITest_CalcJni_Sub
24.(JNIEnv *, jobject, jint, jint);
25. 
26./*
27.* Class:     CalcJni
28.* Method:    Mul
29.* Signature: (II)I
30.*/
31.JNIEXPORT jint JNICALL Java_com.maluchi.CalcJNITest_CalcJni_Mul
32.(JNIEnv *, jobject, jint, jint);
33. 
34./*
35.* Class:     CalcJni
36.* Method:    Div
37.* Signature: (II)I
38.*/
39.JNIEXPORT jint JNICALL Java_com.maluchi.CalcJNITest_CalcJni_Div
40.(JNIEnv *, jobject, jint, jint);
41. 
42./*
43.* Class:     CalcJni
44.* Method:    message
45.* Signature: ()Ljava/lang/String;
46.*/
47.JNIEXPORT jstring JNICALL Java_com.maluchi.CalcJNITest_CalcJni_message
48.(JNIEnv *, jobject);
49. 
50.#ifdef __cplusplus
51.}
52.#endif
53.#endif


CalcJni.c

01.#include <stdio.h>
02.#include "CalcJni.h"
03. 
04.JNIEXPORT jint JNICALL Java_com_maluchi_CalcJNITest_CalcJni_Sum
05.(JNIEnv *env, jobject obj, jint a, jint b)
06.{
07.return a+b;
08.}
09./*
10.* Class:     CalcJni
11.* Method:    Sub
12.* Signature: (II)I
13.*/
14.JNIEXPORT jint JNICALL Java_com_maluchi_CalcJNITest_CalcJni_Sub
15.(JNIEnv *env, jobject obj, jint a, jint b)
16.{
17.return a-b;
18.}
19./*
20.* Class:     CalcJni
21.* Method:    Mul
22.* Signature: (II)I
23.*/
24.JNIEXPORT jint JNICALL Java_com_maluchi_CalcJNITest_CalcJni_Mul
25.(JNIEnv *env, jobject obj, jint a, jint b)
26.{
27.return a*b;
28.}
29./*
30.* Class:     CalcJni
31.* Method:    Div
32.* Signature: (II)I
33.*/
34.JNIEXPORT jint JNICALL Java_com_maluchi_CalcJNITest_CalcJni_Div
35.(JNIEnv *env, jobject obj, jint a, jint b)
36.{
37.if(a == 0 || b ==0)
38.return 0;
39.return a/b;
40.}
41./*
42.* Class:     CalcJni
43.* Method:    message
44.* Signature: ()Ljava/lang/String;
45.*/
46.JNIEXPORT jstring JNICALL Java_com_maluchi_CalcJNITest_CalcJni_message
47.(JNIEnv *env, jobject obj)
48.{
49.return (*env)->NewStringUTF(env, "Hello world!! Welcome to maluchi's world");
50.}


7. 이제 JNI 파일까지 모두 준비가 됐습니다.

 

------------------------------------------------------------------------------------------------------

 

다) 안드로이트 프로젝트에서 JNI 사용하는 방법

 가)에서 NDK를 설정하여 *.so 파일을 만드는 환경 설정을 했고, 

 나)에서 *.so 의 구현 C모듈에서 Java로 읽어들오도록 인터페이스를 만들고 c 파일 구현까지 했습니다.

 

이제 실질적으로 구현한 C 파일을 리눅스에서 컴파일을 하고 *.so 파일을 얻은 후 안드로이트 프로젝트에 넣어서 실제 동작하는지 테스트 해보는 일만 남았습니다.

이 작업을 설명하겠습니다.

 

우선 안드로이드 프로젝트를 하나 만듭니다.

 

1. CalcJNITest 라는 프로젝트를 만들고 패키지경로를 com.maluchi.CalcJNITest 로 합니다.

 

2. 이 프로젝트에 jni 라는 폴더를 만들고 아까 만들어둔 CalcJni.h, CalcJni.c, CalcJni.java 를 넣고, 가) 에서 봤던 Android.mk도 복사해서 넣습니다.

반드시 jni 라는 폴더에 넣어야 합니다. 그리고 Android.mk 의 LOCAL_MODULE 과  LOCAL_SRC_FILES을 다음처럼 수정합니다.

 

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := CalcJni
LOCAL_SRC_FILES := CalcJni.c

include $(BUILD_SHARED_LIBRARY)

 

3. 이제 CalcJNITest라는 프로젝트를 리눅스 NDK홈에 projects라는 폴더를 만들고 projects 폴더로 옮깁니다.

maluchi@ubuntu:~/android-ndk-r5b/projects$ pwd
/home/maluchi/android-ndk-r5b/projects

maluchi@ubuntu:~/android-ndk-r5b/projects$ ls
CalcJNITest

4. CalcJNITest에 프로젝트에 들어가서 ndk-build 명령을 넣습니다.

해당 프로젝트에 jni 폴더 밑에 c 파일이 있고, ndk-build 는 이를 바탕으로 Libs 폴더를 만들어 *.so 파일을 생성합니다.

진행은 다음처럼 하시면 됩니다..

 

maluchi@ubuntu:~/android-ndk-r5b/projects/CalcJNITest$ ls
AndroidManifest.xml  bin                 gen  proguard.cfg  src
assets               default.properties  jni  res
maluchi@ubuntu:~/android-ndk-r5b/projects/CalcJNITest$ ndk-build
Compile thumb  : CalcJni <= CalcJni.c
SharedLibrary  : libCalcJni.so
Install        : libCalcJni.so => libs/armeabi/libCalcJni.so
maluchi@ubuntu:~/android-ndk-r5b/projects/CalcJNITest$ 

5. 결국 프로젝트에 Libs/armeabi/libCalcJni.so 파일이 만들어짐을 알게 됩니다.

Libs를 통째로 복사해서 윈도우의 해당 안드로이드 프로젝트에 복사합니다.

 

6. 이제 이 C구현부를 java로 불러들이는 일만 남았습니다.

 아까 만들어둔 jni/CalcJni.java를 com.maluchi.CalcJNITest 패키지로 복사를 합니다.

 그리고 CalcJni.java 파일 내에 주석 처리한 package package com.maluchi.CalcJNITest; 주석해제 시킵니다.

 

7. 구현을 다음과 같습니다.

01.package com.maluchi.CalcJNITest;
02. 
03.import android.app.Activity;
04.import android.os.Bundle;
05.import android.os.Handler;
06.import android.util.Log;
07.import android.widget.TextView;
08.public class CalcJNITest extends Activity {
09./** Called when the activity is first created. */
10.@Override
11.public void onCreate(Bundle savedInstanceState) {
12.super.onCreate(savedInstanceState);
13.setContentView(R.layout.main);
14. 
15.CalcJni jni = new CalcJni();
16. 
17.String string ="";
18.string = "Sum :"+jni.Sum(1010)+
19.", Sub: "+jni.Sub(2010)+
20.", Mul: "+jni.Mul(10010)+
21.", Div: "+jni.Div(10010)+
22.", Message: "+jni.message();
23. 
24.Log.d("maluchi", string);
25.TextView v = (TextView) findViewById(R.id.txt);
26.v.setText(string);
27. 
28.// mHandler.sendEmptyMessageDelayed(0, 300);
29. 
30.}
31. 
32.Handler mHandler = new Handler()
33.{
34.public void handleMessage(android.os.Message msg) {
35. 
36.CalcJni jni = new CalcJni();
37.String string = "";
38.string = "Sum :" + jni.Sum(1010) + ", Sub: " + jni.Sub(2010)
39.", Mul: " + jni.Mul(10010) + ", Div: "
40.+ jni.Div(10010) + ", Message: " + jni.message();
41.TextView v = (TextView) findViewById(R.id.txt);
42.v.setText(string);
43.}; 
44.};
45.}


마지막으로 에러 관련 상황입니다.

06-16 06:33:34.304: ERROR/AndroidRuntime(602): Uncaught handler: thread main exiting due to uncaught exception
06-16 06:33:34.354: ERROR/AndroidRuntime(602): java.lang.UnsatisfiedLinkError: Sum
06-16 06:33:34.354: ERROR/AndroidRuntime(602):     at com.maluchi.CalcJNITest.CalcJni.Sum(Native Method)
 

위처럼 나타나는 에러는 JNI의 메서드명 규칙의 JNICALL Java_ 패키지명_클래스명_메서드명() 규칙이 잘못됐을 때 나타는 현상입니다.

 

만약 다음과 같은 예외가 발생한다면 라이브러리 패스가 올바르게 설정되어 있는지 확인합니다.

java.lang.UnsatisfiedLinkError: no hello in shared library path at java.lang.Runtime.loadLibrary(Runtime.java)
        at java.lang.System.loadLibrary(System.java)
       at java.lang.Thread.init(Thread.java)


+ Recent posts