七的博客

Java 语言调用CC++动态库

C语言

Java语言调用CC++动态库

最近工作中需要在 Java 程序中调用 dll 动态库 中的方法,以前没有专门去研究过。场景是要跟 C++ 端共享加密套件、Licence 校验等,所以是直接共享动态库的形式,两边使用一套算法。

动态库,又称为动态链接库。 简单来说就是一个包含了可以被多个程序同时使用的代码和数据的文件。Windows系统编译出来的动态库称作 Dynamic-Link Library ( *.dll 文件)。在 Linux 系统下编译出来的动态库称作 shared library ( *.so 文件)。

动态库用一种编程语言编写,然后可以被其他编程语言编写的程序调用,只要程序遵循相同的调用约定。如果你接触过 Python 的话,你可能不会陌生。 Python 经常就是干这种活,虽然本身语言性能不行,但是人家调用 C 或者C++ 编写的动态库,跑的贼快。

在之前项目里面找了下几种调用动态库的方式,主要分为2种:

  • JNI :Java Native Interface
  • JNA :Java Native Access

两种方式各有优缺点,适合在不同的场景下。

1. JNI

1.1 JNI是什么

JNI 就是 Java Native Interface 的缩写,通常是用于 Java 语言跟其他语言通信,最常用的就是跟 C/C++ 进行交互。 Java程序可以直接调用 C/C++ 的方法。

1.2 怎么去使用 JNI

使用 JNI 的大概流程如下:

  1. 编写 *.java 文件定义方法
  2. 编译 *.java 到 *.class
  3. 生成 C/C++ 头文件 *.h
  4. 编写 C/C++ 程序实现头文件方法
  5. 编译 C/C++ 程序为动态库
  6. Java 程序中引用动态库函数

1.2.1 先定义 Java 类以及方法

主要是使用 native 关键字定义你要调用 C/C++ 的方法。

public class HelloWorld {
    public native void displayHelloWorld();
}

1.2.2 对上面的类生成 C/C++ 文件头

调用 javac 进行编译。 Windows 环境下注意添加编码 UTF-8 ,不然编译会失败。

javac HelloWorld.java
//javac -encoding UTF-8 HelloWorld.java
javah -jni 你的包名.HelloWorld

注意事项: 上面调用 javah 时务必指定包名,不然会报错 java.lang.IllegalArgumentException: Not a valid class name

1.2.3 实现 C/C++ 方法

上面的 javah 命令执行完成后,会生成一个文件名类似 com_suny_jni_HelloWorld.h的文件,文件名中的 com_suny_jni_ 是包名,HelloWorld 是类名。头文件内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_suny_jni_HelloWorld */

#ifndef _Included_com_suny_jni_HelloWorld
#define _Included_com_suny_jni_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_suny_jni_HelloWorld
 * Method:    displayHelloWorld
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_suny_jni_HelloWorld_displayHelloWorld
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

开始写一个 C_HelloWorld.c 文件来实现接口

#include "com_suny_jni_HelloWorld.h"
#include <stdio.h>

JNIEXPORT void JNICALL Java_com_suny_jni_HelloWorld_displayHelloWorld(JNIEnv *env, jobject obj) {
    printf("Hello, world! by C !\n");
}

1.2.5 编译本地代码为动态库

根据不同的系统生成不同的动态库文件:

  • Windows 系统为 *.dll
  • Linux 系统为 *.so

执行命令:

gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o HelloWorld.dll C_HelloWorld.c

命令执行完后会生成 HelloWorld.dll 文件。

1.2.6 引用动态库文件

写一个 Java 类去调用动态库

public class HelloWorldCaller {


    static {
        System.load("HelloWorld.dll的绝对路径");
    }

    public static void main(String[] args) {
        new HelloWorld().displayHelloWorld();
    }
}

程序运行后控制台输出:

Hello, world! by C !

1.3 注意事项

  • 注意 32 位与 64 位的一致性:如果你的Java是32位的,那么生成的DLL也必须是32位的,反之亦然。

2.JNA

2.1 JNA 是什么

JNA 是 Java Native Access 的缩写,就是一个开源的Java框架。

它提供了一种简单的方式,允许Java代码直接调用本地库( 如 .DLL 或 .so 文件)中的函数,而不需要编写 JNI (Java Native Interface) 代码或生成JNI头文件。JNA 工作原理就是是在运行时动态映射 Java 的方法到本地库的函数。

JNA 目前也是在 Github 上进行维护: https://github.com/java-native-access/jna

项目非常的活跃,一直持续在更新

2.2 为什么要使用 JNA

  • 一定情况下简化开发: 上面的 JNI 使用方式相对还是比较复杂的,而且有个不方便的是需要先写 Java 生成好头文件后,再去由 C/C++ 实现。有时候提供动态库的不一定会帮你去实现这个接口,这种情况下 JNA 基于使用场景了。再或者说你在网上找到的动态库,你只能根据别人的文档来发起调用,你不可能让别人给你修改函数名入参等。

  • 显著减少因为内存管理引起的错误:由于 JNA 会自动处理类型转换和内存管理,因此减少了因手动处理这些问题而引入的 BUG。

2.3 怎么使用 JNA

使用 JNA 的大概流程如下:

  1. 项目中导入外部的动态库 *.dll 或者 *.so。
  2. Java 程序中引用动态库函数。

2.3.1 定义本地函数

