레이블이 Java인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Java인 게시물을 표시합니다. 모든 게시물 표시

2018년 7월 4일 수요일

[JNI] 객체 사용과 메모리 관리

개요


JNI를 통해 C에서 Java의 객체에 접근할 수 있고 자유롭게 생성할 수 있다. 그러나 C에서 접근했거나 생성한 객체가 여전히 C에서 참조를 갖고 있는지 알 수 없으므로, garbage collector가 이러한 객체를 어떻게 처리할지 알 수 없게 된다. 이를 위해 JNI는 C에서 해당 객체에 대한 참조를 명시적으로 제거할 수 있는 함수를 제공하여 garbage collector가 잘 동작할 수 있도록 메커니즘이 마련되어 있다.

이번 글에서는 C에서 Java 객체를 사용하는 방법과, 메모리 관리 방법을 소개한다.


참고 자료


The Java Native Interface: Programmer's Guide and Specification


1. String


C에서 String을 처리할 수 있는 함수는 다음과 같다.


다음과 같이 사용할 수 있다.

Hello.java
public class Hello
{
    public native String getText(String message);
}

hello.c
JNIEXPORT jstring JNICALL
Java_Hello_getText(JNIEnv *env, jobject obj, jstring message)
{
    char buff[255];
    const char *msg;

    /* get string from java String object */
    msg = (*env)->GetStringUTFChars(env, message, NULL);
    if(msg == NULL)
        return NULL; /* OutOfMemoryError already thrown */

    printf("received from java : %s\n", msg);

    /* free the memory allocated for msg */
    (*env)->ReleaseStringUTFChars(env, message, msg);

    scanf("%s", buff);

    /* create java String object */
    return (*env)->NewStringUTF(env, buff);
}

Java String은 GetStringUTFChars() 함수를 통해 C 문자열(캐릭터의 배열)로 가져올 수 있다.
가져온 문자열은 사용이 모두 끝난 후에 ReleaseStringUTFChars() 함수를 통해 할당된 메모리 영역을 반환해야 한다.

C 문자열로부터 NewStringUTF() 함수를 통해 java String 객체를 생성할 수 있다.
이를 통해 생성된 객체는 전적으로 java에서만 사용되는 것으로 간주되며, C에서 참조를 갖고 있더라도 garbage collector가 이를 확인하지 않으므로, java에서의 참조만 없다면 해당 객체는 제거될 수 있다.


2. Object construction


객체를 생성하는 순서는 다음과 같다.
1. 생성할 객체의 class를 얻는다.
2. 생성자(constructor)를 얻는다.
3. 생성자 매개변수와 함께 객체를 생성한다.

다음은 사용 예이다.

JNIEXPORT jobject JNICALL
Java_Hello_getObject(JNIEnv *env, jobject obj)
{
    jClass class;
    jmethodID constructor;
    int parameter = 1;
    jobject result;

    /* get class */
    class = (*env)->FindClass(env, "java/lang/Integer");
    if(class == NULL)
        return NULL;

    /* get constructor */
    constructor = (*env)->GetMethodID(env, class, "<init>", "(I)V");
    if(constructor == NULL)
        return NULL;

    /* construct object */
    result = (*env)->NewObject(env, class, constructor, parameter);

    return result;
}

FindClass() 함수를 통해 특정 클래스를 얻어올 수 있다. 두 번째 매개변수로 얻어올 클래스의 패키지 경로를 포함한 전체 이름을 적는다.

GetMethodID() 함수는 특정 클래스의 메소드를 얻어오는 함수이다. 원래 세 번째 매개변수에는 메소드의 이름, 네 번째 매개변수에는 메소드의 시그니처를 넣어야 하지만, 생성자를 얻어올 경우 메소드의 이름을 "init"으로, 시그니처의 반환 타입은 void를 의미하는 "V"로 고정해야 한다.

메소드 시그니처는 메소드의 반환 타입과 매개변수들의 타입을 문자열로 정의한 것으로 "({매개변수}){반환타입}" 형식이다.

