Android/App개발2011. 6. 10. 13:08
http://www.hanb.co.kr/network/view.html?bi_id=1033
 

저자:
 김대곤

Java Native Interface에 관련된 서적을 찾으려고 아마존을 뒤적일 때, 다음과 같은 서평을 보게 되었다. “한 가지 분명한 사실은 JNI를 쓰지 않는 것이 훨씬 좋다는 것이다. JNI는 다루기 힘들고, 불안정하며, 에러가 발생할 소지가 많으며, 무엇보다도 JNI가 쏟아내는 Runtime 에러는 자바 가상 머신을 날려 버릴 수 있는 사악한 기술이다.” 이 서평을 쓴 사람은 그 책을 사서 봤을 것이며, 단지 관심에서가 아니라 심각하게 Java Native Interface를 사용했을 것이며, 아마도 사용하고 있을 것이다. JNI는 자주 사용되지는 않지만, 그렇다고 불필요한 기술도 아니다. 아니 절대적으로 필요한 기술임이 틀림없다.

Java Native Interface는 자바에서 다른 언어로 개발된 기능을 내부적으로 사용할 수 있게 해 준다. 이미 C 언어나 C++로 개발된 기능이 있다면 그것을 자바 프로그램 안에서 사용할 수 있게 해 준다. 이 글은 JNI을 사용하는 두가지 간단한 예제를 통해 JNI가 무엇이며, JNI를 어떻게 사용하는지에 대해 간단히 살펴볼 것이다. 첫 번째 예제는 항상 등장하는 HelloWorld식 예제이고, 두 번째는 C 언어로 개발된 정렬 프로그램을 자바 프로그램에서 사용하는 예제이다. 

썬 자바 사이트에는 JNI를 구현하는 여섯가지 단계를 설명하고 있다.
1. Native 메소드를 호출하는 자바 코드 작성.
2. 첫 단계에서 쓴 자바 코드의 컴파일.
3. javah 명령어를 사용하여 헤더 파일의 생성.
4. 지정된 언어로 호출되는 Native 메소드를 구현.
5. Native 메소드를 구현한 코드를 동적 라이브러리로 컴파일.
6. 실행.

Native 메소드를 호출하는 자바 코드에는 세 가지 요소가 포함된다. Native 메소드의 선언, 실제 Native 메소드의 사용, 동적 라이브러리를 시스템 상에 상주하도록 불러오는 것이다. 이 세 가지는 아래 코드에서 강조되어 있다.


[HelloWorld.java]
/*
 * Created on 2004. 12. 31.
 * File name : HelloWorld.java
 * @author DaeGon Kim
 *
 */
public class HelloWorld {

              String greeting = "Hello Java World";

              public native void display();

              public static void main(String[] args) {

                            HelloWorld hw = new HelloWorld()
              System.out.println(hw.greeting);
                            hw.display();
                            System.out.println(hw.greeting);

              }

              static {
                            System.load("D:\\Project\\Java\\hello.dll");
              }

}


두 번째와 세 번째 단계에서는 javac 명령어로 HelloWorld.java를 컴파일한 후, javah 명령어로 헤더 파일을 생성한다. 
> javac HelloWorld.java
> javah HelloWorld
javah 명령을 실행하면, 아래에 보이는 HelloWorld.h가 생성되며, 이 파일을 수정하지 않는다.


[HelloWorld.h]
/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloWorld
 * Method:    display
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloWorld_display
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif


헤더 파일에서 강조된 부분은 실제 C 언어로 구현되어야 하는 함수이다. JNIEXPORT와 JNICALL과 같은 자바와 관련된 용어의 등장에서 알 수 있듯이, 헤더 파일에 정의된 함수를 구현하는 C 파일을 컴파일하기 위해서는 자바 시스템에서 포함되어 있는 파일이 필요하다. 환경설정에 관한 설명은 C 파일의 컴파일 단계에서 설명하기로 하고, C 언어로 위 함수를 구현해 보자.


[HelloWorld.c]
#include 
#include "HelloWorld.h"
#include 


JNIEXPORT void JNICALL Java_HelloWorld_display(JNIEnv *env, jobject this) {

    jfieldID       fldid;
    jstring        greeting;
    const   char   *temp;

    // 넘겨지는 객체 this의 클래스를 가져온다.   
    jclass instance = (*env)->GetObjectClass(env,this);

    printf("   Start of C implementation.\n");
    
    if ( this == NULL ) {
        fprintf(stderr, "Input pointer is null!");
        return;
    }
    
    // 클래스 안에서 greeting이라는 속성이 가진 위치를 가져온다.
    fldid = (*env)->GetFieldID(env, instance, "greeting", "Ljava/lang/String;");
    
    if ( fldid == NULL ) {
        fprintf(stderr, "Failed to get the field id\n");
        return;
    } 
    
    // 실제 객체에서 fldid에 있는 값을 읽어온다.
    greeting  = (*env)->GetObjectField(env, this, fldid);
    // 일어온 값을 C 데이터로 변환한다.
    temp      = (*env)->GetStringUTFChars(env, greeting, NULL);
   
    printf("      %s \n", temp);

    // 자바의 String 필드에 저장할 값을 만든다.
    greeting = (*env)->NewStringUTF(env, "Hello C World");
   
    if ( greeting == NULL ) {
        printf("Out of memory\n");
        return;
    }
    
    // greeting에 저정돤 값을 자바 객체로 전달한다.
    (*env)->SetObjectField(env, this, fldid, greeting);

    printf("   End of C implementation \n");
   
    return;

}


