안드로이드 애플리케이션이나 다른 일반적인 자바 애플리케이션을 작성하는 중 C / C ++로 코드를 작성해야하는 경우가 있습니다. Java는 모든 배열 액세스, 모든 타입 캐스트, 함수 호출 및 리턴 등에 대한 많은 보안 검사가있는 동적 언어입니다. 이러한 검사는 성능에 영향을 미치고 이미지를 조작하려는 경우 C로 작성하는 것이 좋습니다.
JNI - 자바 네이티브 인터페이스
JNI는 네이티브 C / C ++ 코드를 인터페이싱하기위한 java 확장입니다. 다음 상황에서 사용해야합니다.
- 알고리즘 실행 - 성능 향상 (보안 검사 없음 및 동적 기능 없음)
- 포인터로 작업 - 예를 들어 하드웨어에 액세스해야하는 경우
- Java에서 사용할 수없는 호출 함수 - 예를 들어 ioctl과 같은 일부 시스템 호출
동일한 주소 공간 - 다른 힙
Java는 가비지 컬렉터 인 자동 메모리 관리를 사용합니다. C / C ++에서 수동 메모리 관리 사용 (malloc-free / new-delete)
가비지 수집기는 메모리를 관리하고 조각을 최소화하고 불필요한 메모리 영역을 할당 해제하기 위해 객체를 이동합니다. 이것은 시스템이 프로세스 주소 공간에 2 개의 다른 힙을 생성하는 이유입니다. 하나는 Java 용이고 다른 하나는 native 용입니다. C 코드에서 C 코드로의 배열은 항상 주소로 처리됩니다. Java 코드에서 Java 코드로의 송신 배열은 항상 참조로 수행됩니다 . Java에서 C로 배열을 전송하는 사소한 JNI 구현은 값 으로 수행됩니다 (java 힙에서) 자바 배열이 있고 네이티브 함수로 보내고 싶다면 먼저 네이티브 힙에 복사해야합니다. 함수가 리턴 한 후 다시 복사하십시오. 똑똑한 구현에서는 가비지 수집기가 함수가 반환 될 때까지 배열을 고정하고 주소 만 보냅니다.
우리가 가지고있는 또 다른 문제는 문자열 조작입니다. C 문자열은 null로 끝나는 문자 ( '\ 0')로 끝나는 문자 배열입니다. Java에서는 속성과 메소드를 가진 객체이므로 java에서 C로 문자열을 보내는 동안 null로 끝나는 char을 추가해야하며 때때로 문자열을 원시 힙에 복사해야합니다.
내 예제에서는 Android Studio를 사용하여 기본 코드가있는 Android 애플리케이션을 작성합니다.Android에서는 JNI를 사용하기 위해 NDK를 설치해야합니다.
간단한 예 :
먼저 Gradle 스크립트에 기본 지원을 추가해야합니다.
| externalNativeBuild { cmake { path "CMakeLists.txt" } } |
빌드 시스템에 cmake를 사용하고 있으며 다른 모든 선언은 CMakeLists.txt 파일에 있습니다.
네이티브 지원을 추가하려면 먼저 라이브러리 (공유 객체 또는 DLL)를로드해야합니다.
| static { System . loadLibrary ( "native-lib" ) ; } |
이 코드는 libnative-lib.so 파일을로드합니다 (또는 Windows 응용 프로그램의 경우 native-lib.dll).
이제 java 클래스에서 native 함수를 선언해야합니다. - C 함수를 호출하려면이 함수를 호출해야합니다.
| public native int add ( int a , int b ) ; |
C의 심볼 이름은 Java_ [package] _ [class] _function이어야합니다. 따라서 com.devarea.jnitest 패키지의 MainActivity 클래스에서 네이티브 함수를 선언하려면 C 코드를 정확히 다음과 같이 작성해야합니다.
| extern "C" JNIEXPORT jint JNICALL Java_com_devarea_jnitest_MainActivity_add ( JNIEnv * env , jobject instance , jint a , jint b ) { return a + b ; } |
파일 확장자가 cpp이면 extern "C"가 필요하므로 기호 이름은 함수 이름으로 만 사용됩니다.
이 함수는 4 개의 매개 변수를 가져옵니다.
- JNIEnv - JNI 인터페이스의 포인터 - 많은 함수 포인터를 가지는 클래스
- jobject - Java 객체
- jint a, jint b - java 정수에 대한 typedef
함수를 정적으로 선언하면 jclass가됩니다. 두 번째 매개 변수는 Java 클래스 객체를 나타냅니다.
우리는 jobject와 jclass를 사용하여 C 코드의 자바 프로퍼티와 메소드에 접근 할 수있다 - 아래를 보라.
기본 유형
자바 원시 (byte, short, int, long, float, double, char, boolean)는 값으로 전달되므로 이들과 함께 사용하기 쉽다. C 코드에서 정확한 typedef를 사용한다.
문자열
문자열은 Java와 C에서 서로 다르므로 특별한 처리가 필요합니다.
Java 문자열을 C로 보내기 :
자바 코드 :
| public native int getStrLen ( String s ) ; |
C 코드 :
| extern "C" JNIEXPORT jint JNICALL Java_com_devarea_jnitest_MainActivity_getStrLen ( JNIEnv * env , jobject instance , jstring s_ ) { const char * s = env -> GetStringUTFChars ( s_ , 0 ) ; jint len = strlen ( s ) ; env -> ReleaseStringUTFChars ( s_ , s ) ; return len ; } |
먼저 자바 문자열을 원시 힙에 복사 한 다음 길이를 계산하고 기본 문자열을 해제합니다.
Java 힙에 문자열을 고정하고 포인터 만 보낼 수도 있습니다 (ReleaseStringUTFChars는 고정 해제에 필요합니다)
VM이 실제로 문자열을 복사했는지 알 필요가있는 경우 주소로 jboolean 변수를 두 번째 인수로 보낼 수 있습니다.
| jboolean b ; const char * s = env -> GetStringUTFChars ( s_ , & b ) ; if ( b ) puts ( "we have a copy" ) ; |
C에서 Java로 문자열 반환 :
자바 코드 :
| public native String stringFromJNI ( ) ; |
C 코드 :
| extern "C" JNIEXPORT jstring JNICALL Java_com_mabel_developer_jnitest_MainActivity_stringFromJNI ( JNIEnv * env , jobject /* this */ ) { std :: string hello = "Hello from C++" ; return env -> NewStringUTF ( hello . c_str ( ) ) ; } |
배열
C 코드에 java 배열을 보내려는 경우 :
Java 코드
| public static native int addArray ( int [ ] arr ) ; |
C 코드 :
| extern "C" JNIEXPORT jint JNICALL Java_com_devarea_MainActivity_addArray ( JNIEnv * env , jclass type , jintArray jarr ) { jint * arr = env -> GetIntArrayElements ( jarr , NULL ) ; int res = 0 ; int size = env -> GetArrayLength ( jarr ) ; for ( int i = 0 ; i < size ; i ++ ) res += arr [ i ] ; env -> ReleaseIntArrayElements ( jarr , arr , 0 ) ; return res ; } |
먼저 GetIntArrayElements를 사용하여 Java Array를 C로 복사 한 다음 배열 크기를 묻고 c 배열을 조작하고 놓습니다.
문자열 처리와 비슷합니다.
C에서 Java Array 반환 :
자바 코드 :
| public static native int [ ] getArray ( ) ; |
C 코드 :
| extern "C" JNIEXPORT jintArray JNICALL Java_com_mabel_devarea_MainActivity_getArray ( JNIEnv * env , jclass type ) { int arr [ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 0 } ; jintArray ret = env -> NewIntArray ( 10 ) ; env -> SetIntArrayRegion ( ret , 0 , 10 , arr ) ; return ret ; } |
글로벌 및 로컬 참조 :
로컬 및 글로벌 참조의 차이점을 이해하는 것이 매우 중요합니다. 네이티브 코드를 전달하고 객체를 처리하면 VM이 로컬 참조를 만들고 함수가 반환 할 때까지 객체가 이동하거나 해제되지 않습니다.
레퍼런스를 네이티브 코드의 글로벌 변수에 저장하고 다른 호출 중에 참조를 사용하려면 어떻게해야합니까?
자바 코드 :
| public static native void saveArray ( int [ ] arr ) ; public static native int addArray ( ) ; |
saveArray에서 참조를 저장하고 addArray에 사용합니다.
C 코드 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | jintArray gl_arr ; extern "C" JNIEXPORT void JNICALL Java_com_devarea_jnitest_MainActivity_saveArray ( JNIEnv * env , jclass type , jintArray arr_ ) { gl_arr = arr_ ; } extern "C" JNIEXPORT jint JNICALL Java_com_devarea_MainActivity_addArray ( JNIEnv * env , jclass type ) { jint * arr = env -> GetIntArrayElements ( gl_arr , NULL ) ; int res = 0 ; int size = env -> GetArrayLength ( gl_arr ) ; for ( int i = 0 ; i < size ; i ++ ) res += arr [ i ] ; env -> ReleaseIntArrayElements ( gl_arr , arr , 0 ) ; return res ; } |
이것은 잘못된 코드입니다 !!!
VM에 객체에 대한 참조를 저장하고 있으므로 이동하지 않거나 호출간에 해제 할 수 있도록 VM에 알려야합니다. 이를 위해서는 전역 참조를 선언해야합니다.
올바른 C 코드 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | jintArray gl_arr ; extern "C" JNIEXPORT void JNICALL Java_com_devarea_jnitest_MainActivity_saveArray ( JNIEnv * env , jclass type , jintArray arr_ ) { gl_arr = ( jintArray ) env -> NewGlobalRef ( ( jobject ) arr_ ) ; } extern "C" JNIEXPORT jint JNICALL Java_com_devarea_jnitest_MainActivity_addArray ( JNIEnv * env , jclass type ) { jint * arr = env -> GetIntArrayElements ( gl_arr , NULL ) ; int res = 0 ; int size = env -> GetArrayLength ( gl_arr ) ; for ( int i = 0 ; i < size ; i ++ ) res += arr [ i ] ; env -> ReleaseIntArrayElements ( gl_arr , arr , 0 ) ; env -> DeleteGlobalRef ( gl_arr ) ; return res ; } |
직접 버퍼 액세스
때로는 많은 양의 데이터로 작업해야하며 VM이 데이터를 복사하지 않도록해야합니다. 이 경우 ByteBuffer를 사용할 수 있습니다.
자바 코드 :
| ByteBuffer buf = ByteBuffer . allocateDirect ( 1000 ) ; processData ( buf ) ; . . . . . public static native void processData ( ByteBuffer buf ) ; |
C 코드 :
| extern "C" JNIEXPORT void JNICALL Java_com_devarea_jnitest_MainActivity_processData ( JNIEnv * env , jclass type , jobject buf ) { char * data = ( char * ) env -> GetDirectBufferAddress ( buf ) ; int len = env -> GetDirectBufferCapacity ( buf ) ; for ( int i = 0 ; i < len ; i ++ ) { data [ i ] = 'X' ; } } |
둘 이상의 기능에서 동일한 버퍼를 사용하는 경우 매우 유용 할 수 있습니다. 또한 네이티브 코드 (malloc 사용) 내에서 버퍼를 만들고 env -> NewDirectByteBuffer를 사용하여 Java 코드로 반환 할 수 있습니다
반사
JNI에서 유일하게 지원되는 타입은 문자열과 배열이고, 다른 객체는 jobject로 전달되며, 리플렉션을 사용해야하는 데이터, 속성 및 메서드에 액세스하려는 경우 :
예제 - 자바 코드 :
| package com . example . testjni ; public class Nlib { public native int func ( int a , int b ) ; private int getNum ( int a ) { return a + 100 ; } static { System . loadLibrary ( "testJNI" ) ; } } |
"func"는 멤버 함수이므로 Nlib 객체를 만들고 func을 호출 할 때 사용해야합니다.
| Nlib obj = new Nlib ( ) obj . func ( ) |
obj (this)가 jobject로 전달됩니다.
C 코드 :
| JNIEXPORT jint JNICALL Java_com_example_testjni_Nlib_func ( JNIEnv * env , jobject obj , jint a , jint b ) { jclass cl = env -> GetObjectClass ( obj ) ; jmethodID m = env -> GetMethodID ( cl , "getNum" , "(I)I" ) ; jint num = env -> CallIntMethod ( obj , m , a + b ) ; return num ; } |
이 예에서는 obj에 대한 Class 객체를 검색 한 다음 리플렉션을 사용하여 getNum 함수의 메서드 객체를 가져 와서 정수를 매개 변수로 가져 와서 정수를 반환합니다. 마지막으로 우리는 객체 obj를 사용하여 함수를 호출하고 인자로 + b를 전송합니다.
이 메서드를 사용하면 모든 함수를 호출 할 수 있지만 작성하기가 복잡합니다 (이 예제와 같은 private 함수를 호출 할 수도 있음)
JNI_OnLoad
네이티브 라이브러리를 초기화하는 함수를 작성할 수 있습니다.이 함수의 한 용도는 함수 포인터 테이블을로드하여 긴 함수 이름을 제거하는 것입니다. 예를 들면 다음과 같습니다.
자바 코드 :
| public class MyLib { static { System . loadLibrary ( "mylib" ) ; } public native static long f1 ( long n ) ; public native static long f2 ( long n ) ; public native static long f3 ( long n ) ; } |
C ++ 코드 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | namespace com_example_cpptest { static jlong myf1 ( JNIEnv * env , jclass clazz , jlong n ) { // implement; } static jlong myf2 ( JNIEnv * env , jclass clazz , jlong n ) { // implement; } static jlong myf3 ( JNIEnv * env , jclass clazz , jlong n ) { // implement; } static JNINativeMethod method_table [ ] = { { "f1" , "(J)J" , ( void * ) myf1 } , { "f2" , "(J)J" , ( void * ) myf2 } , { "f3" , "(J)J" , ( void * ) myf3 } } ; } using namespace com_example_cpptest ; jint JNI_OnLoad ( JavaVM * vm , void * reserved ) { JNIEnv * env ; javaVM = vm ; if ( vm -> GetEnv ( reinterpret_cast < void * * > ( &env ) , JNI_VERSION_1_6 ) != JNI_OK ) { return JNI_ERR ; } else { jclass clazz = env -> FindClass ( "com/devarea/jnitest/MyLib" ) ; if ( clazz ) { jint ret = env -> RegisterNatives ( clazz , method_table , sizeof ( method_table ) / sizeof ( method_table [ 0 ] ) ) ; env -> DeleteLocalRef ( clazz ) ; return ret == 0 ? JNI_VERSION_1_6 : JNI_ERR ; } else { return JNI_ERR ; } } } |
코드에서 알 수 있듯이 이런 식으로 함수 포인터를 사용하기 때문에 긴 기호 이름을 가진 함수를 선언 할 필요가 없습니다.
네이티브 스레드로 작업하기
때로는 네이티브 코드에 스레드를 만들려고합니다. 예를 들어 작업을 비동기로 만들려면 네이티브 시스템 호출 또는 함수를 사용하면됩니다.
| static jlong fibNR ( JNIEnv * env , jclass clazz , jlong n ) { pthread_t t1 ; pthread_create ( & t1 , NULL , threadfn , ( void * ) n ) ; return 0 ; } |
이제 간단히 threadfn 함수를 작성하십시오. 그러나 당신이 JNI 인터페이스에 접근하기를 원하지 않는다면 - 매개 변수 env는 각각의 쓰레드를 위해 만들어지고 만약 당신이 커스텀 쓰레드에서 그것을 사용하고자한다면 당신은 하나를 생성해야한다. 이 작업은 AttachCurrentThread를 사용하여 수행됩니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | static void * threadfn ( void * p ) { jlong res = longlastfn ( ( long ) p ) ; JNIEnv * env ; int n = javaVM -> AttachCurrentThread ( & env , NULL ) ; // use env to access any function javaVM -> DetachCurrentThread ( ) ; return ( void * ) res ; } static jlong fibNR ( JNIEnv * env , jclass clazz , jlong n ) { pthread_t t1 ; pthread_create ( & t1 , NULL , threadfn , ( void * ) n ) ; return 0 ; } |
JNI_OnLoad에 전역 변수로 저장하거나 스레드를 만드는 함수에서 env에서 가져 와서 javaVM에 액세스 할 수 있습니다.
| JavaVM * vm ; env -> GetJavaVM ( & vm ) ; |
예외 처리
다음 두 가지 경우에 예외를 사용할 수 있습니다.
Java 예외를 생성하는 원시 코드 호출 :
자바 코드 :
| public native void getException ( ) throws IOException ; |
C 코드 :
| extern "C" JNIEXPORT void JNICALL Java_com_mabel_developer_jnitest_MainActivity_getException ( JNIEnv * env , jclass type ) { // Do something and if you want to throw exception: jclass cl = env -> FindClass ( "java/io/IOException" ) ; env -> ThrowNew ( cl , "error from JNI" ) ; return ; } |
두 번째 경우는 native에서 Java 코드를 호출하고 예외를 throw하는지 확인합니다.
| env -> CallIntMethod ( obj , method , 10 , 20 ) ; if ( env -> ExceptionCheck ( ) ) { // handle exception env -> ExceptionClear ( ) ; } |
안녕. 당신은 자바와 배열 경계 검사에 대해 매우 잘못되었습니다. 내부 루프에서 최적화되었습니다. 모든 유형 변환에 대해 점검이 수행되지 않습니다. 주제에 대해 읽어보십시오 ...