JavaJNI调用C/C++代码

记于:2024-04-14
地点:浙江省·温州市·家里
天气:阴雨

背景#

因为一个项目需要Java JNI调用C++ SDK,只有编译好的Windows DLL,没有对应的源码。

测试#

简单demo#

JniTest.java

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.yeshimin.test.jni;

import java.lang.reflect.Field;
import java.util.Arrays;

public class JniTest {

// 设置dll加载路径
static {
try {
String path = "C:\\Users\\ysd\\IdeaProjects\\test\\dll";
// 无效
// System.setProperty("java.library.path", path);
// 需要使用这种方式
// 参考:https://fahdshariff.blogspot.com/2011/08/changing-java-library-path-at-runtime.html
addLibraryPath(path);
} catch (Exception e) {
throw new RuntimeException(e);
}

System.loadLibrary("jnitest");
}

/**
* Adds the specified path to the java library path
*
* @param pathToAdd the path to add
* @throws Exception
*/
public static void addLibraryPath(String pathToAdd) throws Exception {
final Field usrPathsField = ClassLoader.class.getDeclaredField("usr_paths");
usrPathsField.setAccessible(true);

// get array of paths
final String[] paths = (String[]) usrPathsField.get(null);

// check if the path to add is already present
for (String path : paths) {
if (path.equals(pathToAdd)) {
return;
}
}

//add the new path
final String[] newPaths = Arrays.copyOf(paths, paths.length + 1);
newPaths[newPaths.length - 1] = pathToAdd;
usrPathsField.set(null, newPaths);
}

// ================================================================================

// 本地方法
private static native String sayHello(String name);

public static void main(String[] args) {
String result = sayHello("ysm");
System.out.println("result: " + result);
}
}

生成本地方法对应的C++代码的头部文件;
可以使用javah或javac(jdk版本>=9)命令;
com_yeshimin_test_jni_JniTest.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_yeshimin_test_JniTest */

#ifndef _Included_com_yeshimin_test_jni_JniTest
#define _Included_com_yeshimin_test_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHello
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

接下需要根据头部文件中函数定义进行实现
JniTest.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "com_yeshimin_test_jni_JniTest.h"
// <jni.h>里已包含<stdio.h>

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHello
(JNIEnv *env, jclass cls, jstring j_str) {

const char *c_str = NULL;
char buff[128] = {0};
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL) {
printf("out of memory\n");
return NULL;
}
sprintf(buff, "Hello, %s\n", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env, buff);
}

编译

1
2
3
4
5
6
# 编译JniTest.c,输出jnitest.dll,并将其放到自定义dll目录下
# 我的自定义dll目录为C:\Users\ysd\IdeaProjects\test\dll
gcc -m64 -shared -o jnitest.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" .jni\JniTest.c

# jni需要jni.h相关头部,这里需要指定头部所在目录;具体位置因系统和jdk版本而异
# -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32"

运行

1
result: Hello, ysm

不同包下#

场景为,调用点所在的包(和类)与目标本地代码的声明不一致时,调用会报错;
!!!重要前提,场景假设为,目标代码是编译好的dll;直接源码编译到一起的不算~

注意,JniTest.java和JniTestInternal.c是在不同包下,JniTestInternal是目标代码,JniTest是调用类

JniTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.yeshimin.test.jni;

import java.lang.reflect.Field;
import java.util.Arrays;

public class JniTest {

// 设置dll加载路径
static {
// 略...

System.loadLibrary("jnitestinternal");
}

// 本地方法
private static native String sayHelloInternal(String name);

public static void main(String[] args) {
String result = sayHelloInternal("ysm");
System.out.println("result: " + result);
}
}

org_sample_JniTestInternal.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_sample_JniTest */

#ifndef _Included_org_sample_JniTestInternal
#define _Included_org_sample_JniTestInternal
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: org_sample_JniTestInternal
* Method: sayHelloInternal
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_org_sample_JniTestInternal_sayHelloInternal
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

JniTestInternal.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "org_sample_JniTestInternal.h"

