(JAVA / C++) JNI를 이용한 연동(CentOS7 x86_64)

이동기·2022년 10월 23일
2
post-thumbnail

[참고사항]

  1. VMWare에 CentOS7 64bit를 설치하여 테스트한 내용을 기반으로 작성하였습니다.
    Spring Boot(for API), C++(for JNI Library)가 사용되었습니다.

  2. 64 bit gcc 컴파일러 설치, JAVA 설치가 필요합니다.

  3. 헤더파일 관련

    • windows에 있는 헤더 파일로 .so 파일로 컴파일 시 에러가 나거나 linux에서 사용 불가능한 so파일로 빌드 됩니다
    • linux에서 file {so파일명} 명령어를 쳤을때 window라는 단어가 들어가면 사용 불가능한 파일입니다.
  4. cpp 파일 코딩 후 [.so 동적라이브러리 빌드 예시] 항목에 있는 라인을 순서대로 진행하면 so 파일이 생성됩니다.

  5. linux 동적 라이브러리 지정

    • /etc/profile에서 LD_LIBRARY_PATH로 라이브러리경로를 지정해주어야합니다.
    • ldd {$LD_LIBRARY_PATH/so파일명}을 했을때 error가 발생하지 않아야합니다.
    • profile 파일 수정 후 sudo source profile 명령어로 수정내용을 적용해야합니다.
    • LD_LIBRARY_PATH로 지정해둔 경로에 .so 파일 업로드 후 sudo ldconfig 명령어로 동적 라이브러리 캐시를 갱신 시켜 적용해야합니다.
  6. Spring 기준으로 jar를 실행할때 -Djava.library.path=/home/app/jniLibs 와 같이 라이브러리가 존재하는 경로를 기입해주어서 실행하여야합니다.

    • 해당 내용은 loadLibrary 기능을 사용하기 위함입니다.
    • 예시) nohup java -jar -Dspring.profiles.active=onpremise -Djava.library.path=/home/app/jniLibs {jar명}
  7. windows는 .dll만 사용 가능하며 linux는 .so 파일만 사용가능합니다.

    • 서로 다른 os에서 빌드하여 파일을 옮길 시 사용 불가능한 현상이 있습니다.
    • 동적라이브러리 파일명이 libcEngine.so 또는 libcEngine.dll 일때 OS별로 loadLibrary 기능 사용에 차이가 있습니다. 공통적으로 파일의 확장자는 무시합니다.
    • .dll : System.loadLibrary("/src/main/java/libcEngine") -> Spring Boot 프로젝트 내부에 dll이 있다는 가정
    • .so : System.loadLibrary("cEngine") -> -Djava.library.path로 전달된 argument로 라이브러리 위치를 판단하며 파일명 앞에 lib를 제외함
  8. C++개발 시 extern "C"를 선언하여야 합니다. 해당 코드가 왜 필요한지 아직 파악하지 못했습니다. extern "C"를 선언하지 않고 빌드하였을때는 .so 파일은 제대로 읽었지만 안에 있는 메서드를 찾지 못하는 에러가 발생하였습니다.

  9. extern "C"는 C++에 C언어를 선언하는 것으로 C++의 기능을 제한하겠다는 의미입니다. 예를 들어 오버로딩은 C++만 가능 해서 extern "C"를 선언하여 메서드를 구성하면 오버로딩하지 못합니다. 추측하기로는 C언어는 오버로딩이 안되서 메서드 명 자체로 고유하기 때문에 명확하게 메서드를 찾을 수 있어서 가능했던거 같습니다. Java Class 헤더 파일과 C++코드에서 서로 동일한 메서드 명이 선언되어있어서 메서드를 찾지 못하는 오류가 났을 가능성도 있습니다.

  10. 블로그 포스팅을 보면 JAVA Class 헤더파일과 같이 사용했던데 제가 할때는 JAVA Class 헤더파일이 있으면 메서드를 찾지 못하는 오류만 났고 굳이 필요도 없었습니다. 해당 부분은 좀 더 확인이 필요합니다.

  11. C++ 코드에서 JAVA Class를 가져와 사용하려면 메서드 내에서 사용하지 않더라도 파라미터 변수에 jclass를 받아야합니다. 없으면 전달한 객체의 필드값들을 사용할 수가 없는데 왜 그런건지 이유는 정확히 파악은 하지 못하였습니다.

    • 예시) extern "C" JNIEXPORT jint JNICALL Java_com_test_c_Connector_testSum(JNIEnv* env, jclass _clazz, jobject obj) { }
  12. 해당 포스팅을 작성하면서 패키지명과 파일명을 바꿔서 파일명이나 패키지명에 오타가 있을 수도 있습니다. 실제로는 패키지명과 파일명이 절대 불일치 하지 않도록 맞춰줘야합니다.

