Hello JNI (1)

From Evernote:

Hello JNI

JNI가 무엇이고, 또 장단점은 무엇이고 하는 등의 내용들은 과감히 생략한다.

개략적인 작업 순서는 이러하다.

  1. 자바 클래스를 작성.
  2. 헤더 파일 추출.
  3. C 소스 파일 작성 및 컴파일.
  4. 자바 프로그램을 실행.


첫 번째 JNI 프로젝트는 Java에서 C로 전달하는 매개변수도 없고, C에서 Java로 전달받는 매개변수도 없다. 다만, C 라이브러리의 함수를 호출할 것이고, 호출된 C 함수에서는 화면에 한 줄 출력할 뿐이다.


자, 첫 번째 자바 클래스이다. 자바 프로젝트를 하나 만들고, 메인 클래스를 하나 생성한다.

package kr.pe.elex.example.jni;

/**
 * JNI 테스트
 *
 * @author Elex
 */
public class HelloJni {
       
        static {
              System.loadLibrary("libhellojni");
       }
       
        private native void printOut();

        public static void main(String[] args) {

              HelloJni test = new HelloJni();
              test.printOut();

       }

}


#. 스태틱 블럭 안에서 loadLibrary() 메서드를 사용해서 라이브러리 파일을 로드한다. 이때, .dll, .so 따위의 확장자명은 생략한다. JVM이 시스템에 맞게 알아서 찾아다가 불러온다.

#. C 라이브러리에서 처리할 메서드는 native 키워드를 붙인다. 당연히, 메서드의 몸체 부분은 없다.

#. main() 메서드는 그냥 인스턴스를 생성하고 메서드를 호출할 뿐...




다음엔, 생성된 자바 클래스 파일(.class)로부터 C 헤더 파일을 추출해야 하는데 javah.exe를 사용한다. javah 툴은 jdk의 bin 폴더 안에 있다. 사용법은 간단히 javah 뒤에 인자로 클래스명을 주고 실행하면 된다.
커맨드 프롬프트에서 다음과 같은 방식으로 입력한다.

javah kr.pe.elex.example.jni.HelloJni

그러면 헤더 파일(.h)이 하나 만들어지는데, 이 파일을 기초로 C 라이브러리를 생성할 것이다.
생성된 헤더 파일의 중요 부분만 표시하자면 다음과 같다.

#include <jni.h>

JNIEXPORT void JNICALL Java_kr_pe_elex_example_jni_HelloJni_printOut (JNIEnv *, jobject);

자바의 메서드 이름이 C 함수에서는 어떻게 지정되어야 하는지 딱 보면 감이 올 것이다. 이것이 익숙해지면 javah 툴을 사용하지 않고도 직접 헤더 파일을 작성할 수 있는 경지에 오를 수 있다.




이제 C 라이브러리를 만들어야 하니 이클립스에서 C/C++ 퍼스펙티브로 전환, 새로운 C 프로젝트를 생성한다. 이때, 프로젝트 타입은 Shared Library로 지정한다.


프로젝트 안에 소스 폴더를 하나 만들어주고, 좀 전에 만들었던 헤더 파일을 가져온다. 
헤더 파일을 열어보면 알 수 있듯이, jni.h라는 파일을 필요로 하고 있다. 하지만, 방금 만든 C 프로젝트는 자바의 존재를 모르고 있을테니 몇가지 추가 설정이 필요하다.

프로젝트 설정 창을 열고서 C/C++ Build > Settings > Tool Settings > GCC C Compiler > Includes 에 인클루드 패스를 추가해야 한다. jdk 폴더 아래에 있는 include 폴더와 그 아래에 있는 win32 폴더를 추가해준다.
  • C:\Program Files\Java\jdk1.7.0_25\include
  • C:\Program Files\Java\jdk1.7.0_25\include\win32



MinGW C Linker > Libraries 부분에 자바 라이브러리 경로를 추가해 준다.
  • C:\Program Files\Java\jdk1.7.0_25\lib



끝으로, Build Artifact 탭으로 넘어와서 출력될 라이브러리 파일의 이름을 설정해 준다. 다음 그림과 같이 설정하면 libhellojni.dll 파일이 생성될 것이다.





이제 C 라이브러리 프로젝트에 C 소스 파일을 하나 추가하고, 다음과 같이 작성한다.