/*
* Class: org_sample_JniTestInternal
* Method: sayHelloInternal
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_org_sample_JniTestInternal_sayHelloInternal
(JNIEnv *env, jclass cls, jstring j_str) {
const char *c_str = NULL;
char buff[128] = {0};
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL) {
printf("out of memory\n");
return NULL;
}
sprintf(buff, "Hello, %s\n", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env, buff);
}

编译

1
2
# 编译JniTestInternal.c,输出jnitestinternal.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitestinternal.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" .jni\JniTestInternal.c

运行

1
result: Hello, ysm

如果将类名修改,使得调用点所在包(和类名)与目标定义不一致,比如将类名修改为”OtherName”,编译运行后,将会报错:

1
2
3
Exception in thread "main" java.lang.UnsatisfiedLinkError: org.sample.OtherName.sayHelloInternal(Ljava/lang/String;)Ljava/lang/String;
at org.sample.OtherName.sayHelloInternal(Native Method)
at org.sample.OtherName.main(OtherName.java:60)

解决方案
使用“代理”的方式调用目标方法;即在同包名(和类名)对应的本地实现中包含目标头部,并且封装一个方法进行目标调用;具体见下面代码

JniTest.java

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
package com.yeshimin.test.jni;

import java.lang.reflect.Field;
import java.util.Arrays;

public class JniTest {

// 设置dll加载路径
static {
// 略...

System.loadLibrary("jnitest");
System.loadLibrary("jnitestinternal"); // 依赖的dll也要加载
}

private static native String sayHello(String name);

private static native String sayHelloByProxy(String name);

private static native String sayHelloInternal(String name);

public static void main(String[] args) {

// 调用同包名下的本地方法;执行成功
String result = sayHello("ysm");
System.out.println("result: " + result);

// “代理”方式下,调用不同包名下的本地方法;执行成功
String result2 = sayHelloByProxy("ysm");
System.out.println("result2: " + result2);

// 调用不同包名下的本地方法;执行报错
String result3 = sayHelloInternal("ysm");
System.out.println("result3: " + result3);
}
}

com_yeshimin_test_jni_JniTest.h

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
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_yeshimin_test_JniTest */

#ifndef _Included_com_yeshimin_test_jni_JniTest
#define _Included_com_yeshimin_test_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHello
(JNIEnv *, jclass, jstring);

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHelloByProxy
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHelloByProxy
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

JniTest.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
#include "com_yeshimin_test_jni_JniTest.h"
#include "org_sample_JniTestInternal.h"

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHello
(JNIEnv *env, jclass cls, jstring j_str) {

const char *c_str = NULL;
char buff[128] = {0};
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL) {
printf("out of memory\n");
return NULL;
}
sprintf(buff, "Hello, %s\n", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env, buff);
}

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: sayHelloByProxy
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_yeshimin_test_jni_JniTest_sayHelloByProxy
(JNIEnv *env, jclass cls, jstring j_str) {

return Java_org_sample_JniTestInternal_sayHelloInternal(env, cls, j_str);
}

org_sample_JniTestInternal.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_sample_JniTest */

#ifndef _Included_org_sample_JniTestInternal
#define _Included_org_sample_JniTestInternal
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: org_sample_JniTestInternal
* Method: sayHelloInternal
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_org_sample_JniTestInternal_sayHelloInternal
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

JniTestInternal.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "org_sample_JniTestInternal.h"

/*
* Class: org_sample_JniTestInternal
* Method: sayHelloInternal
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_org_sample_JniTestInternal_sayHelloInternal
(JNIEnv *env, jclass cls, jstring j_str) {
const char *c_str = NULL;
char buff[128] = {0};
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL) {
printf("out of memory\n");
return NULL;
}
sprintf(buff, "Hello, %s\n", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str);
return (*env)->NewStringUTF(env, buff);
}

编译

1
2
3
4
5
6
7
# 编译JniTestInternal.c,输出jnitestinternal.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitestinternal.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" .jni\JniTestInternal.c

# 编译JniTest.c,输出jnitest.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitest.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" -L"C:\Users\ysd\IdeaProjects\test\dll" -l"jnitestinternal" .jni\JniTest.c

# -L"C:\Users\ysd\IdeaProjects\test\dll" -l"jnitestinternal" // 用于指定依赖的dll

运行

1
2
3
4
5
6
7
result: Hello, ysm

result2: Hello, ysm

Exception in thread "main" java.lang.UnsatisfiedLinkError: com.yeshimin.test.jni.JniTest.sayHelloInternal(Ljava/lang/String;)Ljava/lang/String;
at com.yeshimin.test.jni.JniTest.sayHelloInternal(Native Method)
at com.yeshimin.test.jni.JniTest.main(JniTest.java:69)

调用dll#

场景跟上面【不同包下】类似,区别为目标是第三方sdk中的dll文件,且没有头部文件

JniTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.yeshimin.test.jni;

import java.lang.reflect.Field;
import java.util.Arrays;

public class JniTest {

// 设置dll加载路径
static {
// 略...

System.loadLibrary("jnitest");
System.loadLibrary("jnitestinternal"); // 依赖的dll也要加载
}

private static native int dynamicInvoke();

public static void main(String[] args) {
int intResult = dynamicInvoke();
System.out.println("intResult: " + intResult);
}
}

com_yeshimin_test_jni_JniTest.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_yeshimin_test_JniTest */