首先编写一个 C 的类 C_HelloWorld_JNA.c

#include <stdio.h>

__declspec(dllexport) void displayHelloWorld() {
    printf("Hello, world! by C !\n");
}

2.3.2 编译文件

gcc -shared -o C_HelloWorld_JNA.dll C_HelloWorld_JNA.c

2.3.3 Java工程引入依赖

Java工程要引入 JNA 的依赖,以 Maven 为例:

<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna</artifactId>
    <version>5.5.0</version> 
</dependency>

2.3.4 定义接口映射

创建一个 Java 接口定义接口映射,通过 JNA 映射到 C 库中的函数

import com.sun.jna.Library;
import com.sun.jna.Native;

public interface HelloWorldLibrary extends Library {

    HelloWorldLibrary INSTANCE = Native.load("C_HelloWorld_JNA", HelloWorldLibrary.class);

    void displayHelloWorld();
}

2.3.4 Java程序调用动态库

public class HelloWorldCaller {

    static {
        System.load("你的动态库绝对路径地址 C_HelloWorld_JNA.dll");
    }


    public static void main(String[] args) {
        HelloWorldLibrary.INSTANCE.displayHelloWorld();
    }
}

程序运行后控制台输出:

Hello, world! by C !

2.4 JNA 的注意事项

2.4.1 平台兼容性

编写的 C 代码应该根据对应的操作系统进行编译,比如函数导出的声明,在Windows 上是 __declspec(dllexport) , 在 Linux 系统上你可能要替换成 __attribute__((visibility("default")))

2.4.2 错误被吃掉的问题

在 Java 接口中使用 JNA 可能不会直接抛出错误,这是个很可怕的问题。 所以你需要在 C 代码中妥善处理错误,在 Java 代码中也要进行检查。

2.4.3 考虑性能问题

JNA 虽然调用本地代码方便,但是相对于 JNI,性能上更没这么好,本质上是在运行时来动态解析和以及发起函数调用,这是会损耗一些性能的。

就好比你去一个人家里,你知道家的具体地址,跟你一路问路过去,花费的时间肯定不一样。

2.4.4 类型问题

跨语言调用虽然看起来方便,但是各种编程语言类型不一定匹配的,所以要仔细挑选合适的类型,否则运行过程中就会出问题。

类型映射如下:

Native Type Size Java Type Common Windows Types
char 8-bit integer byte BYTE, TCHAR
short 16-bit integer short WORD
wchar_t 1632-bit character char TCHAR
int 32-bit integer int DWORD
int boolean value boolean BOOL
long 3264-bit integer NativeLong LONG
long long 64-bit integer long __int64
float 32-bit FP float
double 64-bit FP double
char* C string String LPCSTR
void* pointer Pointer LPVOID, HANDLE, LPXXX

3. 遇到的错误

3.1 Can’t load IA 32-bit .dll on a AMD 64-bit platform

Exception in thread "main" java.lang.UnsatisfiedLinkError:   Can't load IA 32-bit .dll on a AMD 64-bit platform
	at java.lang.ClassLoader$NativeLibrary.load(Native Method)
	at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1941)
	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1857)
	at java.lang.Runtime.loadLibrary0(Runtime.java:870)
	at java.lang.System.loadLibrary(System.java:1122)
  • 原因:当前系统是 64位的, 加载的 .dll 为32位的。
  • 解决方法: 将 .dll 换成 64位的即可。

3.2 no xxxDLL in java.library.path

Exception in thread "main" java.lang.UnsatisfiedLinkError: no xxxDLL in java.library.path
	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
	at java.lang.Runtime.loadLibrary0(Runtime.java:870)
	at java.lang.System.loadLibrary(System.java:1122)

  • 原因:在环境变量中找不到动态库地址
  • 解决方法: 配置好环境变量。比如在 java.library.path 变量中添加动态库文件夹。

3.3 Directory separator should not appear in library name: E:\xxxx\xxxxDLL.dll

Exception in thread "main" java.lang.UnsatisfiedLinkError: Directory separator should not appear in library name: E:\xxxx\xxxxDLL.dll
	at java.lang.Runtime.loadLibrary0(Runtime.java:867)
	at java.lang.System.loadLibrary(System.java:1122)
  • 原因: System.loadLibrary() 方法指定的动态库的名称,而不包括路径或文件扩展名。
  • 解决方法: 调用 System.loadLibrary() 时传入动态库名称

3.4 方法不存在异常

Exception in thread "main" java.lang.UnsatisfiedLinkError: com.suny.test.JNITest.test(Ljava/lang/String;I)Ljava/lang/String;
	at com.suny.test.JNITest.test(Native Method)
	at com.suny.test.JNITest.main(JNITest.java:11)
  • 原因:没有调用 System.loadLibrary(“动态库名称”);
  • 解决方案: 调用 System.loadLibrary(“动态库名称”) 即可

4. 怎么去选择 JNA 或者 JNI

  • 如果不在意更低一点的性能,那么可以考虑 JNA 。JNA 的性能通常比 JNI 方式差一点,但是通过动态映射和反射,使得调用动态库变的很简单,只需要少量的 Java 代码即可完成调用。

  • 如果更在意性能,同时能接受更加多的工作量,那么可以使用 JNI 的方式。还有一点就是,如果团队对 C/C++ 和 Java 都掌握的比较深,那么 JNI 可能是更好的选择,JNI 可控性更强。

参考链接