/*
 * hellojni.c
 *
 *  Created on: 2013. 9. 8.
 *      Author: Elex
 */

#include "kr_pe_elex_example_jni_HelloJni.h"

JNIEXPORT void JNICALL Java_kr_pe_elex_example_jni_HelloJni_printOut
  (JNIEnv *env, jobject obj)
{
        puts("Hello Jni!");
}


#. javah 툴을 사용해서 생성한 헤더 파일을 인클루드 시킨다.

#. 헤더 파일에 선언된 함수 원형을 복사해와서 함수 몸체를 구현한다. 헤더의 선언부에는 매개변수명 부분이 생략되어 있으니 추가한다. 

#. 함수의 몸체 부분은 단순히 화면에 한 줄 출력하도록 작성되었다.

#. 이클립스 툴바의 망치 모양 아이콘을 클릭하면 .dll 파일이 만들어진다.



이제 자바 프로젝트에게 생성된 라이브러리 파일의 경로를 알려줘야 한다. 자바 퍼스펙티브로 이동해서 프로젝트 설정 화면을 열고 Java Build Path > Source > Native library location 항목에 C 프로젝트의 Debug 경로를 지정해 준다. 
(* Debug 모드로 컴파일 했으니 Debug인 것이다.)
(** dll 파일을 자바 프로젝트 폴더로 복사해 오면 이런 설정 과정은 필요가 없다. 하지만, 지금은 C 파일을 수시로 변경할 것이므로 이렇게 설정해 두는 것이 오히려 편하다.)



이제, 자바 프로젝트를 실행하면 화면에 "Hello Jni!"가 출력되어야 하지만, 오류가 출력될 것이다.
Exception in thread "main" java.lang.UnsatisfiedLinkError: kr.pe.elex.example.jni.HelloJni.printOut()V
       at kr.pe.elex.example.jni.HelloJni.printOut( Native Method)
       at kr.pe.elex.example.jni.HelloJni.main( HelloJni.java:19)

원래 유닉스/리눅스 계열에서는 이렇게 까지만 하면 정상적으로 실행되지만 윈도우즈에서는 조금 다르다, 윈도우즈에서는...
뭐.. 윈도우즈 DLL 작성 규칙이 어쩌고 저쩌고 그러는데 ... 윈도우즈랑은 안친해서 그런건 잘 모르겠고... 여튼 DLL 함수명이 밑줄(_)로 시작해야 하는 모양이다. 
그러니, 다시 C 프로젝트로 넘어가서 C 소스 파일 및 헤더 파일의 함수명 앞에 '_'를 붙여 주고 다시 컴파일 한다. 수정된 C 소스 파일은 다음과 같다.

#include "kr_pe_elex_example_jni_HelloJni.h"

JNIEXPORT void JNICALL _Java_kr_pe_elex_example_jni_HelloJni_printOut
  (JNIEnv *env, jobject obj)
{
        puts("Hello Jni!");
}


자바 프로젝트로 넘어가서 실행해보면...

콘솔탭에서 출력된 결과를 확인할 수 있다.


<프로젝트 파일>
http://www.elex.pe.kr/attachment/cfile3.uf@253C6C4C522C7E54298579.7z
http://www.elex.pe.kr/attachment/cfile8.uf@21542E4C522C7E53117B80.7z


<예고>
이 글에서는 자바의 메서드를 실행하면 C의 함수가 실행되는 것까지만 다루고 있다.
다음 글에서는 C로부터 문자열을 받아오도록 수정해 보겠다.



덧.
#. System.loadLibrary()에는 라이브러리 파일의 확장자명을 붙이지 않는다. 라이브러리 이름만 넘겨주면 jvm이 알아서 라이브러리 경로등을 뒤져서 찾는다.

#. System.loadLibrary() 대신에 System.load()를 사용할 수도 있는데, 이 때에는 라이브러리 파일의 절대 경로를 지정해야 한다.

#. 자바 실행 명령에 -Djava.system.library.path 옵션을 추가해서 라이브러리 파일의 경로를 지정해 줄 수도 있다.

#. 일반적으로, 윈도우즈에서는 라이브러리 파일(.dll)은 PATH로 설정된 위치에 있으면 된다.

이 블로그의 인기 게시물

좌표 변환: 회전 이동

Unmappable character for encoding MS949

Hello JNI (3), C 라이브러리에 문자열 전달