Cassandra는 최근에 JNI(Java Native Interface) 대신 JNA (Java Native Access) 방식을 이용해서 native 영역에 메모리를 copy 하는 작업이 많은 경우에 사용되어 플랫폼의 속도를 향상시키는 방법을 쓰고 있습니다. 또한 일부 임베디드 프로젝트나 서버프로젝트에서 JNA 기법을 사용하는 예들이 많아져 오픈소스를 활용하는 저희에게 좋은 정보이면서 점차적으로 알아야 할 정보가 될 것 같아서 관련된 내용을 공유하고자 합니다. (참고로 SWIG 이라는 라이브러리도 있는데, 제가 아직 몰라서 살펴보고 공유하겠습니다.)
1. JNI (Java Native Interface)
JNI는 Java 에서 native 영역(c, c++)으로 들어가 호출 또는 native (c, c++)에서 java로 호출하는 interface입니다. c, c++ 언어로 만든 라이브러리, 솔루션을 바로 java에서 사용할 수 있습니다. 웹 서비스의 경우라면 c, c++ 언어로 만든 이미지 합성 또는 동영상 라이브러리를 사용하여 서비스하는 경우라 할 수 있습니다. 안드로이드 프레임워크의 경우에는 open gl과 같은 영역에서 연동할 수 있도록 되어 있습니다.
리눅스에서 JNI를 간단히 테스트하는 코드를 보겠습니다.
자바 클래스는 간단히 리눅스 shared object (so) 을 읽어 native 지시자로 구현된 method를 호출합니다 .
HelloJNI.java
class HelloJNI {
native void printHello();
native void printString(String str);
static {
System.load("/work/JniTest/hellojni.so");
}
public static void main(String[] args) {
HelloJNI myJNI = new HelloJNI();
myJNI.printHello();
myJNI.printString("Hello in C");
}
}
먼저 컴파일을 하고, native 구현해야 할 함수를 정의하는 header 파일을 생성합니다.
# javac HelloJNI.java
# javah HelloJNI
HelloJNI.h 파일에는 구현해야 할 “Java_HelloJNI_printHello”, “Java_HelloJNI_printString” 함수를 정의하고 있습니다.
<HelloJNI.h>
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJNI
* Method: printHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloJNI_printHello
(JNIEnv *, jobject);
/*
* Class: HelloJNI
* Method: printString
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_HelloJNI_printString
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
그 다음은 HelloJNI.h 의 함수를 구현한 HelloJNI.c 파일을 간단히 작성합니다. printHello 함수는 단순히 문자열을 출력하고, printString 함수는 자바로부터 받은 문자열을 그대로 출력합니다.
<HelloJNI.c>
#include "HelloJNI.h"
JNIEXPORT void JNICALL Java_HelloJNI_printHello(JNIEnv *env, jobject obj) {
printf("Hello World !!! jni\n");
}
JNIEXPORT void JNICALL Java_HelloJNI_printString(JNIEnv *env, jobject obj, jstring string) {
const char *str = (*env)->GetStringUTFChars(env,string,0);
printf("%s\n", str);
return;
}
c코드를 컴파일하고 나서 shared object(so)로 만듭니다.
# gcc -c -I$JAVA_HOME/include -I$JAVA_HOME/include/linux HelloJNI.c
# gcc -shared -o HelloJNI.so HelloJNI.o
# ls -al HelloJNI.so
HelloJNI.so
이제는 마지막으로 자바를 실행합니다.
# java HelloJNI
Hello World !!!
Hello in C!!!
JNI 를 사용하기 위해서는 다음과 같이 6단계의 개발 단계를 거치게 됩니다. 번거롭지만 다들 JNI를 구현할 때는 이렇게 하고 있습니다.
JNI를 쓰는 이유는 java 영역에서 할 수 있는 부분을 Native 영역으로 이동하여 연동할 수 있고, 속도가 늦은 부분은 속도를 높일 수 있습니다. 안드로이드 플랫폼의 내부구조는 껍데기는 java이고, 내부는 JNI의 코드로 내부 모듈로 이루어져 있다고 할 수 있습니다.
JNI의 가장 큰 문제는 메모리 부분입니다. Native 로 구현한 공간에는 Auto GC를 하지 못하고 일일이 메모리 관리를 해야 하고, 잘못하면 메모리릭을 일어나게 할 수 있습니다. 또한 JVM 메모리를 침범하여 crash 가 되기도 합니다. 그래서 native 사용할 때는 항상 잘 사용해야 합니다.
2. JNA (Java Native Access)
JNI 개발 측면에서는 번거로운 부분이 많은데, 이런 부분을 쉽게 해주는 API가 있는데, 바로 JNA 입니다. JNA는 libffi (Foreign function interface library)라 불리는 native library를 사용하여 dynamic하게 쓸 수 있게 합니다. Native 언어로 만들어진 함수를 사용하기 위해서 Header 파일 생성, Header 파일을 구현한 C소스, compile 과정이 없습니다. 번거로운 과정이 많이 생략 가능합니다.
어떻게 사용되고 동작되는지 살펴보겠습니다.
JNA 라이브러리는 원래 java.net(http://java.net/projects/jna/)에 있었는데, 지금은 github(https://github.com/twall/jna)로 옮겨진 상태입니다.
https://github.com/twall/jna/downloads 에 접근해서 jna.jar(https://github.com/downloads/twall/jna/jna.jar)를 다운받습니다.
위에서 언급했던 JNI 예제의 Native 코드를 생성합니다.
<myLib.h>
void printHello();
void printString(char* str);
<myLib.c>
#include <stdio.h>
void printHello() {
printf("Hello World !!! jna\n");
}
void printString(char* str) {
printf("%s\n", str);
}
이 파일들을 컴파일하여 shared object 파일로 만듭니다.
# gcc -c myLib.c
# gcc -shared -o myLib.so myLib.o
# ls myLib.so
이제는 myLib.so 파일을 정적으로 읽는 java 클래스를 하나 생성합니다. com.sun.jan.Library를 상속하는 interface 를 하나 만들고, shared object 를 읽어 자기 자신의 Instance 를 생성하여 header 파일을 생성하지 않고 바로 native 코드의 함수를 호출할 수 있도록 합니다.
<HelloWorld.java>
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;
public class HelloWorld {
public static void main(String[] args) {
CLibrary.INSTANCE.printHello();
CLibrary.INSTANCE.printString("Hi\n");
}
}
interface CLibrary extends Library {
CLibrary INSTANCE = (CLibrary) Native.loadLibrary(
("/home/kimyonghwan/myLib.so"), CLibrary.class);
void printHello();
void printString(String str);
}
그리고, 바로 classpath에 jna.jar와 현재 디렉토리를 추가하여 컴파일과 실행을 하면 예측한 대로 결과가 나옵니다.
# javac -classpath jna.jar HelloWorld.java
# java -classpath jna.jar:. HelloWorld
Hello World !!! jna
Hi
JNA 방식으로 코딩을 하니 훨씬 이해가 쉽습니다. 기존에 이미 만들어진 shared object를 바로 클래스만 작성해서 호출하는 방식이기 때문에 복잡한 interface가 필요가 없습니다. 또한 JAVA의 SRC 디렉토리에는 지저분한 C header 파일과 C 소스 파일은 더 이상 필요 없을 것입니다 .
그러나 jna의 한계가 있습니다. C++ 코드를 사용할 수 없습니다. (jnaerator라는 오픈소스가 C++로 변환하기는 합니다만, thirty party라 제외합니다.) 또한, api 특성상 JNI의 성경을 다 포함하지 못합니다. 예를 들어 native에서 jvm을 start하는 것은 JNA가 지원하지 않습니다.
단순히 Java단에서 C 코드로 일을 시킬 때 아주 편리합니다. 권한 만 있다면 리눅스 또는 윈도우 커널 라이브러리에 접근해서 interface에 바인딩하며 명령어를 실행할 수 있습니다. 또한 JNI에서 한 것처럼 C언어에서 함수에 Pointer를 연결하여 Java에서 쓸 수 있도록 있으며, Callback 이 일어나면 Java로 올려 처리도 가능합니다.
이해를 돕기 위해서 재미있는 예제를 하나 소개하겠습니다. 윈도우에서 제공하는 core library 중의 하나인 user32.dll 라이브러리를 읽어서 윈도우 OS의 현재 실행 중인 window 객체의 text를 얻어오는 예제입니다.
<User32Test.java>
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.win32.StdCallLibrary;
public class User32Test {
public interface User32 extends StdCallLibrary {
User32 INSTANCE = (User32) Native.loadLibrary("user32", User32.class);
interface WNDENUMPROC extends StdCallCallback {
boolean callback(Pointer hWnd, Pointer arg);
}
boolean EnumWindows(WNDENUMPROC lpEnumFunc, Pointer arg);
int GetWindowTextA(Pointer hWnd, byte[] lpString, int nMaxCount);
}
public static void main(String[] args) {
final User32 user32 = User32.INSTANCE;
user32.EnumWindows(new User32.WNDENUMPROC() {
int count;
public boolean callback(Pointer hWnd, Pointer userData) {
byte[] windowText = new byte[512];
user32.GetWindowTextA(hWnd, windowText, 512);
String wText = Native.toString(windowText);
wText = (wText.isEmpty()) ? "" : "; text: " + wText;
System.out.println("Found window " + hWnd + ", total "
+ ++count + wText);
return true;
}
}, null);
}
}
Eclipse에서 소스를 복사하고, build path에 jna.jar를 넣어주고 Run 을 실행하면 재미있는 결과가 나옵니다.
<결과>
..
Found window native@0x1074c, total 58; text: 네이버 백신
Found window native@0x10580, total 59; text: Network Flyout
Found window native@0x103fe, total 61; text: N드라이브 탐색기
..
샘플 코드의 예제를 도식화하면 Java와 Dll proxy와 dll 간의 관계로 풀어낼 수 있습니다.
만약 User32의 window lock api를 사용하면 java에서도 쉽게 window locking이 되게 할 수 있습니다. Kernel32도 사용 가능합니다.
3. 결론
JNI 의 불편함대신 간편하게 사용될 수 있는 JNA는 점차 보편화 되고 있어서, 오픈소스 코드를 분석하거나 또는 Native 모듈을 개발하는데 큰 도움이 될 것입니다.