[211005] JNI(Java Native Interface)로 C++ dll 사용하기 - cmd 사용
프로젝트 진행 중에 dll을 java로 사용해야 하는 일이 생겼다. 이때 JNI를 처음 알게 되었고, 성공하기까지 엄청난 삽질을 했다... 삽질을 하는 내내 성공하게 되면 꼭 블로그에 기록해야겠다고 생각했다.
이 글은 JNI에 대한 간단한 설명, 그리고 실습 예제를 다룰 것이다. cmd를 사용해서 자바를 컴파일하고 헤더를 생성한 후, C++ 환경에서 dll을 생성하였다.
.
JNI(Java Native Interface)
java는 JVM을 통해서 어느 장치에서나 유연하게 사용할 수 있다는 장점이 있지만, 그럼에도 불구하고 특정 프로세서에서 돌아가는 코드를 사용해야 하는 일이 생긴다. 예를 들면 아래와 같다.
몇몇 하드웨어를 다룰 때
프로세스의 성능 개선이 필요할 때
외부 프로그래밍 코드를 자바에서 재사용하고자 할 때
JDK는 이런 상황을 위해서 JVN과 native code(주로 C, C++)를 연결해주는 툴을 제공하는데, 그것이 JNI(Java Native Interface)다.
JNI 사용하기
기본적인 사용 순서는 아래와 같다.
1. dll에 정의될 native 함수 정보를 포함해서 자바 클래스를 만든다.
2. 클래스를 컴파일하고, 컴파일해서 나온 .class 파일로 헤더 파일을 생성한다.
3. 2에서 만든 헤더 파일을 사용해서 c++로 dll을 만든다.
4. 3에서 만든 dll을 1에서 불러오게 만든다.
1. 자바 클래스 만들기
public class Student{
static {
System.loadLibrary("Student");
}
public static void main(String[] args) {
Student student = new Student();
System.out.println("number of student: " + student.getStudentNum());
System.out.println("hello student: " + student.sayHello());
}
private native int getStudentNum();
private native String sayHello();
}
간단한 클래스를 만들었다.
dll에 선언될 함수의 이름을 native 키워드를 붙인 메소드 형태로 선언해줘야 한다. 몸체는 없어도 된다.
그리고 static 안에 System.loadLibrary("dll이름")를 사용해서 프로그램이 실행될 때 라이브러리를 로드할 수 있도록 해야 한다. System.loadLibrary()를 사용할 때 파라미터로 넘기는 dll 이름에 확장자는 붙이지 않아도 된다. 그렇지만 나중에 프로그램을 실행할 때 라이브러리가 있는 경로에 대한 classpath는 잡아줘야 한다. 이 부분은 4번에서 설명한다.
dll이름은 클래스의 이름과 똑같이 설정했다.
System.loadLibrary()와 유사한 메소드는 System.load()가 있다. System.load()는 dll 경로를 절대 경로로 넘겨야 하며, 확장자도 붙여야 한다.
2. 헤더 파일 생성
이전에는 javah를 사용해서 자바 헤더 파일을 생성했는데, java10 이후로는 javah가 제거되어서 사용할 수 없다.
컴파일을 할 때 javac -h 옵션을 사용해서 컴파일과 동시에 헤더 파일을 생성해줘야 한다.
아무런 로그가 뜨지 않았다는 성공한 것이다. java 파일이 있던 디렉토리에 가보면 .class 파일과 .h 파일이 생긴 것을 확인할 수 있다.
만약 자바 파일이 패키지 형태로 구성되어 있다면 위와 같은 방법으로 컴파일이 안 될 수도 있다. 안 될 경우에는... 구글링을 해서 찾아보기를 추천한다.
3. c++로 dll 만들기
visual studio를 사용했으며, 먼저 빈 프로젝트를 생성한다. 이때 설정하는 프로젝트 이름이 나중에 dll 이름이 된다. 프로젝트명을 위 자바 소스에서 System.loadLibrary()의 파라미터로 넘긴 이름과 똑같이 하면 된다.
대부분 java 64bit 버전을 쓰고 있을 것이다. 프로젝트 구성을 x64(32bit 자바를 쓴다면 x86으로 설정해야 한다.), Release 모드를 선택한다. 나는 다른 환경에서 내가 만든 dll 파일이 돌아가지 않는 현상 때문에 꼬박 하루를 삽질했는데, Release 모드로 디버깅하지 않았기 때문이었다.
헤더파일에 아까 생성한 Student.h 파일을 추가한다. 헤더 소스에 빨간 줄이 뜨는 걸 볼 수 있다. jni.h의 경로를 지정해줘야 한다. 상단 메뉴 탭에서 속성 > 프로젝트 속성 > 구성 속성 > VC++ 디렉터리 에 들어간 뒤 포함 디렉터리를 수정한다.
[$자바 설치 경로]\include
[$자바 설치 경로]\include\win32
두가지를 추가해준다. 확인 후 적용하면 빨간 줄이 사라지는 걸 볼 수 있다.
구성 속성 > 일반 탭에서 구성형식도 exe에서 dll로 바꿔준다.
설정이 끝났으면 헤더 파일을 보자.
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Student */
#ifndef _Included_Student
#define _Included_Student
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Student
* Method: getStudentNum
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_Student_getStudentNum
(JNIEnv*, jobject);
/*
* Class: Student
* Method: sayHello
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_Student_sayHello
(JNIEnv*, jobject);
#ifdef __cplusplus
}
#endif
#endif
헤더파일 생성 시 자동으로 코드가 작성된 것을 확인할 수 있다.
맨 상단에 DO NOT EDIT THIS FILE - it is machine generated 라고 적힌 것을 확인할 수 있다.
헤더파일은 수정할 필요 없이 그대로 가져다 쓰면 된다.
이제 헤더파일을 사용하는 c++ 파일을 만든다. Student.cpp 파일을 생성한 후 내용을 작성하였다.
#include <jni.h>
#include <Windows.h>
#include "Student.h"
int numOfStudent = 10;
/*
* 학생 수를 반환하는 함수
*
*/
JNIEXPORT jint JNICALL Java_Student_getStudentNum(JNIEnv* env, jobject obj) {
return numOfStudent;
}
/*
* 학생이 인사하는 함수
*
*/
JNIEXPORT jstring JNICALL Java_Student_sayHello(JNIEnv* env, jobject obj) {
char buf[] = "안녕하세요";
jstring str = env->NewStringUTF(buf);
return str;
}
헤더에 있는 함수명을 그대로 가져와서 사용했다. 함수 몸체에 필요한 기능을 간단하게 구현해주었다. c++ 파일에도 jni.h를 추가해주어야 한다.
생성한 자바 헤더 파일을 include해주는 걸 잊지 말아야 한다.
작성 후 [로컬 windows 디버거]버튼을 누르면 경고문이 뜬다.
그 후에 [$프로젝트 경로\Release] 혹은 [$프로젝트 경로\x64\Release] (혹은 Debug)로 이동하면 dll 파일이 잘 생성된 것을 확인할 수 있다.
4. dll 실행
먼저 dll을 라이브러리 파일을 관리하기에 적합한 장소로 옮겨준다. 어디에 둬도 괜찮지만 통상적으로 패키지 형태의 프로젝트라면 lib 아래에, 혹은 자바 설치 경로의 \bin 아래에 두는 것 같다. 나는 자바 파일이 있는 폴더 안에 새로운 디렉토리를 만들어서 그 아래에 dll 파일을 두기로 했다.
이제 프로그램을 실행한다.
나는 임의의 경로에 dll 파일을 두었기 때문에, -Djava.library.path 속성 값으로 dll의 경로를 줬다. 실행하면 dll에 정의된 함수가 잘 실행되는 것을 확인할 수 있다.
classpath가 잡혀있는 경로 아래(java\bin, windows\system32 등)에 dll을 뒀다면 java.library.path를 지정해줄 필요가 없다.
* dll 연동 중에 발생했던 문제 사항
- java.lang.UnsatisfiedLinkError : 라이브러리를 찾지 못해서 생기는 문제다. -Djava.library.path 속성 값으로 dll이 위치한 경로를 넘겨주거나, classpath 경로 안에 dll을 위치시킨다. 이래도 해결되지 않는다면 dll 내부적인 문제거나, 다른 예상 못한 문제일 가능성이 있다. 헤더파일을 포함하지 않았거나... 내 경우에는 dll을 Release 모드로 빌드하지 않아서 해당 에러가 떴다.
- Can't load IA 32-bit .dll on a AMD 64-bit platform : 실행하는 java 버전과 dll의 버전이 호환되지 않아서 생기는 문제다. 어느 한 쪽을 32비트나 64비트로 바꿔줘야 한다.
- 참고 자료
https://m.blog.naver.com/sssang97/221737572369
https://stackoverflow.com/questions/1358541/jni-hello-world-unsatisfied-link-error