[CentOS7 환경변수 설정 예시(vi /etc/profile)]

export PATH=$PATH:/usr/bin/gcc:/home/app/jniLibs
export JAVA_HOME=$JAVA_HOME:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.332.b09-1.el7_9.x86_64
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/app/jniLibs

[.so 동적라이브러리 빌드 예시]

  • m64 : 64bit로 빌드
  • I : 참조할 외부 폴더
g++ -fPIC -c libcEngine.cpp -m64 -I/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.332.b09-1.el7_9.x86_64/include -I/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.332.b09-1.el7_9.x86_64/include/linux
g++ -shared -Wl,-soname,libcEngine.so -o libcEngine.so libcEngine.o -m64 -I/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.332.b09-1.el7_9.x86_64/include -I/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.332.b09-1.el7_9.x86_64/include/linux

[JAVA Code Example]


/** Restful API Contoller */
package com.test.api.jni;

import com.test.data.TestIntClass;
import com.test.c.Connector;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/api/v1/jni/test")
@RequiredArgsConstructor
public class JniController {

    @GetMapping
    @ResponseStatus(HttpStatus.OK)
    public int findAllNodeListOfSite(int a, int b) {
        TestIntClass testIntClass = new TestIntClass();
        testIntClass.input1 = a;
        testIntClass.input2 = b;

        Connector cConnector = new Connector();
        log.error("start");

        int result = cConnector.testSum(testIntClass);

        log.error("result :: " + result);
        log.error("end");
        
        return result;
    }
}
/** JNI Service */
package com.test.c;

import com.test.data.TestIntClass;

public class Connector {

    // ADD JNI
    /** 
       windows : cEngine.dll
       linux : cEngine.so
    */
    static {
        //System.loadLibrary("/src/main/java/libcEngine"); // at the windows(.dll)
        System.loadLibrary("cEngine"); // at the linux(.so)
    }

    public native int testSum(TestIntClass testIntClass);

}
/** JNI Parameter Domain */
package com.test.data;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class TestIntClass {
    public int input1 = 0;
    public int input2 = 0;
    public int output = 0;
}

[C++ Code Example]


#include <stdio.h>
#include "/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.332.b09-1.el7_9.x86_64/include/jni.h"

struct TestIntClass
{
        int input1;
        int input2;
        int output;
};

extern "C" JNIEXPORT jint JNICALL Java_com_test_c_Connector_testSum
(JNIEnv* env, jclass _clazz, jobject obj) {
        printf("start c code\n");

        jclass clazz;
        clazz = env->GetObjectClass(obj);

        jfieldID fid1;
        jfieldID fid2;
        jfieldID fid3;

        TestIntClass testIntCls;
        fid1 = env->GetFieldID(clazz, "input1", "I");
        testIntCls.input1 = env->GetIntField(obj, fid1);
        printf("\n inputDATA1 :: %d", testIntCls.input1);

        fid2 = env->GetFieldID(clazz, "input2", "I");
        testIntCls.input2 = env->GetIntField(obj, fid2);
        printf("\n inputDATA2 :: %d", testIntCls.input2);

        fid3 = env->GetFieldID(clazz, "output", "I");
        testIntCls.output = env->GetIntField(obj, fid3);
        printf("\n ouputDATA :: %d", testIntCls.output);

        testIntCls.output = testIntCls.input1 + testIntCls.input2;
        printf("\nresultDATA :: %d", testIntCls.output);

        printf("\n end c code");

        return testIntCls.output;
}

[linux .so 테스트 결과]

[windows .dll 테스트 결과]

profile
개발자가 되고 싶은 '개'발자입니다.

0개의 댓글