2018년 6월 22일 금요일

[JNI] JNI로 C와 Java 연동하기

개요


Java를 사용하면 하드웨어나 OS의 대부분 기능을 제한 없이 사용할 수 있지만 그래도 C와 같은 저수준의 언어보다 섬세할 수는 없다. 이를 해소하기 위해 Java와 C/C++ 사이에 인터페이스를 제공하는 것이 JNI(Java Native Interface)이다.

주로 JNI는 다음과 같은 이유로 사용된다.
1. 이미 C/C++로 작성된 라이브러리를 Java에서 활용하기 위해
2. 처리속도 향상을 위해
3. 하드웨어나 OS의 기능 중 Java에서 아직 제공하지 않는 기능을 사용하기 위해


참고 자료


The Java Native Interface: Programmer's Guide and Specification


개발 과정


1. Java에서 native method 선언


먼저 Java에서 native method를 선언한다.

package helloJNI;

public class HelloJNI
{
    static
    {
        System.loadLibrary("hellojni");
    }

    public native String printHello(String message);

    public static void main(String[] args)
    {
        HelloJNI helloJNI = new HelloJNI();
        System.out.println("Hello, from " + helloJNI.printHello("Java") + ".");
    }
}

native method는 "native" 키워드로 선언할 수 있으며, abstract method처럼 내용을 구현하지 않고 세미콜론으로 끝낸다.

native method는 런타임에 C로 작성된 공유 라이브러리의 함수를 호출하여 동작하므로 공유 라이브러리 파일이 필요하다. 이것은 System.loadLibrary() 메소드를 통해 이루어질 수 있으며, 인자로 공유 라이브러리의 이름이 들어간다.
위의 예에서 공유 라이브러리 이름은 "hellojni"이며, 이에 해당하는 공유 라이브러리 파일의 이름은 Windows에서 "hellojni.dll"이고, Linux에서 "libhellojni.so"이다.


2. Header file 생성하기


C로 JNI를 통해 Java에 제공할 함수를 만들기 전에, Java에서 이해할 수 있는 함수 프로토타입이 선언된 헤더 파일을 생성해야 한다.
헤더 파일은 JDK 내에 포함된 실행 파일인 javah를 통해 생성할 수 있다.

eclipse를 사용하여 간단히 javah를 사용하는 방법도 있지만 여기서는 명령행에서 사용하는 방법을 소개한다.

javah는 간단히 다음과 같이 사용할 수 있다.

> javah.exe [패키지명].[클래스명]

만약 이 명령어를 호출하는 디렉토리에 컴파일된 Java 패키지가 없는 경우에는 다음과 같이 경로를 지정해줄 수 있다.

> javah.exe -classpath .;[경로] [패키지명].[클래스명]

경로에는 eclipse로 개발하는 경우 프로젝트의 bin 디렉토리의 경로를 입력하면 된다.

".;"는 .class 파일을 검색할 디렉토리에 현재 디렉토리를 추가한다는 의미로 여기서는 없어도 무방하다. 여러 디렉토리에 대해 검색하고 싶은 경우 여러 경로를 세미콜론으로 구분하면 된다.

그 밖에 다른 유용한 옵션은 여기를 참고하자.

이번 예에서는 다음과 같이 헤더 파일을 생성하였다. (eclipse project의 bin 디렉토리 내에서 실행)

> javah.exe helloJNI.HelloJNI

이렇게 하면 현재 작업 디렉토리에 "helloJNI_HelloJNI.h"이라는 이름의 헤더 파일이 생성된다. [패키지명]_[클래스명].h 형식이며, 패키지 경로의 구분자 '.'은 '_'로 대체된다.

다음은 생성된 헤더 파일이다.

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class helloJNI_HelloJNI */

#ifndef _Included_helloJNI_HelloJNI
#define _Included_helloJNI_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     helloJNI_HelloJNI
 * Method:    printHello
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_helloJNI_HelloJNI_printHello
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

