java调用ddl动态链接库,以及遇到的一些坑
大家好,我是不会java的java废材生。某一天突然接到个小需求,要求用c++写一个函数并用java来调用,而且特地强调不允许通过网络请求的方式调用(当然作者比较废也不会写c++的服务接口),这就需要本地调用ddl动态链接库来实现了。作者以前没有做过类似是场景,不过java有性能瓶颈,所以一些偏复杂的功能可能就是用其他性能较好的语言来实现的(比如c、c++等)再在java里面调用那些实现好的功能,以下是作者做的案例并踩的一些坑,希望能帮助到大家。
什么是ddl
我们在实现类似上面需求之前,要大致了解一下什么是ddl。
ddl是一种文件的类型,在Windows开启文件后缀名展示的话能看到它是应用程序扩展文件,标准点叫动态链接库文件(Dynamic Link Library)。在Linux上,动态链接库的文件后缀为.so(有兴趣的朋友可以自行搜索)。
ddl文件是实现资源共享的一种方式,本质就是编译好的机器指令文件,直接打开就会看到一些乱码;动态是指只有在程序运行时才会去调用某个ddl,因此ddl性能开销就会小很多(静态链接的性能开销会大不少,具体有兴趣也可以百度)。
C++生成ddl的两种方式(使用clion工具)
在生成ddl之前,需要注意设置好编译环境,clion工具只提供了cmake项目构建工具。编译器的话一般选择用MinGW,MinGW要和jdk位数一样,64位的jdk对应64位的MinGW,MinGW下载链接。
编译环境配置
设置->构建、执行、部署->工具链,按照下图配置好确定即可。
新建一个c++项目
选择c++库,按下图勾选即可
其实选择可执行文件那个也是可以的,但我们简单打印个helloworld不需要执行测试。项目创建成功后会默认生成一个library.cpp文件和library.h文件。
方式一(适用于c和c++之间的调用)
1.代码编写
使用 __declspec(dllexport)标识符
新建一个cpp文件,直接在函数前面加上这个标识符就可以,该关键字可以将该函数标识为要导出的函数,如下面void hello()函数
c++
__declspec(dllexport) void hello(){
printf("helloworld\n");
}
注意事项:这种方式导出函数得到ddl之后,导出的函数名会改变,在编译链接的时候,c++会按照自己的规则篡改函数名称。
解决方式:在定义导出函数时加上extern"C"限定符
可以这样
c++
extern"C" __declspec(dllexport) void hello(){
printf("helloworld\n");
}
也可以这样将多个要导出了函数放一个块里
c++
extern"C" {
__declspec(dllexport) void hello(){
printf("helloworld\n");
}
}
完整代码附图如下
2.配置CMakeList.txt文件
在编写完上面代码之后就可以去生成ddl文件了。
找到项目根目录下的CMakeList.txt配置文件(创建项目的话会默认生成),加入如下内容
scss
cmake_minimum_required(VERSION 3.19) #创建项目默认就有
project(dlltest) #ddltest是项目名称,也是创建项目默认就有
set(CMAKE_CXX_STANDARD 11) #版本,有默认就默认,没默认选11
add_library(dlltest SHARED mytest.cpp)
因为创建的是c++库项目,所以配置里有add_library,如果是创建的是c++可执行文件的项目就要自己手动加入。
dlltest就是生成的ddl文件名称,mytest.cpp就要导出为dll的代码源文件
配置好之后如下图
3.构建项目生成dll文件
点击最上方那栏构建->构建项目
项目构建成功之后会在cmake-build-debug目录下生成ddl文件,不过默认加上了lib的前缀。
将这个dll文件复制到程序可以访问到的地方就可以调用那个void hello()函数了,如果访问不到会报错文件找不到。
注意:这一种方式少一些流程,但依然存在问题,只能解决c和c++之间调用函数命名的问题,生成了ddl给java调用的时候就会报错java.lang.UnsatisfiedLinkError,想用java调用就用方式二吧。
方式二(生成java可调用的ddl)
了解java调用ddl常见的三种方法
java调用dll常用有三种方法,分别是用jni、jna和jnative。
jni是jdk自带的,性能最好(推荐),其他两种是基于jni的封装。
本文使用jni的方法来调用。
了解java中native关键字
它用于声明一个方法是由本地代码实现的,也就是说这个方法的实现并不是用 Java 编写的,而是使用其他语言(比如 c/c++)编写的,并通过jni调用。
如果希望在 java 中调用dll中的函数,并且不希望函数名被改变,你可以使用 native 关键字声明一个本地方法,并在本地代码中实现这个方法。
1.在java中创建一个类声明native方法
java
public class MylibTest {
public native void hello(); //方法名要和c++里的函数名一一致
}
2.生成native方法对应的c++头文件
该头文件的作用是定义c++能够识别的java中方法对应的c++函数。
生成头文件需要用到命令,不同版本jdk所需命令不一样。
jdk1.8版本
javah命令,在MylibTest.java所在的目录下执行命令。
javah MylibTest.java
1.8版本以上
方式一
这个地方有坑,网上查的时候说是java8之后的版本用javac -h命令(但没补充,作者一执行就报错),这个命令是没问题但是如上面命令一样直接执行就会出问题。
我们能看到下面执行报错说无源文件。
那怎么解决能,指定一个输出源目录就可以了
-h 指定头文件的输出目录,.代表当前目录,test则是在当前目录下生成一个test目录
javac -h test MylibTest.java
执行之后就会在MylibTest.java所在目录生成一个test目录
执行成功
生成test目录
test目录下会有一个头文件。
方式二
在与方式一同样的目录位置执行如下命令,encoding指定编码为uft8
javac -encoding utf8 -h . MylibTest.java
作者jdk17这两种命令都用过了,实测没问题。
3.将生成的头文件引入c++项目
之前创建好了c++项目这里就不再创建了。
将生成的头文件复制粘贴到项目跟目录,生成的头文件里面用到了jni.h和jni_md.h头文件(jni.h和jni_md.h头文件在jdk目录下的include目录里面,找一找很快就能找到),也一并复制粘贴过来。
生成的头文件类容如下
arduino
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_stx_MylibTest */
#ifndef _Included_com_stx_MylibTest
#define _Included_com_stx_MylibTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_stx_MylibTest
* Method: hello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_stx_MylibTest_hello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
4.在当前目录新建一个cpp文件并实现函数
新建的cpp需要引入生成的头文件和jni.h头文件,并实现生成头文件里面的函数
arduino
#include "jni.h"
#include "com_stx_LibTest.h"
JNIEXPORT void JNICALL Java_com_stx_LibTest_hello (JNIEnv *, jobject){
printf("helloworld\n");
}
5.配置CMakeList.txt
在里面的add_library添加cpp和引用到生成的那个头文件(同前面的一样)
scss
add_library(dlltest SHARED com_stx_LibTest.h xxx.cpp)
6.构建生成dll
原理同方式一提到的一样。
7.将生成的ddl直接粘贴到jdk的bin目录下
bin目录是java在加载库时最优先访问的目录。
8.编写java测试类
java
public class LibTest {
static {
System.loadLibrary("libdlltest"); //这里的名字要和ddl的文件名一致
}
public native void hello();
public static void main(String[] args) {
new LibTest().hello();
}
}
9.运行测试
可以看到测试成功
总结
在第一次弄的时候,踩了不少坑,弄了一个下午没弄出来(可能是作者比较菜的原因)。作者没有因此而放弃,查了诸多文章之后给实现出来了,总的来说多试试、多看看文章、多看看书,总会有收获的。