각각 java 타입에 해당하는 시그니처는 다음과 같다.

TypeSignature
voidV
booleanZ
byteB
charC
intI
longJ
floatF
doubleD
objectL{패키지 경로를 포함한 클래스 전체 이름};
type[][{해당 타입의 시그니처}

예를 들어서

void aaa() -> ()V
int bbb(boolean a, char, b) -> (ZC)I
String[] ccc(int[] c, MyClass d) -> ([ILmyPackage/MyClass;)[Ljava/lang/String;

이 된다.

마지막으로 NewObject() 함수를 통해 해당 객체를 생성한다. 4번째 파라미터부터는 지정된 생성자의 파라미터로 사용될 변수를 순서대로 넣으면 된다. 만약 생성자의 파라미터가 두 개라면 4번째 파라미터에 생성자의 1번째 파라미터, 5번째 파라미터에 생성자의 2번째 파라미터를 넣으면 된다.


3. Array


다음은 배열을 처리하는 함수들이다.


<Type>에 원하는 배열 타입을 입력하면 된다. 예를 들어 int 배열을 생성하고 싶은 경우
NewIntArray() 함수를 사용한다.

Get<Type>ArrayRegion() 함수와 Get<Type>ArrayElements() 함수는 모두 배열의 값을 얻어올 수 있다는 공통점이 있지만 사용 방법이 다르다.

Get<Type>ArrayRegion() 함수는 다음과 같이 얻어올 메모리 영역이 미리 확보되어 있을 때 사용한다.

JNIEXPORT jint JNICALL
Java_Hello_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
    int buf[10];
    int i, sum = 0;

    /* get int array data */
    (*env)->GetIntArrayRegion(env, arr, 0, 10, buf);

    for(i = 0; i < 10; i++)
        sum += buf[i];

    return sum;
}

반면 Get<Type>ArrayElements() 함수는 메모리 영역이 확보된 배열 포인터를 반환한다. 따라서 해당 포인터를 모두 사용하고 난 다음에는 Release<Type>ArrayElements() 함수를 통해 해당 메모리 영역을 반환해야 한다.

JNIEXPORT jint JNICALL
Java_Hello_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
    int *buf;
    int i, sum = 0;

    /* get int array data */
    buf = (*env)->GetIntArrayElements(env, arr, NULL);
    if(buf == NULL)
        return 0;

    for(i = 0; i < 10; i++)
        sum += buf[i];

    /* release int array memory */
    (*env)->ReleaseIntArrayElements(env, arr, buf, 0);

    return sum;
}


4. DeleteLocalRef()


만약 C에서 객체를 생성하였는데, Java로 반환되지도 않고 더 이상 사용되지 않는다면 DeleteLocalRef() 함수를 통해 반드시 해당 객체에 대한 참조를 지워야 한다.

예를 들어 다음과 같이 object array를 만드는 경우에 배열의 각 요소는 배열에 넣은 후 반환되지 않고 더 이상 사용되지 않는다. 이 때 DeleteLocalRef() 함수를 호출해야 한다.

JNIEXPORT jobjectArray JNICALL
Java_Hello_getNameList(JNIEnv *env, jobject obj)
{
    int i;
    char buf[255];
    jstring name;

    jclass stringClass;
    jobjectArray nameList;

    /* construct String array */
    stringClass = (*env)->FindClass(env, "java/lang/String");
    if(stringClass == NULL)
        return NULL;

    nameList = (*env)->NewObjectArray(env, 10, stringClass, NULL);
    if(nameList == NULL)
        return NULL;

    printf("enter 10 names\n");
    for(i = 0; i < 10; i++)
    {
        scanf("%s", buf);

        /* construct new String */
        name = (*env)->NewStringUTF(env, buf);
        if(name == NULL)
            return NULL;

        /* insert String to array */
        (*env)->SetObjectArrayElement(env, nameList, i, (jobject)name);

        /* delete local reference */
        (*env)->DeleteLocalRef(env, name);
    }

    return nameList;
}

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.