여기에 선언된 "Java_helloJNI_HelloJNI_printHello()" 함수를 구현하면 된다.


3. C언어로 JNI를 통해 Java에 제공할 함수 구현


이렇게 구현했다.

#include <stdio.h>
#include <jni.h>

#include "helloJNI_HelloJNI.h"

JNIEXPORT jstring JNICALL Java_helloJNI_HelloJNI_printHello
    (JNIEnv *env, jobject obj, jstring message)
{
    const char* msg = NULL;
    char* buf = "C world";

    msg = (*env)->GetStringUTFChars(env, message, NULL);
    if(msg == NULL)
        return NULL; /* error occurred */

    printf("Hello, from %s.\n", msg);

    (*env)->ReleaseStringUTFChars(env, message, msg);

    return (*env)->NewStringUTF(env, buf);
}

"jni.h" 파일을 include해야 하며, 이전 순서에서 생성한 헤더 파일도 include해야 한다.

위의 예제는 인자로 받은 String을 출력한 다음 다른 String을 return한다.

C에서 String을 포함한 Java object를 다루는 방법은 다음 글에서 설명한다.

이제 빌드하여 공유 라이브러리 파일을 생성하자


4. 공유 라이브러리 빌드 및 실행


음 그냥 빌드해서 공유 라이브러리를 생성하면 된다. 방법은 다양하다.
플랫폼에 따라 Visual studio를 사용해도 되고 mingw나 gcc를 사용해도 된다.

그러나 Java의 범용성을 활용하기 위해서는 여러 플랫폼에서, 또는 한 플랫폼에서 여러 플랫폼을 타겟으로 빌드하는 환경을 만들어 놓는 것이 좋다.
그래서 여기서도 CMake를 사용할 것이다.

CMake를 사용하면 단 한번의 빌드 형상 정의를 통해 여러 플랫폼에서 빌드할 수도 있고 크로스 컴파일을 통해 다른 플랫폼을 타겟으로 빌드할 수 있다.

즉 이 글에서 정의한 빌드 형상(CMakeLists.txt) 파일 하나로 다양한 타겟의 공유 라이브러리를 쉽게 만들 수 있다.

이 예제에서 CMakeLists.txt 파일은 다음과 같다.

cmake_minimum_required(VERSION 2.8.4)

if(WIN32)
    set(JDK_ROOT "D:/Program/Java/jdk1.8.0_172")
elseif(UNIX)
    set(JDK_ROOT "/usr/lib/jvm/java-8-openjdk-amd64")
endif()

include_directories(${JDK_ROOT}/include)
if(WIN32)
    include_directories(${JDK_ROOT}/include/win32)
elseif(UNIX)
    include_directories(${JDK_ROOT}/include/linux)
endif()

add_library(hellojni SHARED hellojni.c)

"JDK_ROOT" 변수의 값은 각자 환경에 맞게 수정하자.
시스템에 설치된 JDK의 루트 디렉토리로 설정하면 된다.

이 파일을 통해 빌드 형상은 다음과 같의 정의된다.
1. JDK 내에 있는 include 디렉토리와 그 안에 win32 디렉토리를 include directory 경로에 추가한다.
2. 공유 라이브러리로 빌드하기 위해 add_library()에 SHARED 옵션을 사용한다.
3. 이 때 공유 라이브러리 이름은 Java에서 System.loadLibrary() 메소드의 인자로 사용한 문자열과 동일해야 한다.

Visual studio 등을 사용할 때에도 위와 같이 설정하면 된다. 즉 include directory 경로 추가와, 공유 라이브러리 빌드 설정을 하면 된다.

공유 라이브러리가 생성되었으면 이제 실행하면 된다.
단, 실행하기 전에 공유 라이브러리 파일의 경로는 각자 환경에 맞게 잘 설정해야 한다.

Hello, from C world.
Hello, from Java.

댓글 1개: