VMWare에 CentOS7 64bit를 설치하여 테스트한 내용을 기반으로 작성하였습니다.
Spring Boot(for API), C++(for JNI Library)가 사용되었습니다.
64 bit gcc 컴파일러 설치, JAVA 설치가 필요합니다.
헤더파일 관련
cpp 파일 코딩 후 [.so 동적라이브러리 빌드 예시] 항목에 있는 라인을 순서대로 진행하면 so 파일이 생성됩니다.
linux 동적 라이브러리 지정
Spring 기준으로 jar를 실행할때 -Djava.library.path=/home/app/jniLibs 와 같이 라이브러리가 존재하는 경로를 기입해주어서 실행하여야합니다.
windows는 .dll만 사용 가능하며 linux는 .so 파일만 사용가능합니다.
C++개발 시 extern "C"를 선언하여야 합니다. 해당 코드가 왜 필요한지 아직 파악하지 못했습니다. extern "C"를 선언하지 않고 빌드하였을때는 .so 파일은 제대로 읽었지만 안에 있는 메서드를 찾지 못하는 에러가 발생하였습니다.
extern "C"는 C++에 C언어를 선언하는 것으로 C++의 기능을 제한하겠다는 의미입니다. 예를 들어 오버로딩은 C++만 가능 해서 extern "C"를 선언하여 메서드를 구성하면 오버로딩하지 못합니다. 추측하기로는 C언어는 오버로딩이 안되서 메서드 명 자체로 고유하기 때문에 명확하게 메서드를 찾을 수 있어서 가능했던거 같습니다. Java Class 헤더 파일과 C++코드에서 서로 동일한 메서드 명이 선언되어있어서 메서드를 찾지 못하는 오류가 났을 가능성도 있습니다.
블로그 포스팅을 보면 JAVA Class 헤더파일과 같이 사용했던데 제가 할때는 JAVA Class 헤더파일이 있으면 메서드를 찾지 못하는 오류만 났고 굳이 필요도 없었습니다. 해당 부분은 좀 더 확인이 필요합니다.
C++ 코드에서 JAVA Class를 가져와 사용하려면 메서드 내에서 사용하지 않더라도 파라미터 변수에 jclass를 받아야합니다. 없으면 전달한 객체의 필드값들을 사용할 수가 없는데 왜 그런건지 이유는 정확히 파악은 하지 못하였습니다.
해당 포스팅을 작성하면서 패키지명과 파일명을 바꿔서 파일명이나 패키지명에 오타가 있을 수도 있습니다. 실제로는 패키지명과 파일명이 절대 불일치 하지 않도록 맞춰줘야합니다.
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
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
/** 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;
}
#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;
}