#ifndef _Included_com_yeshimin_test_jni_JniTest
#define _Included_com_yeshimin_test_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_yeshimin_test_jni_JniTest
* Method: dynamicInvoke
* Signature: (Ljava/lang/Integer;)Ljava/lang/Integer;
*/
JNIEXPORT jint JNICALL Java_com_yeshimin_test_jni_JniTest_dynamicInvoke
(JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

JniTest.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
#include <windows.h>
#include <stdbool.h>
#include "com_yeshimin_test_jni_JniTest.h"
#include "org_sample_JniTestInternal.h"

/*
* Class: com_yeshimin_test_jni_JniTest
* Method: dynamicInvoke
* Signature: (Ljava/lang/Integer;)Ljava/lang/Integer;
*/
JNIEXPORT jint JNICALL Java_com_yeshimin_test_jni_JniTest_dynamicInvoke
(JNIEnv *env, jclass cls) {

typedef int (__stdcall *TargetMethodPtr)(); // 声明 TargetMethodPtr 类型,名称自定义
TargetMethodPtr targetMethod;

HMODULE hModule = LoadLibrary(TEXT("Log_MD_VC120_v3_1.dll")); // 加载DLL
if (hModule != NULL) {
targetMethod = (TargetMethodPtr)GetProcAddress(hModule, "?ConfigureFromEnvironment@CLog@GenICam_3_1@@SA_NXZ"); // 获取函数地址
if (targetMethod != NULL) {
printf("function found");
bool result = targetMethod(); // 调用DLL函数
FreeLibrary(hModule); // 释放DLL模块
return result ? 1 : 0;
} else {
printf("function not found");
}
FreeLibrary(hModule); // 释放DLL模块
} else {
// getLastError
DWORD dwError = GetLastError();
printf("Error code: %lu\n", dwError);
}
return -1; // 返回 NULL 表示失败
}

编译

1
2
3
4
5
6
7
# 编译JniTestInternal.c,输出jnitestinternal.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitestinternal.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" .jni\JniTestInternal.c

# 编译JniTest.c,输出jnitest.dll,并将其放到自定义dll目录下
gcc -m64 -shared -o jnitest.dll -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include" -I"C:\Program Files\Eclipse Adoptium\jdk-8.0.402.6-hotspot\include\win32" -L"C:\Users\ysd\IdeaProjects\test\dll" -l"jnitestinternal" .jni\JniTest.c

# -L"C:\Users\ysd\IdeaProjects\test\dll" -l"jnitestinternal" // 用于指定依赖的dll

运行
将上面两个dll以及用到的第三方dll及其依赖dll放到项目根目录下才行正常运行,原因未知,见【遇到的问题】第3点

1
intResult: 1

遇到的问题#

1.提示32位代码在64位架构下跑不通;是下载错了mingw架构,应该下载64位的
2.指定自定义dll目录无效;解决方法见上面【简单demo】代码
3.在【调用dll】测试过程中,dll文件放到自定义dll目录下,链接不到,放到项目根目录下就可以,暂时搞不清楚
4.在【调用dll】测试过程中,一直获取不到目标函数;
开始是通过dllexp工具查看的函数函数名称为 public: static bool __cdecl GenICam_3_1::CLog::ConfigureFromEnvironment(void),
也试过 GenICam_3_1::CLog::ConfigureFromEnvironment(void) 和 GenICam_3_1::CLog::ConfigureFromEnvironment,都不行
后来是通过visual studio的dumpbin工具查看到dll的正确函数名 ?ConfigureFromEnvironment@CLog@GenICam_3_1@@SA_NXZ

参考资料#