이제 필요한 모든 파일을 갖추어졌다. 이제 컴파일하고 실행하는 일만 남아 있다. 컴파일 하기 위해서는 몇 가지 설정이 필요하다. 앞으로 기술되는 설정은 윈도우를 기준으로 하고 있고, 마이크로 소프트 C 컴파일러를 쓴다는 가정하에서 기술되었다. 먼저, jni.h가 필요하다. 이 파일은 자바 홈디렉토리(이하 JAVAHOME) 아래 include 디렉토리에 있으며, 그 아래에 있는 win32 디렉토리도 include에 포함되어 있어야 한다.

set INCLUDE=%JAVAHOME%\include;%JAVAHOME%\include\win32;%INCLUDE%

LIB 속성을 위해서는 JAVAHOME 아래에 있는 lib 디렉토리가 포함되어야 한다.

set LIB=%JAVAHOME%\lib;%LIB%

위에서 설명한 설정은 JNI를 사용하게 됨에 따라 추가적으로 해야 하는 설정이며, Command Prompt에서 VC7 컴파일러를 실행하고자 할 때는 다음의 설정이 추가적으로 필요하다.

INCLUDE : VC 홈 디렉토리 아래에 있는 include 디렉토리
LIB : VC 홈 디렉토리 아래에 있는 lib 디렉토리
PATH : Studio 홈 - Common7 아래에 있는 IDE, VC 홈 디렉토리 아래에 있는 bin 디렉토리.

이제 남은 일은 HelloWorld.c를 컴파일하고 HelloWorld를 실행하는 일만 남았다.

cl HelloWorld.c -Fehello.dll -MD -LD

위의 명령어를 실행하면 hello.dll 파일이 생성된다. 그리고 나서 HelloWorld 클래스를 실행하면 다음과 같은 결과를 얻게 될 것이다. 


Hello Java World
   Start of C implementation.
      Hello Java World
   End of C implementation
Hello C World


이것으로 첫 번째 예제가 성공한 것이다. 이 예제는 일반적으로 JNI가 사용되는 방식과 거리가 있어 보인다. JNI는 개발 중인 자바 어플리케이션이 있으며, 이미 개발된 Native 라이브러리가 존재한다. 두 번째 예제에서는 기존에 C 언어로 개발된 정수 정렬 함수를 자바에서 사용하는 예제이다. 

먼저 Sort.c를 살펴보자. 흔히 볼 수 있는 C로 구현된 Insertion 정렬 함수이다.


[Sort.c]
#include 
#include 

int *sortIntegers(int *A, int n) {
    
    int i   = 0;
    int j   = 0;
    int key = 0;
    
    for ( j=1; j < n ; j++ ) {
        key = A[j];
        i = j - 1;
        while ( i >=0 && A[i] > key ) {
            A[i+1] = A[i];
            i--;
        }
        A[i+1] = key;
    }
    
    return A;
    
}


Sort.h 에는 Sort.c 안에 구현된 함수의 리스트가 나열되어 있다.


[Sort.h]
int *sortIntegers(int *A, int n);


이제 이 함수의 기능을 사용하는 Sorting.java 를 작성하자.


[Sorting.java]
public class Sorting {

    public static native int[] sort(int[] A, int n);
    
    public static void main(String args[]) {
        
        int Input[] = {3,4,7,2,5,6,9,11,1,8,10};
        
        int sorted[] = sort(Input, Input.length);
        
        for (int i=0; i < sorted.length ; i++ ) {
            System.out.println("A[" + i + "] = " + sorted[i]  );
        }
    }
    
    static {
        System.load("D:\\Project\\Java\\sorting.dll");
    }
    
}


Sorting.java를 컴파일 하고, Sorting 클래스를 사용하여 Sorting.h 를 생성한다. 이제 남은 일은 HelloWorld.c 와 같은 입력값과 반환값을 전달해주는 C 프로그램이다. 


[Sorting.c]
#include 
#include "Sorting.h"
#include "Sort.h"
#include 

JNIEXPORT jintArray JNICALL Java_Sorting_sort(JNIEnv *env, jclass cl, jintArray arr, jint n) {

    jint       *A   = NULL;
    jintArray  temp = NULL;
    
    A = (*env)->GetIntArrayElements(env, arr, NULL);
    
    if ( A == NULL ) {
        fprintf(stderr, "Failed to get int array\n");
        return arr;
    }
    
    A = sortIntegers(A, n);
    
    temp = (*env)->NewIntArray(env, n);
    
    (*env)->ReleaseIntArrayElements(env, temp, A, 0);
    
    return temp;

}


Sorting.c 를 컴파일하자. 이 때 다음과 같은 에러가 발생한다면, 

“LINK : warning LNK4098: defaultlib 'LIBC' conflicts with use of other libs; use /NODEFAULTLIB:library”

Sorting.c 와 Sort.c가 다른 방식으로 컴파일 되었기 생긴 에러이다. Sort.c를 MD 옵션으로 다시 컴파일하면, 정상적으로 컴파일될 것이다. 이제 Sorting 클래스를 사용할 수 있다. Sorting 클래스를 실행하면 다음과 같은 출력을 보게 될 것이다.

A[0] = 1
A[1] = 2
A[2] = 3
A[3] = 4
A[4] = 5
A[5] = 6
A[6] = 7
A[7] = 8
A[8] = 9
A[9] = 10
A[10] = 11


필자는 여기에서 어떻게 Wrapper 프로그램을 작성하는지에 대해서 설명하지 않았다. 하지만 앞에서 다룬 두 예제에는 Primitive 타입인 int, 배열, 객체를 다루었기 때문에 각 경우에 해당되는 함수 리스트만 있으면, 프로그램에 별 어려움이 없으리라 생각된다. 다음 기사에는 JNI가 자바 가상 머신을 날려버려 전체 자바 어플리케이션이 다운되는 것을 피해가는 방법에 대해서 살펴보도록 하자.
Posted by 삼스